mirror of
https://github.com/Hopiu/django-embed-video.git
synced 2026-03-16 21:30:23 +00:00
Updated video template tag to accept kwargs
Some video backends support optional arguments embedded in the URL query string that adjust how the video is displayed (e.g. Youtube supports rel=0 which means don't show related videos). Updated the template tag to accept optional KEY=VALUE pairs that will be added onto the embedded URL as a query string. Also added support for specifying default query strings on a per-backend basis (e.g. so that all YouTube urls have ?rel=0 added to the end). Added appropriate tests and documentation.
This commit is contained in:
parent
8fc5022dd1
commit
5ebea38220
5 changed files with 240 additions and 24 deletions
|
|
@ -25,3 +25,27 @@ EMBED_VIDEO_TIMEOUT
|
|||
Sets timeout for ``GET`` requests to remote servers.
|
||||
|
||||
Default: ``10``
|
||||
|
||||
|
||||
.. setting:: EMBED_VIDEO_{BACKEND}_QUERY
|
||||
|
||||
EMBED_VIDEO_{BACKEND}_QUERY
|
||||
---------------------------
|
||||
|
||||
Set default query dictionary for including in the embedded URL. This is
|
||||
backend-specific. Substitute the actual name of the backend for {BACKEND}
|
||||
(i.e. to set the default query for the YouTube backend, the setting name would
|
||||
be ``EMBED_VIDEO_YOUTUBE_QUERY`` and for SoundCloud the name would be
|
||||
``EMBED_VIDEO_SOUNDCLOUD_QUERY``).
|
||||
|
||||
As an example, if you set the following::
|
||||
|
||||
EMBED_VIDEO_YOUTUBE_QUERY = {
|
||||
'wmode': 'opaque',
|
||||
'rel': '0'
|
||||
}
|
||||
|
||||
All URLs for YouTube will include the query string ``?wmode=opaque&rel=0``.
|
||||
|
||||
The default value for EMBED_VIDEO_YOUTUBE_QUERY is ``{'wmode': 'opaque'}``.
|
||||
None of the other backends have default values set.
|
||||
|
|
|
|||
|
|
@ -48,6 +48,34 @@ Default sizes are ``tiny`` (420x315), ``small`` (480x360), ``medium`` (640x480),
|
|||
{% video my_video '100% x 50%' %}
|
||||
|
||||
|
||||
Some backends (e.g. YouTube) allow configuration of the embedding via passing
|
||||
query parameters. To specify the parameters:
|
||||
|
||||
::
|
||||
|
||||
{% video item.video 'small' rel=0 %}
|
||||
|
||||
{% video item.video 'small' start=2 stop=5 repeat=1 %}
|
||||
|
||||
{% video item.video rel=0 as my_video %}
|
||||
URL: {{ my_video.url }}
|
||||
Thumbnail: {{ my_video.thumbnail }}
|
||||
Backend: {{ my_video.backend }}
|
||||
{% video my_video 'small' %}
|
||||
{% endvideo %}
|
||||
|
||||
Parameters may also be template variables:
|
||||
|
||||
::
|
||||
|
||||
{% video item.video 'small' start=item.start stop=item.stop repeat=item.repeat %}
|
||||
|
||||
|
||||
.. tip::
|
||||
|
||||
You can provide default values for the query string that's included in the
|
||||
embedded URL by updating your Django settings file.
|
||||
|
||||
|
||||
.. tip::
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,18 @@ import requests
|
|||
import json
|
||||
|
||||
try:
|
||||
# Python <= 2.7
|
||||
import urlparse
|
||||
from urllib import urlencode
|
||||
except ImportError:
|
||||
# support for py3
|
||||
import urllib.parse as urlparse
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .utils import import_by_path
|
||||
from .settings import EMBED_VIDEO_BACKENDS, EMBED_VIDEO_TIMEOUT
|
||||
|
|
@ -106,7 +111,7 @@ class VideoBackend(object):
|
|||
``{{ width }}``, ``{{ height }}``
|
||||
"""
|
||||
|
||||
def __init__(self, url, is_secure=False):
|
||||
def __init__(self, url, is_secure=False, query=None):
|
||||
"""
|
||||
First it tries to load data from cache and if it don't succeed, run
|
||||
:py:meth:`init` and then save it to cache.
|
||||
|
|
@ -114,6 +119,12 @@ 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 = self.get_default_query()
|
||||
if query is not None:
|
||||
self._query.update(query)
|
||||
|
||||
@cached_property
|
||||
def code(self):
|
||||
|
|
@ -155,7 +166,10 @@ class VideoBackend(object):
|
|||
"""
|
||||
Returns URL folded from :py:data:`pattern_url` and parsed code.
|
||||
"""
|
||||
return self.pattern_url.format(code=self.code, protocol=self.protocol)
|
||||
url = self.pattern_url.format(code=self.code, protocol=self.protocol)
|
||||
if self._query:
|
||||
url += '?' + urlencode(self._query, doseq=True)
|
||||
return mark_safe(url)
|
||||
|
||||
def get_thumbnail_url(self):
|
||||
"""
|
||||
|
|
@ -178,6 +192,13 @@ 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_{}_QUERY'.format(backend_name)
|
||||
return getattr(settings, settings_key, default).copy()
|
||||
|
||||
|
||||
class YoutubeBackend(VideoBackend):
|
||||
"""
|
||||
|
|
@ -200,8 +221,9 @@ class YoutubeBackend(VideoBackend):
|
|||
re.I | re.X
|
||||
)
|
||||
|
||||
pattern_url = '{protocol}://www.youtube.com/embed/{code}?wmode=opaque'
|
||||
pattern_url = '{protocol}://www.youtube.com/embed/{code}'
|
||||
pattern_thumbnail_url = '{protocol}://img.youtube.com/vi/{code}/hqdefault.jpg'
|
||||
default_query = {'wmode': 'opaque'}
|
||||
|
||||
def get_code(self):
|
||||
code = super(YoutubeBackend, self).get_code()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
from django.template import Library, Node, TemplateSyntaxError
|
||||
from django.template import Library, Node, TemplateSyntaxError, Variable
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.encoding import smart_str
|
||||
import re
|
||||
import logging
|
||||
import requests
|
||||
from collections import defaultdict
|
||||
|
||||
from ..backends import detect_backend, VideoBackend, \
|
||||
VideoDoesntExistException, UnknownBackendException
|
||||
|
|
@ -11,6 +13,9 @@ 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')
|
||||
class VideoNode(Node):
|
||||
|
|
@ -47,7 +52,8 @@ class VideoNode(Node):
|
|||
{% endvideo %}
|
||||
|
||||
"""
|
||||
error_msg = 'Syntax error. Expected: ``{% video URL ... %}``'
|
||||
error_msg = 'Syntax error. Expected: ``{% video URL ' \
|
||||
'[size] [key1=val1 key2=val2 ...] [as var] %}``'
|
||||
default_size = 'small'
|
||||
|
||||
re_size = re.compile('(?P<width>\d+%?) *x *(?P<height>\d+%?)')
|
||||
|
|
@ -55,29 +61,57 @@ class VideoNode(Node):
|
|||
def __init__(self, parser, token):
|
||||
self.size = None
|
||||
self.bits = token.split_contents()
|
||||
self.query = None
|
||||
|
||||
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',))
|
||||
parser.delete_first_token()
|
||||
else:
|
||||
try:
|
||||
self.size = parser.compile_filter(self.bits[2])
|
||||
except IndexError:
|
||||
self.size = self.default_size
|
||||
option_bits = self.bits[2:]
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
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 render(self, context):
|
||||
url = self.url.resolve(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)
|
||||
try:
|
||||
if self.size:
|
||||
return self.__render_embed(url, context)
|
||||
return self.__render_embed(url, context, resolved_query)
|
||||
else:
|
||||
return self.__render_block(url, context)
|
||||
return self.__render_block(url, context, resolved_query)
|
||||
except requests.Timeout:
|
||||
logger.exception('Timeout reached during rendering embed video (`{0}`)'.format(url))
|
||||
except UnknownBackendException:
|
||||
|
|
@ -87,44 +121,46 @@ class VideoNode(Node):
|
|||
|
||||
return ''
|
||||
|
||||
def __render_embed(self, url, context):
|
||||
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)
|
||||
return self.embed(url, size, context=context, query=query)
|
||||
|
||||
def __render_block(self, url, context):
|
||||
def __render_block(self, url, context, query):
|
||||
as_var = self.bits[-1]
|
||||
|
||||
context.push()
|
||||
context[as_var] = self.get_backend(url, context=context)
|
||||
context[as_var] = self.get_backend(url, context=context, query=query)
|
||||
output = self.nodelist_file.render(context)
|
||||
context.pop()
|
||||
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
def get_backend(backend_or_url, context=None):
|
||||
def get_backend(backend_or_url, context=None, query=None):
|
||||
"""
|
||||
Returns instance of VideoBackend. If context is passed to the method
|
||||
and request is secure, than the is_secure mark is set to backend.
|
||||
|
||||
|
||||
A string or VideoBackend instance can be passed to the method.
|
||||
"""
|
||||
|
||||
|
||||
backend = backend_or_url if isinstance(backend_or_url, VideoBackend) \
|
||||
else detect_backend(backend_or_url)
|
||||
|
||||
|
||||
if context and 'request' in context:
|
||||
backend.is_secure = context['request'].is_secure()
|
||||
|
||||
backend.update_query(query)
|
||||
|
||||
return backend
|
||||
|
||||
@staticmethod
|
||||
def embed(url, size, context=None):
|
||||
def embed(url, size, GET=None, context=None, query=None):
|
||||
"""
|
||||
Direct render of embed video.
|
||||
"""
|
||||
backend = VideoNode.get_backend(url, context=context)
|
||||
backend = VideoNode.get_backend(url, context=context, query=query)
|
||||
width, height = VideoNode.get_size(size)
|
||||
return mark_safe(backend.get_embed_code(width=width, height=height))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
from unittest import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
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 testfixtures import LogCapture
|
||||
|
||||
from embed_video.templatetags.embed_video_tags import VideoNode
|
||||
|
||||
URL_PATTERN = re.compile(r'src="?\'?([^"\'>]*)"')
|
||||
|
||||
|
||||
class EmbedVideoNodeTestCase(TestCase):
|
||||
def setUp(self):
|
||||
|
|
@ -17,8 +28,8 @@ class EmbedVideoNodeTestCase(TestCase):
|
|||
self.token = Mock(methods=['split_contents'])
|
||||
|
||||
@staticmethod
|
||||
def _grc():
|
||||
return RequestContext(HttpRequest())
|
||||
def _grc(context=None):
|
||||
return RequestContext(HttpRequest(), context)
|
||||
|
||||
def test_embed(self):
|
||||
template = Template("""
|
||||
|
|
@ -212,3 +223,98 @@ class EmbedVideoNodeTestCase(TestCase):
|
|||
{% video "https://soundcloud.com/xyz/foo" %}
|
||||
""")
|
||||
self.assertEqual(template.render(self._grc()).strip(), '')
|
||||
|
||||
##########################################################################
|
||||
# Tests for adding GET query via KEY=VALUE pairs
|
||||
##########################################################################
|
||||
|
||||
def _validate_GET_query(self, rendered, expected):
|
||||
# Use this functioon to test the KEY=VALUE optional arguments to the
|
||||
# templatetag because there's no guarantee that they will be appended
|
||||
# to the URL query string in a particular order. By default the
|
||||
# YouTube backend adds wmode=opaque to the query string, so be sure to
|
||||
# include that in the expected querystring dict.
|
||||
url = URL_PATTERN.search(rendered).group(1)
|
||||
result = urlparse.urlparse(url)
|
||||
qs = urlparse.parse_qs(result[4])
|
||||
self.assertEqual(qs, expected)
|
||||
|
||||
@override_settings(EMBED_VIDEO_YOUTUBE_QUERY={'rel': 0, 'stop': 5})
|
||||
def test_embed_with_query_settings_override(self):
|
||||
# Test KEY=VALUE argument with default values provided by settings
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' %}
|
||||
""")
|
||||
expected = {
|
||||
'rel': ['0'],
|
||||
'stop': ['5'],
|
||||
}
|
||||
rendered = template.render(self._grc())
|
||||
self._validate_GET_query(rendered, expected)
|
||||
|
||||
def test_embed_with_query_var(self):
|
||||
# Test KEY=VALUE argument with the value resolving to a context
|
||||
# variable.
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' rel=show_related loop=5 %}
|
||||
""")
|
||||
expected = {
|
||||
'rel': ['0'],
|
||||
'loop': ['5'],
|
||||
'wmode': ['opaque']
|
||||
}
|
||||
context = {'show_related': 0}
|
||||
rendered = template.render(self._grc(context))
|
||||
self._validate_GET_query(rendered, expected)
|
||||
|
||||
def test_embed_with_query_multiple(self):
|
||||
# Test multiple KEY=VALUE arguments
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' rel=0 loop=1 end=5 %}
|
||||
""")
|
||||
expected = {
|
||||
'rel': ['0'],
|
||||
'loop': ['1'],
|
||||
'end': ['5'],
|
||||
'wmode': ['opaque']
|
||||
}
|
||||
self._validate_GET_query(template.render(self._grc()), expected)
|
||||
|
||||
def test_embed_with_query_multiple_list(self):
|
||||
# Test multiple KEY=VALUE arguments where the key is repeated multiple
|
||||
# times (this is valid in a URL query).
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' rel=0 loop=1 end=5 end=6 %}
|
||||
""")
|
||||
expected = {
|
||||
'rel': ['0'],
|
||||
'loop': ['1'],
|
||||
'end': ['5', '6'],
|
||||
'wmode': ['opaque']
|
||||
}
|
||||
self._validate_GET_query(template.render(self._grc()), expected)
|
||||
|
||||
def test_tag_youtube_with_query(self):
|
||||
# Test KEY=VALUE arguments when used as a tag block.
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' rel=0 as ytb %}
|
||||
{{ ytb.url }}
|
||||
{% endvideo %}
|
||||
""")
|
||||
rendered = 'http://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque&rel=0'
|
||||
self.assertEqual(template.render(self._grc()).strip(), rendered)
|
||||
|
||||
def test_embed_with_query_rel(self):
|
||||
template = Template("""
|
||||
{% load embed_video_tags %}
|
||||
{% video 'http://www.youtube.com/watch?v=jsrRJyHBvzw' rel=0 %}
|
||||
""")
|
||||
rendered = u'''<iframe width="480" height="360" src="http://www.youtube.com/embed/jsrRJyHBvzw?wmode=opaque&rel=0"
|
||||
frameborder="0" allowfullscreen></iframe>'''
|
||||
self.assertEqual(template.render(self._grc()).strip(), rendered)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue