Settings options in embed tags

This commit is contained in:
Juda Kaleta 2014-07-26 16:19:10 +02:00
parent 6013055e4c
commit e89a82dac1
7 changed files with 142 additions and 188 deletions

View file

@ -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.*

View file

@ -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()

View file

@ -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')

View file

@ -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<key>[\w]+)=(?P<value>.+)$')
@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<width>\d+%?) *x *(?P<height>\d+%?)')
re_size = re.compile('[\'"]?(?P<width>\d+%?) *x *(?P<height>\d+%?)[\'"]?')
re_option = re.compile(r'^(?P<key>[\w]+)=(?P<value>.+)$')
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 '<VideoNode "%s">' % 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)

View file

@ -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)

View file

@ -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)

View file

@ -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,
'<iframe width="960" height="720" '
'src="http://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque" '
'frameborder="0" allowfullscreen></iframe>'''
)
def test_direct_embed_tag(self):
template = """
{% load embed_video_tags %}
@ -123,6 +103,7 @@ class EmbedTestCase(TestCase):
'frameborder="0" allowfullscreen></iframe>'
)
@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></iframe>'
)
@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,
'<iframe width="480" height="360" '
'src="http://www.youtube.com/embed/jsrRJyHBvzw?wmode=transparent&rel=0&stop=5" '
'frameborder="0" allowfullscreen></iframe>'
)
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,
'<iframe width="480" height="360" '
@ -247,16 +215,31 @@ class EmbedTestCase(TestCase):
def test_set_options(self):
template = """
{% load embed_video_tags %}
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' 300x200 is_secure=True query="rel=1" %}
"""
{% load embed_video_tags %}
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' "300x200" is_secure=True query="rel=1" %}
"""
self.assertRenderedTemplate(
template,
'<iframe width="300" height="200" '
'src="https://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque&rel=1" '
'src="https://www.youtube.com/embed/jsrRJyHBvzw?rel=1" '
'frameborder="0" allowfullscreen></iframe>'
)
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,
'<iframe width="500" height="200" '
'src="http://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque" '
'frameborder="0" allowfullscreen></iframe>'
)
class EmbedVideoNodeTestCase(TestCase):
def setUp(self):