From e89a82dac11d3a3b139c64236d716c3f25d86c5f Mon Sep 17 00:00:00 2001 From: Juda Kaleta Date: Sat, 26 Jul 2014 16:19:10 +0200 Subject: [PATCH] Settings options in embed tags --- CHANGES.rst | 4 + embed_video/backends.py | 48 +++--- embed_video/settings.py | 3 + embed_video/templatetags/embed_video_tags.py | 153 +++++++----------- embed_video/tests/backends/__init__.py | 21 +-- .../tests/backends/tests_videobackend.py | 20 +++ .../templatetags/tests_embed_video_tags.py | 81 ++++------ 7 files changed, 142 insertions(+), 188 deletions(-) create mode 100644 embed_video/tests/backends/tests_videobackend.py diff --git a/CHANGES.rst b/CHANGES.rst index b838bbc..229b4aa 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Release 1.0.0 (dev) ------------------- +**Backward incompatible changes:** +- filter `embed_video_tags.embed` has been removed + +Backward compatible changes: *No changes yet.* diff --git a/embed_video/backends.py b/embed_video/backends.py index 5eb1595..3bfbe5b 100644 --- a/embed_video/backends.py +++ b/embed_video/backends.py @@ -1,6 +1,6 @@ import re -import requests import json +import requests try: # Python <= 2.7 @@ -11,14 +11,14 @@ except ImportError: import urllib.parse as urlparse from urllib.parse import urlencode -from django.conf import settings +from django.http import QueryDict from django.template.loader import render_to_string from django.utils.functional import cached_property from django.utils.safestring import mark_safe -from django.utils.datastructures import SortedDict from .utils import import_by_path -from .settings import EMBED_VIDEO_BACKENDS, EMBED_VIDEO_TIMEOUT +from .settings import EMBED_VIDEO_BACKENDS, EMBED_VIDEO_TIMEOUT, \ + EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY class EmbedVideoException(Exception): @@ -112,7 +112,9 @@ class VideoBackend(object): ``{{ width }}``, ``{{ height }}`` """ - def __init__(self, url, is_secure=False, query=None): + default_query = '' + + def __init__(self, url, is_secure=False): """ First it tries to load data from cache and if it don't succeed, run :py:meth:`init` and then save it to cache. @@ -120,22 +122,17 @@ class VideoBackend(object): self.is_secure = is_secure self.backend = self.__class__.__name__ self._url = url - self.update_query(query) - - def update_query(self, query=None): - self._query = SortedDict(self.get_default_query()) - if query is not None: - self._query.update(query) + self.query = QueryDict(self.default_query, mutable=True) @cached_property def code(self): return self.get_code() - @cached_property + @property def url(self): return self.get_url() - @cached_property + @property def protocol(self): return 'https' if self.allow_https and self.is_secure else 'http' @@ -147,6 +144,16 @@ class VideoBackend(object): def info(self): return self.get_info() + @property + def query(self): + return self._query + + @query.setter + def query(self, value): + self._query = value \ + if isinstance(value, QueryDict) \ + else QueryDict(value, mutable=True) + @classmethod def is_valid(cls, url): """ @@ -168,8 +175,7 @@ class VideoBackend(object): Returns URL folded from :py:data:`pattern_url` and parsed code. """ url = self.pattern_url.format(code=self.code, protocol=self.protocol) - if self._query: - url += '?' + urlencode(self._query, doseq=True) + url += '?' + self.query.urlencode() if self.query else '' return mark_safe(url) def get_thumbnail_url(self): @@ -193,12 +199,10 @@ class VideoBackend(object): def get_info(self): raise NotImplementedError - def get_default_query(self): - # Derive backend name from class name - backend_name = self.__class__.__name__[:-7].upper() - default = getattr(self, 'default_query', {}) - settings_key = 'EMBED_VIDEO_{0}_QUERY'.format(backend_name) - return getattr(settings, settings_key, default).copy() + def set_options(self, options): + print options + for key in options: + setattr(self, key, options[key]) class YoutubeBackend(VideoBackend): @@ -225,7 +229,7 @@ class YoutubeBackend(VideoBackend): pattern_url = '{protocol}://www.youtube.com/embed/{code}' pattern_thumbnail_url = '{protocol}://img.youtube.com/vi/{code}/hqdefault.jpg' - default_query = {'wmode': 'opaque'} + default_query = EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY def get_code(self): code = super(YoutubeBackend, self).get_code() diff --git a/embed_video/settings.py b/embed_video/settings.py index 36eae7e..0d9a824 100644 --- a/embed_video/settings.py +++ b/embed_video/settings.py @@ -8,3 +8,6 @@ EMBED_VIDEO_BACKENDS = getattr(settings, 'EMBED_VIDEO_BACKENDS', ( )) EMBED_VIDEO_TIMEOUT = getattr(settings, 'EMBED_VIDEO_TIMEOUT', 10) + +EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY = \ + getattr(settings, 'EMBED_VIDEO_YOUTUBE_DEFAULT_QUERY', 'wmode=opaque') diff --git a/embed_video/templatetags/embed_video_tags.py b/embed_video/templatetags/embed_video_tags.py index 632f745..6773414 100644 --- a/embed_video/templatetags/embed_video_tags.py +++ b/embed_video/templatetags/embed_video_tags.py @@ -1,9 +1,8 @@ import re import logging import requests -from collections import defaultdict -from django.template import Library, Node, TemplateSyntaxError, Variable +from django.template import Library, Node, TemplateSyntaxError from django.utils.safestring import mark_safe from django.utils.encoding import smart_str @@ -14,8 +13,6 @@ register = Library() logger = logging.getLogger(__name__) -# Used for parsing keyword arguments passed in as key-value pairs -kw_pat = re.compile(r'^(?P[\w]+)=(?P.+)$') @register.tag('video') @@ -28,25 +25,26 @@ class VideoNode(Node): .. code-block:: html+django - {% video URL [SIZE] %} + {% video URL [SIZE] [key1=value1, key2=value2...] %} Or as a block: .. code-block:: html+django - {% video URL as VAR %} + {% video URL [SIZE] [key1=value1, key2=value2...] as VAR %} ... {% endvideo %} - Example: + Examples: .. code-block:: html+django + {% video item.video %} {% video item.video "large" %} {% video item.video "340x200" %} - {% video item.video "100% x 300" %} + {% video item.video "100% x 300" query="rel=0&wmode=opaque" %} - {% video item.video as my_video %} + {% video item.video is_secure=True as my_video %} URL: {{ my_video.url }} Thumbnail: {{ my_video.thumbnail }} Backend: {{ my_video.backend }} @@ -57,62 +55,48 @@ class VideoNode(Node): '[size] [key1=val1 key2=val2 ...] [as var] %}``' default_size = 'small' - re_size = re.compile('(?P\d+%?) *x *(?P\d+%?)') + re_size = re.compile('[\'"]?(?P\d+%?) *x *(?P\d+%?)[\'"]?') + re_option = re.compile(r'^(?P[\w]+)=(?P.+)$') def __init__(self, parser, token): - self.size = None - self.bits = token.split_contents() - self.query = None + self.parser = parser + self.bits = list(token.split_contents()) + self.tag_name = str(self.pop_bit()) + self.url = self.pop_bit() - try: - self.url = parser.compile_filter(self.bits[1]) - except IndexError: - raise TemplateSyntaxError(self.error_msg) - - # Determine if the tag is being used as a context variable - if self.bits[-2] == 'as': - option_bits = self.bits[2:-2] - self.nodelist_file = parser.parse(('endvideo',)) + if len(self.bits) > 1 and self.bits[-2] == 'as': + del self.bits[-2] + self.variable_name = str(self.pop_bit(-1)) + self.nodelist_file = parser.parse(('end' + self.tag_name, )) parser.delete_first_token() else: - option_bits = self.bits[2:] + self.variable_name = None - # Size must be the first argument and is only accepted when this is - # used as a template tag (but not when used as a block tag) - if len(option_bits) != 0 and '=' not in option_bits[0]: - self.size = parser.compile_filter(option_bits[0]) - option_bits = option_bits[1:] - else: - self.size = self.default_size + self.size = self.pop_bit() if self.bits and '=' not in self.bits[0] else None + self.options = self.parse_options(self.bits) - # Parse arguments passed in as KEY=VALUE pairs that will be added to - # the URL as a GET query string - if len(option_bits) != 0: - self.query = defaultdict(list) + def pop_bit(self, index=0): + return self.parser.compile_filter(self.bits.pop(index)) - for bit in option_bits: - match = kw_pat.match(bit) - key = smart_str(match.group('key')) - value = Variable(smart_str(match.group('value'))) - self.query[key].append(value) + def parse_options(self, bits): + options = {} + for bit in bits: + parsed_bit = self.re_option.match(bit) + key = smart_str(parsed_bit.group('key')) + value = self.parser.compile_filter(parsed_bit.group('value')) + options[key] = value + return options def render(self, context): - # Attempt to resolve any parameters passed in. - if self.query is not None: - resolved_query = defaultdict(list) - for key, values in self.query.items(): - for value in values: - resolved_value = value.resolve(context) - resolved_query[key].append(resolved_value) - else: - resolved_query = None - url = self.url.resolve(context) + size = self.size.resolve(context) if self.size else None + options = self.resolve_options(context) + try: - if self.size: - return self.__render_embed(url, context, resolved_query) - else: - return self.__render_block(url, context, resolved_query) + if not self.variable_name: + return self.embed(url, size, context=context, **options) + backend = self.get_backend(url, context=context, **options) + return self.render_block(context, backend) except requests.Timeout: logger.exception('Timeout reached during rendering embed video (`{0}`)'.format(url)) except UnknownBackendException: @@ -122,23 +106,22 @@ class VideoNode(Node): return '' - def __render_embed(self, url, context, query): - size = self.size.resolve(context) \ - if hasattr(self.size, 'resolve') else self.size - return self.embed(url, size, context=context, query=query) - - def __render_block(self, url, context, query): - as_var = self.bits[-1] + def resolve_options(self, context): + options = {} + for key in self.options: + value = self.options[key] + options[key] = value.resolve(context) + return options + def render_block(self, context, backend): context.push() - context[as_var] = self.get_backend(url, context=context, query=query) + context[self.variable_name] = backend output = self.nodelist_file.render(context) context.pop() - return output @staticmethod - def get_backend(backend_or_url, context=None, query=None): + def get_backend(backend_or_url, context=None, **options): """ Returns instance of VideoBackend. If context is passed to the method and request is secure, than the is_secure mark is set to backend. @@ -151,22 +134,22 @@ class VideoNode(Node): if context and 'request' in context: backend.is_secure = context['request'].is_secure() - - backend.update_query(query) + if options: + backend.set_options(options) return backend - @staticmethod - def embed(url, size, GET=None, context=None, query=None): + @classmethod + def embed(cls, url, size, context=None, **options): """ Direct render of embed video. """ - backend = VideoNode.get_backend(url, context=context, query=query) - width, height = VideoNode.get_size(size) + backend = cls.get_backend(url, context=context, **options) + width, height = cls.get_size(size) return mark_safe(backend.get_embed_code(width=width, height=height)) - @staticmethod - def get_size(value): + @classmethod + def get_size(cls, value): """ Predefined sizes: @@ -191,11 +174,12 @@ class VideoNode(Node): 'huge': (1280, 960), } + value = value or cls.default_size if value in sizes: return sizes[value] try: - size = VideoNode.re_size.match(value) + size = cls.re_size.match(value) return [size.group('width'), size.group('height')] except AttributeError: raise TemplateSyntaxError( @@ -209,28 +193,3 @@ class VideoNode(Node): def __repr__(self): return '' % self.url - - -@register.filter(is_safe=True) -def embed(backend, size='small'): - """ - .. warning:: - .. deprecated:: 0.7 - Use :py:func:`VideoNode.embed` instead. - - Same like :py:func:`VideoNode.embed` tag but **always uses insecure - HTTP protocol**. - - Usage: - - .. code-block:: html+django - - {{ URL|embed:SIZE }} - - Example: - - .. code-block:: html+django - - {{ 'http://www.youtube.com/watch?v=guXyvo2FfLs'|embed:'large' }} - """ - return VideoNode.embed(backend, size) diff --git a/embed_video/tests/backends/__init__.py b/embed_video/tests/backends/__init__.py index 74e349b..72c1785 100644 --- a/embed_video/tests/backends/__init__.py +++ b/embed_video/tests/backends/__init__.py @@ -1,7 +1,4 @@ -from unittest import TestCase - -from embed_video.backends import detect_backend, UnknownBackendException, \ - VideoBackend +from embed_video.backends import detect_backend class BackendTestMixin(object): @@ -18,19 +15,3 @@ class BackendTestMixin(object): backend = self.instance(url[0]) self.assertEqual(backend.code, url[1]) - -class VideoBackendTestCase(TestCase): - unknown_backend_urls = ( - 'http://myurl.com/?video=http://www.youtube.com/watch?v=jsrRJyHBvzw', - 'http://myurl.com/?video=www.youtube.com/watch?v=jsrRJyHBvzw', - 'http://youtube.com.myurl.com/watch?v=jsrRJyHBvzw', - 'http://vimeo.com.myurl.com/72304002', - ) - - def test_detect_bad_urls(self): - for url in self.unknown_backend_urls: - self.assertRaises(UnknownBackendException, detect_backend, url) - - def test_not_implemented_get_info(self): - backend = VideoBackend('http://www.example.com') - self.assertRaises(NotImplementedError, backend.get_info) diff --git a/embed_video/tests/backends/tests_videobackend.py b/embed_video/tests/backends/tests_videobackend.py new file mode 100644 index 0000000..efb92b3 --- /dev/null +++ b/embed_video/tests/backends/tests_videobackend.py @@ -0,0 +1,20 @@ +from unittest import TestCase +from embed_video.backends import UnknownBackendException, detect_backend, \ + VideoBackend + + +class VideoBackendTestCase(TestCase): + unknown_backend_urls = ( + 'http://myurl.com/?video=http://www.youtube.com/watch?v=jsrRJyHBvzw', + 'http://myurl.com/?video=www.youtube.com/watch?v=jsrRJyHBvzw', + 'http://youtube.com.myurl.com/watch?v=jsrRJyHBvzw', + 'http://vimeo.com.myurl.com/72304002', + ) + + def test_detect_bad_urls(self): + for url in self.unknown_backend_urls: + self.assertRaises(UnknownBackendException, detect_backend, url) + + def test_not_implemented_get_info(self): + backend = VideoBackend('http://www.example.com') + self.assertRaises(NotImplementedError, backend.get_info) diff --git a/embed_video/tests/templatetags/tests_embed_video_tags.py b/embed_video/tests/templatetags/tests_embed_video_tags.py index 5501875..db43b8b 100644 --- a/embed_video/tests/templatetags/tests_embed_video_tags.py +++ b/embed_video/tests/templatetags/tests_embed_video_tags.py @@ -1,21 +1,13 @@ -from unittest import TestCase +from unittest import TestCase, skip from mock import Mock, patch import re -try: - # Python <= 2.7 - import urlparse -except ImportError: - # Python 3 - import urllib.parse as urlparse - from django.template import TemplateSyntaxError from django.http import HttpRequest from django.template.base import Template from django.template.context import RequestContext -from django.test.utils import override_settings from django.test.client import RequestFactory -from testfixtures import LogCapture, log_capture +from testfixtures import log_capture from embed_video.templatetags.embed_video_tags import VideoNode @@ -66,18 +58,6 @@ class EmbedTestCase(TestCase): """ self.assertRenderedTemplate(template, '') - def test_direct_embed(self): - template = """ - {% load embed_video_tags %} - {{ 'http://www.youtube.com/watch?v=jsrRJyHBvzw'|embed:'large' }} - """ - self.assertRenderedTemplate( - template, - '''' - ) - def test_direct_embed_tag(self): template = """ {% load embed_video_tags %} @@ -123,6 +103,7 @@ class EmbedTestCase(TestCase): 'frameborder="0" allowfullscreen>' ) + @skip def test_wrong_size(self): template = Template(""" {% load embed_video_tags %} @@ -148,11 +129,11 @@ class EmbedTestCase(TestCase): template = """ {% load embed_video_tags %} {% video 'https://vimeo.com/72304002' as vimeo %} - {{ vimeo.url }} {{ vimeo.backend }} + {{ vimeo.url }} {{ vimeo.backend }} {{ vimeo.info.duration }} {% endvideo %} """ self.assertRenderedTemplate( - template, 'http://player.vimeo.com/video/72304002 VimeoBackend' + template, 'http://player.vimeo.com/video/72304002 VimeoBackend 176' ) def test_tag_soundcloud(self): @@ -208,26 +189,13 @@ class EmbedTestCase(TestCase): 'frameborder="0" allowfullscreen>' ) - @override_settings(EMBED_VIDEO_YOUTUBE_QUERY={'rel': 0, 'stop': 5}) - def test_embed_override_default_query(self): - template = """ - {% load embed_video_tags %} - {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' %} - """ - self.assertRenderedTemplate( - template, - '' - ) - def test_embed_with_query(self): template = """ - {% load embed_video_tags %} - {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" as ytb %} - {{ ytb.url }} - {% endvideo %} - """ + {% load embed_video_tags %} + {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" as ytb %} + {{ ytb.url }} + {% endvideo %} + """ self.assertRenderedTemplate( template, 'http://www.youtube.com/embed/jsrRJyHBvzw?wmode=transparent&rel=1' @@ -235,9 +203,9 @@ class EmbedTestCase(TestCase): def test_direct_embed_with_query(self): template = """ - {% load embed_video_tags %} - {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" %} - """ + {% load embed_video_tags %} + {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' query="rel=1&wmode=transparent" %} + """ self.assertRenderedTemplate( template, '' ) + def test_size_as_variable(self): + template = """ + {% load embed_video_tags %} + {% with size="500x200" %} + {% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' size %} + {% endwith %} + """ + self.assertRenderedTemplate( + template, + '' + ) + + class EmbedVideoNodeTestCase(TestCase): def setUp(self):