From 6b17a3f957e34b9f99515ade969a2bbab1e1efa5 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Thu, 25 Sep 2014 18:50:28 -0400 Subject: [PATCH 1/6] markup with oembedding --- README.rst | 5 + django_mesh/managers.py | 7 +- django_mesh/models.py | 88 ++++++++-- django_mesh/templates/django_mesh/base.html | 2 +- .../templates/django_mesh/post_view.html | 2 +- django_mesh/tests/test_models.py | 121 +++++++++++-- django_mesh/tests/util.py | 16 +- django_mesh/tests/youtube_data.py | 159 ++++++++++++++++++ setup.py | 4 +- 9 files changed, 363 insertions(+), 41 deletions(-) create mode 100644 django_mesh/tests/youtube_data.py diff --git a/README.rst b/README.rst index 57b436e..539b7bd 100644 --- a/README.rst +++ b/README.rst @@ -16,6 +16,11 @@ Depends on * auth, admin, comments * django-model-utils * python-pytz +* markdown +* textile +* oembed +* pyembed-markdown +* six Huboard ------- diff --git a/django_mesh/managers.py b/django_mesh/managers.py index 06a94fc..d427ed4 100644 --- a/django_mesh/managers.py +++ b/django_mesh/managers.py @@ -15,10 +15,11 @@ #along with this program. If not, see . # Django imports + from django.db import models -from django.utils import timezone -from django.db.models.query import QuerySet from django.db.models import Q +from django.db.models.query import QuerySet +from django.utils import timezone class PostQuerySet(QuerySet): @@ -48,4 +49,4 @@ def get_for_user(self, user): if user.id is not None: q_object = Q(post__channel__followers=user.id) | q_object - return self.filter(q_object).distinct().filter(post__published__lte=timezone.now()) \ No newline at end of file + return self.filter(q_object).distinct().filter(post__published__lte=timezone.now()) diff --git a/django_mesh/models.py b/django_mesh/models.py index 6bfef82..8899e9d 100644 --- a/django_mesh/models.py +++ b/django_mesh/models.py @@ -17,10 +17,12 @@ # Python imports from __future__ import unicode_literals import re - +import six # Django imports #from django.contrib.sitemaps import ping_google from django.utils.encoding import python_2_unicode_compatible +from django.utils.html import escape + from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone @@ -28,11 +30,24 @@ # 3d party imports from model_utils import Choices +import markdown +import textile # to do: add oembed for textile markup +from pyembed.markdown import PyEmbedMarkdown +from pyembed.core import PyEmbed +from bs4 import BeautifulSoup # App imports from .managers import PostQuerySet, ChannelQuerySet, TagQuerySet -oembed_regex = re.compile(r'^(?P\s*)(?Phttp://.+)', re.MULTILINE) +# URL_REGEX = r"""(?i)\b((?:https?:(?:/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:(?%s' % (each,each), 1) +# # return text # text but with anchors inserted + @python_2_unicode_compatible class _Abstract(models.Model): #microblog compatible. @@ -40,25 +55,58 @@ class _Abstract(models.Model): #microblog compatible. title = models.CharField(max_length=140, unique=True) text = models.TextField(default='') rendered_text = models.TextField(default='', blank=True) + TEXT_TYPE = Choices( + (0, 'SIMPLE', 'Simple',), + (1, 'MARKDOWN', 'Markdown',), + (2, 'TEXTILE', 'Textile'), + ) + + text_type = models.IntegerField(max_length=1, default=TEXT_TYPE.SIMPLE, choices=TEXT_TYPE) + + def render(self, *args, **kwargs): + + self.rendered_text = markdown.markdown(self.text) + + if self.text_type == self.TEXT_TYPE.SIMPLE: + self.rendered_text = self.text + + elif self.text_type == self.TEXT_TYPE.TEXTILE: + self.rendered_text = textile.textile(self.text) - def get_oembed_markup(self, matchobj): - gd = matchobj.groupdict('') - return '%(spacing)s%(url)s' % gd - def render(self): - #TODO: strip out dangerous HTML attributes, only allow basic formatting tags - self.rendered_text = oembed_regex.sub(self.get_oembed_markup, self.text) + soup = BeautifulSoup(self.rendered_text) + + matching_text_nodes = soup.find_all(text = re.compile(URL_REGEX)) + + pyembed = PyEmbed() + + + for matching_text_node in matching_text_nodes: + + plain_text = six.text_type(matching_text_node) + + for each in re.findall(URL_REGEX, plain_text): + plain_text = plain_text.replace(each, '%s' % (each,each), 1) + + if matching_text_node.parent.text == each: # if url on one line, each == url == matching_text_node + try: + oembed = pyembed.embed(matching_text_node) + except: + matching_text_node.replace_with(plain_text) + else: + matching_text_node.replace_with(oembed) + else: + matching_text_node.replace_with(plain_text) + + soup = BeautifulSoup(soup.encode(formatter=None)) + + self.rendered_text = soup.encode(formatter=None).decode() def save(self, *args, **kwargs): if self.rendered_text == '': self.render() super(_Abstract, self).save(*args, **kwargs) -# try: -# ping_google() -# except Exception: -# # Bare 'except' because we could get a variety of HTTP-related exceptions. -# pass def __str__(self): return self.title @@ -132,5 +180,15 @@ def _get_summary(self): def get_absolute_url(self): return reverse('mesh_post_view', kwargs={'slug': self.slug,}) - class Meta: - ordering = ['published'] +class MockYoutube(models.Model): + ysc = models.IntegerField(max_length=3) + yu = models.CharField(max_length=140, unique=True) + yc = models.CharField(max_length=140, unique=True) + yt = models.TextField(default='') + yh = models.CharField(max_length=140, unique=True) + + yosc = models.IntegerField(max_length=3) + you = models.CharField(max_length=140, unique=True) + yoh = models.CharField(max_length=140, unique=True) + yot = models.TextField(default='') + yoj = models.TextField(default='') \ No newline at end of file diff --git a/django_mesh/templates/django_mesh/base.html b/django_mesh/templates/django_mesh/base.html index 94d9808..63913c1 100644 --- a/django_mesh/templates/django_mesh/base.html +++ b/django_mesh/templates/django_mesh/base.html @@ -1 +1 @@ -{% extends "base.html" %} +{% extends "base.html" %} \ No newline at end of file diff --git a/django_mesh/templates/django_mesh/post_view.html b/django_mesh/templates/django_mesh/post_view.html index b551729..822bd42 100644 --- a/django_mesh/templates/django_mesh/post_view.html +++ b/django_mesh/templates/django_mesh/post_view.html @@ -8,7 +8,7 @@

{{ post.title }}

- {{ post.rendered_text }} + {{ post.rendered_text|safe }} diff --git a/django_mesh/tests/test_models.py b/django_mesh/tests/test_models.py index 098513f..13602e7 100644 --- a/django_mesh/tests/test_models.py +++ b/django_mesh/tests/test_models.py @@ -16,6 +16,17 @@ # Test imports from .util import BaseTestCase +import markdown, textile +import requests +from pyembed.markdown import PyEmbedMarkdown +from pyembed.core import PyEmbed +from django_mesh.models import _Abstract +from .youtube_data import get_mock, get_mock_no_oembed +from django.core.urlresolvers import reverse + +from mock import patch, Mock + + class PostTestCase(BaseTestCase): def test_has_auto_summary(self): @@ -35,32 +46,38 @@ def test_has_custom_summary(self): self.assertNotEqual(self.p1.summary, self.p1.teaser) self.assertEqual(self.p1.summary, self.p1.custom_summary) - def test_render_stays_the_same_when_no_links(self): - self.assertNotEqual(self.p1.text, '') - self.assertEqual(self.p1.rendered_text, '') - + def test_markup(self): self.c1.save() + self.p1.channel = self.c1 - self.p1.save() + self.p7.channel = self.c1 + self.p8.channel = self.c1 - self.assertEqual(self.p1.rendered_text, self.p1.text) + self.p1.text = 'hello' + self.p7.text = 'hello' + self.p8.text = 'hello' + + self.p1.save() # simple + self.p7.save() #markdown + self.p8.save() #textile + + expected_html_markup_and_textile = """

hello

""" + expected_html_simple = """hello""" - def test_render_adds_anchors(self): - self.p1.text = """foo - http://somelink - http://anotherlink - """ + self.assertHTMLEqual(expected_html_simple, self.p1.rendered_text) + self.assertHTMLEqual(expected_html_markup_and_textile, self.p7.rendered_text) + self.assertHTMLEqual(expected_html_markup_and_textile, self.p8.rendered_text) + + def test_rendered_text_stays_the_same_when_no_links(self): + self.assertNotEqual(self.p1.text, '') self.assertEqual(self.p1.rendered_text, '') self.c1.save() self.p1.channel = self.c1 self.p1.save() - self.assertEqual(self.p1.rendered_text, """foo - http://somelink - http://anotherlink - """) + self.assertEqual(self.p1.rendered_text, self.p1.text) def test_get_absolute_url(self): self.c1.save() @@ -78,6 +95,80 @@ def test_str_unicode(self): returned_title = str(self.p1) self.assertEqual(self.p1.title, returned_title) + @patch('requests.sessions.Session.request') + def test_check_if_markdown_automatically_wrap_plain_URLs_in_anchor_tags(self, mock_request): + + self.c1.save() + self.p7.channel = self.c1 + + self.p7.text = 'hello, \n plain_url \n www.google.com \n or \n https://www.google.com' # only add anchors if Protocol, also markdown needs doublespace for new paragraph + expected_html = """

hello, plain_url www.google.com or https://www.google.com

""" + self.p7.save() #markdown + + self.assertHTMLEqual(expected_html, self.p7.rendered_text) + self.assertEqual(mock_request.call_count, 0) + + @patch('requests.sessions.Session.request') + def test_simple(self, mock_request): + + self.c1.save() + self.p1.channel = self.c1 + self.p1.text = ' google link is the link, also https://www.facebook.com but not www.google.com\n \n should not have paragraph insert' + self.p1.slug = 'testing_html_escape' + self.p1.save() + expected_html = """ google link is the link, + also https://www.facebook.com + but not www.google.com\n \n should not have paragraph insert""" + + self.assertHTMLEqual(expected_html,self.p1.rendered_text) + self.assertEqual(mock_request.call_count, 0) + + @patch('requests.sessions.Session.request') + def test_automatic_anchors_to_links_on_own_line_if_markdown_chosen(self, mock_request): + mock_request.side_effect = get_mock_no_oembed() + + self.p7.text = "foo\n \n http://somelink\n" # generates foo, paragraph, anchored links + expected_html = """

foo

+

http://somelink

""" + + self.assertEqual(self.p7.rendered_text, '') + + self.c1.save() + self.p7.channel = self.c1 + self.p7.save() + + self.assertHTMLEqual(expected_html, self.p7.rendered_text) + self.assertEqual(mock_request.call_count, 1) + + @patch('requests.sessions.Session.request') + def test_doesnt_embed_with_surrounding_text(self, mock_request): + + self.c1.save() + self.p7.text = "some text here, should NOT embed http://www.youtube.com/watch?v=Uqa8YSxx8Gs other text here" + self.p7.channel = self.c1 + self.p7.save() + expected_html = """

some text here, should NOT embed http://www.youtube.com/watch?v=Uqa8YSxx8Gs other text here

""" + + self.assertHTMLEqual(expected_html, self.p7.rendered_text) + self.assertEqual(mock_request.call_count, 0) + + @patch('requests.sessions.Session.request') + def test_embed_works_with_markdown(self, mock_request): + + mock_request.side_effect = iter(get_mock()) + + self.c1.save() + self.p7.text = "http://www.youtube.com/watch?v=Uqa8YSxx8Gs" + self.p7.channel = self.c1 + self.p7.save() + + expected_html = """

""" + + self.assertHTMLEqual(expected_html, self.p7.rendered_text) + self.assertEqual(mock_request.call_count, 2) + class ChannelTestCase(BaseTestCase): def test_get_absolute_url(self): self.c1.save() diff --git a/django_mesh/tests/util.py b/django_mesh/tests/util.py index 45030cc..f3cedbe 100644 --- a/django_mesh/tests/util.py +++ b/django_mesh/tests/util.py @@ -31,9 +31,7 @@ class BaseTestCase(TestCase): def setUp(self): - self._old_oembed_regex = models.oembed_regex - models.oembed_regex = re.compile(r'^(?P\s*)(?Phttp://.+)', re.MULTILINE) - + self.username = 'test_user' self.password = 'foobar' self.user = User.objects.create_user(self.username, 'test_user@example.com', self.password) @@ -145,6 +143,17 @@ def setUp(self): slug='tree-falls-forest-active', title='Tree Falls in Forest, No One Notices not active', published=timezone.now()+timedelta(days=1), + text='Lorem Ipsum etc.', + text_type=Post.TEXT_TYPE.MARKDOWN, + status=Post.STATUSES.PUBLISHED + ) + + self.p8 = Post( + author=self.user, + slug='unit-again', + title='testing? Learn all about the latest best practice: TDTDD', + text='Lorem Ipsum etc.', + text_type=Post.TEXT_TYPE.TEXTILE, status=Post.STATUSES.PUBLISHED ) @@ -181,4 +190,3 @@ def setUp(self): def tearDown(self): #FIXME: dqc doesn't intercept db destruction or rollback cache.clear() - models.oembed_regex = self._old_oembed_regex \ No newline at end of file diff --git a/django_mesh/tests/youtube_data.py b/django_mesh/tests/youtube_data.py new file mode 100644 index 0000000..8d5e35e --- /dev/null +++ b/django_mesh/tests/youtube_data.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- + +from django_mesh.models import MockYoutube + +youtube_status_code = 200 +youtube_url = 'http://www.youtube.com/watch?v=Uqa8YSxx8Gs' +youtube_headers = {'alternate-protocol': '80:quic,p=0.01', 'x-xss-protection': '1; mode=block; report=https://www.google.com/appserve/security-bugs/log/youtube', 'x-content-type-options': 'nosniff', 'transfer-encoding': 'chunked', 'set-cookie': 'VISITOR_INFO1_LIVE=CjPr-wSJxcs; path=/; domain=.youtube.com; expires=Tue, 09-Jun-2015 08:41:28 GMT, YSC=7OUlrcCybMk; path=/; domain=.youtube.com; httponly', 'expires': 'Tue, 27 Apr 1971 19:44:06 EST', 'server': 'gwiseguy/2.0', 'cache-control': 'no-cache', 'date': 'Wed, 08 Oct 2014 20:48:28 GMT', 'p3p': 'CP="This is not a P3P policy! See http://support.google.com/accounts/bin/answer.py?answer=151657&hl=en for more info."', 'content-type': 'text/html; charset=utf-8', 'x-frame-options': 'SAMEORIGIN'} +youtube_cookies = "[Cookie(version=0, name='VISITOR_INFO1_LIVE', value='CjPr-wSJxcs', port=None, port_specified=False, domain='.youtube.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=1433839288, discard=False, comment=None, comment_url=None, rest={}, rfc2109=False), Cookie(version=0, name='YSC', value='7OUlrcCybMk', port=None, port_specified=False, domain='.youtube.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={'httponly': None}, rfc2109=False)]" + +youtube_text = """ + + + + + + + + + + +My Voicemail from Uncle Ruckus (Kickstarter Reward!) - YouTube + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+= + + + +
+ + + + + + + +
+

    + + + +""" + +youtube_oembed_status_code = 200 +youtube_oembed_url = 'http://www.youtube.com/oembed?format=json&url=http%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DUqa8YSxx8Gs' +youtube_oembed_headers = {'alternate-protocol': '80:quic,p=0.01', 'x-xss-protection': '1; mode=block; report=https://www.google.com/appserve/security-bugs/log/youtube', 'x-content-type-options': 'nosniff', 'transfer-encoding': 'chunked', 'expires': 'Tue, 27 Apr 1971 19:44:06 EST', 'server': 'gwiseguy/2.0', 'cache-control': 'no-cache', 'date': 'Wed, 08 Oct 2014 22:15:43 GMT', 'x-frame-options': 'ALLOWALL', 'content-type': 'application/json', 'p3p': 'CP="This is not a P3P policy! See http://support.google.com/accounts/bin/answer.py?answer=151657&hl=en for more info."'} +youtube_oembed_text = '{"height": 344, "title": "My Voicemail from Uncle Ruckus (Kickstarter Reward!)", "author_url": "http:\\/\\/www.youtube.com\\/user\\/gasphynx", "width": 459, "provider_name": "YouTube", "author_name": "Se\\u00e1n Hayes", "thumbnail_width": 480, "provider_url": "http:\\/\\/www.youtube.com\\/", "thumbnail_height": 360, "thumbnail_url": "http:\\/\\/i.ytimg.com\\/vi\\/Uqa8YSxx8Gs\\/hqdefault.jpg", "html": "\\u003ciframe width=\\"459\\" height=\\"344\\" src=\\"http:\\/\\/www.youtube.com\\/embed\\/Uqa8YSxx8Gs?feature=oembed\\" frameborder=\\"0\\" allowfullscreen\\u003e\\u003c\\/iframe\\u003e", "type": "video", "version": "1.0"}' +youtube_oembed_json = {u'provider_url': u'http://www.youtube.com/', u'version': u'1.0', u'title': u'My Voicemail from Uncle Ruckus (Kickstarter Reward!)', u'html': u'', u'author_name': u'Se\xe1n Hayes', u'height': 344, u'thumbnail_width': 480, u'width': 459, u'thumbnail_url': u'http://i.ytimg.com/vi/Uqa8YSxx8Gs/hqdefault.jpg', u'author_url': u'http://www.youtube.com/user/gasphynx', u'provider_name': u'YouTube', u'type': u'video', u'thumbnail_height': 360} + +from mock import Mock + +def get_mock(): + + ysc = youtube_status_code + yu = youtube_url + yc = youtube_cookies + yt = youtube_text + yh = youtube_headers + yosc = youtube_oembed_status_code + you = youtube_oembed_url + yoh = youtube_oembed_headers + yot = youtube_oembed_text + yoj = youtube_oembed_json + + mock_obj_1 = Mock() + mock_obj_1.status_code = ysc + mock_obj_1.url = yu + mock_obj_1.headers = yh + mock_obj_1.cookies = yc + mock_obj_1.text = yt + mock_obj_1.save() + + mock_obj_2 = Mock() + mock_obj_2.status_code = 200 + mock_obj_2.url = you + mock_obj_2.headers = yoh + mock_obj_2.text = yot + mock_obj_2.json = yoj + mock_obj_2.save() + + return [mock_obj_1, mock_obj_2] + +def get_mock_no_oembed(): + mock = MockYoutube.objects.create( + ysc = youtube_status_code, + yu = youtube_url, + yc = youtube_cookies, + yt = 'youtube_text', + yh = 'youtube_headers', + yosc = 404, + ) + return [mock] \ No newline at end of file diff --git a/setup.py b/setup.py index 689de87..7446343 100755 --- a/setup.py +++ b/setup.py @@ -47,9 +47,9 @@ def runtests(): 'django_mesh_test_project', ], include_package_data=True, - install_requires=['Django>=1.7', 'django-model-utils',], + install_requires=['Django>=1.7', 'django-model-utils', 'markdown', 'textile', 'oembed', 'pyembed-markdown', 'pyembed', 'requests','six'], - tests_require=['django-admin-smoke-tests>=0.1.9','django-settings-context-processor',], + tests_require=['django-admin-smoke-tests>=0.1.9', 'mock', 'django-settings-context-processor',], test_suite='setup.runtests', ) From b969b379002a50fe383067aa6a6166ad90c2755f Mon Sep 17 00:00:00 2001 From: Sean Hayes Date: Mon, 23 Feb 2015 22:54:51 -0500 Subject: [PATCH 2/6] Updated setup config. --- setup.cfg | 2 ++ setup.py | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py index 7446343..3f180dd 100755 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ import django_mesh package_name = 'django_mesh' +test_package_name = '%s_test_project' % package_name def runtests(): import os @@ -13,9 +14,8 @@ def runtests(): import django from django.core.management import call_command - os.environ['DJANGO_SETTINGS_MODULE'] = 'django_mesh_test_project.settings' - if django.VERSION[0] == 1 and django.VERSION[1] >= 7: - django.setup() + os.environ['DJANGO_SETTINGS_MODULE'] = '%s.settings' % test_package_name + django.setup() call_command('test', 'django_admin_smoke_tests') call_command('test') sys.exit() @@ -43,8 +43,8 @@ def runtests(): download_url='https://github.com/SeanHayes/django-mesh', license='GPL', packages=[ - 'django_mesh', - 'django_mesh_test_project', + package_name, + test_package_name, ], include_package_data=True, install_requires=['Django>=1.7', 'django-model-utils', 'markdown', 'textile', 'oembed', 'pyembed-markdown', 'pyembed', 'requests','six'], From ca1f80ee9a8261a55b9022c43c61ff437d616704 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Fri, 27 Feb 2015 03:31:23 -0500 Subject: [PATCH 3/6] upload --- django_mesh/admin.py | 10 ++- django_mesh/managers.py | 10 +++ django_mesh/models.py | 46 +++++++++- .../django_mesh/media_index_view.html | 23 +++++ .../templates/django_mesh/media_view.html | 17 ++++ django_mesh/tests/test_managers.py | 50 ++++++++++- django_mesh/tests/test_models.py | 18 +++- django_mesh/tests/test_urls.py | 75 +++++++++++++++- django_mesh/tests/util.py | 36 +++++++- django_mesh/urls.py | 5 +- django_mesh/views.py | 88 ++++++++++++++++--- django_mesh_test_project/settings.py | 15 +++- django_mesh_test_project/urls.py | 4 +- 13 files changed, 368 insertions(+), 29 deletions(-) create mode 100644 django_mesh/templates/django_mesh/media_index_view.html create mode 100644 django_mesh/templates/django_mesh/media_view.html diff --git a/django_mesh/admin.py b/django_mesh/admin.py index 3ac7b79..a47dfde 100644 --- a/django_mesh/admin.py +++ b/django_mesh/admin.py @@ -18,7 +18,7 @@ from django.contrib import admin # App imports -from .models import Channel, Post, Tag +from .models import Channel, Post, Tag, Media class ChannelAdmin(admin.ModelAdmin): prepopulated_fields = {"slug": ("title",)} @@ -42,4 +42,10 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): db_field.default = request.user return super(PostAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs) -admin.site.register(Post, PostAdmin) \ No newline at end of file +admin.site.register(Post, PostAdmin) + +class MediaAdmin(admin.ModelAdmin): + list_display = ('title', 'media_type') + +admin.site.register(Media, MediaAdmin) + diff --git a/django_mesh/managers.py b/django_mesh/managers.py index d427ed4..3978faf 100644 --- a/django_mesh/managers.py +++ b/django_mesh/managers.py @@ -50,3 +50,13 @@ def get_for_user(self, user): q_object = Q(post__channel__followers=user.id) | q_object return self.filter(q_object).distinct().filter(post__published__lte=timezone.now()) + +class MediaQuerySet(QuerySet): + def get_for_user(self, user): + + q_object = Q(channel__public=True) + + if user.id is not None: + q_object = Q(channel__followers=user.id) | q_object + + return self.filter(q_object).distinct() \ No newline at end of file diff --git a/django_mesh/models.py b/django_mesh/models.py index 8899e9d..afcc2b0 100644 --- a/django_mesh/models.py +++ b/django_mesh/models.py @@ -21,13 +21,13 @@ # Django imports #from django.contrib.sitemaps import ping_google from django.utils.encoding import python_2_unicode_compatible -from django.utils.html import escape from django.core.urlresolvers import reverse from django.db import models from django.utils import timezone from django.conf import settings + # 3d party imports from model_utils import Choices import markdown @@ -37,7 +37,7 @@ from bs4 import BeautifulSoup # App imports -from .managers import PostQuerySet, ChannelQuerySet, TagQuerySet +from .managers import PostQuerySet, ChannelQuerySet, TagQuerySet, MediaQuerySet # URL_REGEX = r"""(?i)\b((?:https?:(?:/{1,3}|[a-z0-9%])|[a-z0-9.\-]+[.](?:com|net|org|edu|gov|mil|aero|asia|biz|cat|coop|info|int|jobs|mobi|museum|name|post|pro|tel|travel|xxx|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cs|cu|cv|cx|cy|cz|dd|de|dj|dk|dm|do|dz|ec|ee|eg|eh|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|Ja|sk|sl|sm|sn|so|sr|ss|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|yu|za|zm|zw)/)(?:[^\s()<>{}\[\]]+|\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\))+(?:\([^\s()]*?\([^\s()]+\)[^\s()]*?\)|\([^\s]+?\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’])|(?:(? %s ' % (self.file_url, self.file_url) + + if self.media_type == Media.MEDIA_TYPES.IMAGE: + embed = ' Your browser does not support the video tag. ' % (self.file_url) + + self.oembed_html = embed + + def get_absolute_url(self): + return reverse('mesh_media_view', kwargs={'slug': self.slug,}) diff --git a/django_mesh/templates/django_mesh/media_index_view.html b/django_mesh/templates/django_mesh/media_index_view.html new file mode 100644 index 0000000..352737e --- /dev/null +++ b/django_mesh/templates/django_mesh/media_index_view.html @@ -0,0 +1,23 @@ +{% extends "django_mesh/base.html" %} + +{% block content %} + +

    +{% if media_list %} + + {% for media in media_list %} + + {% endfor %} + +{% else %} + There are no files to display. +{% endif %} +
    + +{% endblock content %} \ No newline at end of file diff --git a/django_mesh/templates/django_mesh/media_view.html b/django_mesh/templates/django_mesh/media_view.html new file mode 100644 index 0000000..7a415f6 --- /dev/null +++ b/django_mesh/templates/django_mesh/media_view.html @@ -0,0 +1,17 @@ +{% extends "django_mesh/base.html" %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + +
    + {% if Media.MEDIA_TYPES.IMAGE %} + + {{ media.title }} + {% else %} + {{ media.oembed_html | safe}} + {% endif %} + +{% endblock content %} \ No newline at end of file diff --git a/django_mesh/tests/test_managers.py b/django_mesh/tests/test_managers.py index a472cfb..a384204 100644 --- a/django_mesh/tests/test_managers.py +++ b/django_mesh/tests/test_managers.py @@ -15,7 +15,7 @@ #along with this program. If not, see . # App imports -from ..models import Post, Channel, Tag +from ..models import Post, Channel, Tag, Media # Test imports from .util import BaseTestCase @@ -288,4 +288,50 @@ def test_tags_only_show_up_if_user_has_access_to_that_Channel(self): viewable = Tag.objects.get_for_user(new_user) self.assertIn(self.t1, viewable) - self.assertNotIn(self.t2, viewable) \ No newline at end of file + self.assertNotIn(self.t2, viewable) + + +class MediaQuerySetTestCase(BaseTestCase): + def test_get_for_user_with_a_user(self): + user = self.user + + self.c1.save() # public + + self.following_private_channel.save() # following private + self.following_private_channel.followers.add(user) + + self.c3.save() # private channel that we are not following + + self.f1.channel = self.c1 + self.f1.save() + + self.f2.channel = self.c3 + self.f2.save() + + self.f3.channel = self.following_private_channel + self.f3.save() + + viewable = Media.objects.get_for_user(user) + + self.assertIn(self.f1, viewable) + self.assertIn(self.f3, viewable) + self.assertNotIn(self.f2, viewable) + + + def test_get_for_user_anonymous(self): + user = self.user + user.id == None + + self.c1.save() + self.c3.save() + + self.f1.channel = self.c1 + self.f2.channel = self.c3 + + self.f1.save() + self.f2.save() + + viewable = Media.objects.get_for_user(user) + + self.assertIn(self.f1, viewable) + self.assertNotIn(self.f2, viewable) diff --git a/django_mesh/tests/test_models.py b/django_mesh/tests/test_models.py index 13602e7..afaceb0 100644 --- a/django_mesh/tests/test_models.py +++ b/django_mesh/tests/test_models.py @@ -16,6 +16,7 @@ # Test imports from .util import BaseTestCase + import markdown, textile import requests from pyembed.markdown import PyEmbedMarkdown @@ -26,6 +27,9 @@ from mock import patch, Mock +from django.core.files import File +from django.core.files.storage import Storage + class PostTestCase(BaseTestCase): @@ -192,4 +196,16 @@ def test_get_absolute_url(self): def test_str_unicode(self): self.t1.save() returned_title = str(self.t1) - self.assertEqual(self.t1.title, returned_title) \ No newline at end of file + self.assertEqual(self.t1.title, returned_title) + +class MediaTestCase(BaseTestCase): + def test_get_absolute_url(self): + + self.c1.save() + self.f1.channel = self.c1 + + self.f1.save() + url = self.f1.get_absolute_url() + + self.assertGreater(len(url), 0) + # blank slug isn't allowed so only have to test with slug \ No newline at end of file diff --git a/django_mesh/tests/test_urls.py b/django_mesh/tests/test_urls.py index 1bcf1da..4fb4927 100644 --- a/django_mesh/tests/test_urls.py +++ b/django_mesh/tests/test_urls.py @@ -22,6 +22,7 @@ # App imports from ..models import Post +import json class IndexViewTestCase(BaseTestCase): @@ -760,4 +761,76 @@ def test_tags_for_channel_logged_in_user_is_not_following_does_not_show_up(self) self.assertContains(response, self.t1) self.assertNotContains(response, self.t2) - self.assertContains(response, self.t3) \ No newline at end of file + self.assertContains(response, self.t3) + +class MediaIndexViewTestCase(BaseTestCase): + def test_upload_shows_up_on_index_page(self): + self.c1.save() + self.c3.save() + + self.f1.channel = self.c1 + self.f1.save() + + self.f2.channel = self.c3 + self.f2.save() + + response = self.client.get(reverse('mesh_media_index')) + self.assertEqual(response.status_code, 200) + + self.assertContains(response, self.f1.file_url) + self.assertNotContains(response, self.f2.file_url) + +class MediaDetailViewTestCase(BaseTestCase): + def test_detail_page_only_shows_correct_media(self): + self.c1.save() + self.c2.save() + + self.f1.channel = self.c1 + self.f1.save() + + self.f2.channel = self.c2 + self.f2.save() + + response = self.client.get(reverse('mesh_media_view', kwargs={'slug': self.f1.slug})) + self.assertEqual(response.status_code, 200) + + self.assertContains(response, self.f1.file_url) + self.assertNotContains(response, self.f2.file_url) + + +class OembedDetailViewTestCase(BaseTestCase): + # def test_post_request_raises_404(self): ######## you said we dont need to worry about this case right? + # self.c1.save() + # self.f1.channel = self.c1 + # self.f1.save() + + # response = self.client.post(reverse('mesh_oembed', kwargs={'slug': self.f1.slug})) + # self.assertEqual(response.status_code, 404) + + def test_get_request_returns_required_data_for_embedly_for_each_file_type(self): + self.c1.save() + self.f1.channel = self.c1 + self.f1.save() + + self.f4.channel = self.c1 + self.f4.save() + + response = self.client.get(reverse('mesh_oembed', kwargs={'slug': self.f1.slug})) + response2 = self.client.get(reverse('mesh_oembed', kwargs={'slug': self.f4.slug})) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response2.status_code, 200) + + self.assertContains(response, '"url": "%(url)s"' % {"url": self.f1.file_url}) + self.assertContains(response, '"type": "photo"') + self.assertContains(response, '"version": 1.0' ) + self.assertContains(response, '"width": 240' ) + self.assertContains(response, '"height": 160' ) + + self.assertContains(response2, '"url": "%(url)s"' % {"url": self.f4.file_url}) + self.assertContains(response2, '"type": "video"') + self.assertContains(response2, '"version": 1.0' ) + self.assertContains(response2, '"width": 700' ) + self.assertContains(response2, '"height": 350' ) + self.assertContains(response2, '"html": %(html)s' % {"html": json.dumps(self.f4.oembed_html)}) + diff --git a/django_mesh/tests/util.py b/django_mesh/tests/util.py index f3cedbe..4e41948 100644 --- a/django_mesh/tests/util.py +++ b/django_mesh/tests/util.py @@ -27,7 +27,7 @@ #App imports from .. import models -from ..models import Channel, Post, Tag +from ..models import Channel, Post, Tag, Media class BaseTestCase(TestCase): def setUp(self): @@ -178,7 +178,39 @@ def setUp(self): self.t4 = Tag( slug='slug-for-a-tag-4', title='title for a tag 4', - text=' this is tag 4s text', + text=' this is tag 4s text' + ) + + self.f1 = Media( + slug = 'file', + title='f1', + upload_file = '/upload/f1', + + media_type = 1 + ) + + self.f2 = Media( + slug = 'file2', + upload_file = '/upload/f2', + + media_type = 1, + title='f2', + ) + + self.f3 = Media( + slug = 'file3', + upload_file = '/upload/f3', + + media_type = 1, + title='f3', + ) + + self.f4 = Media( + slug = 'video-type', + upload_file = '/upload/f4', + + media_type = 2, + title='f4', ) self.t5 = Tag( diff --git a/django_mesh/urls.py b/django_mesh/urls.py index 2eebd07..0a7e3c8 100644 --- a/django_mesh/urls.py +++ b/django_mesh/urls.py @@ -17,7 +17,7 @@ #Django imports from django.conf.urls import patterns, url -from .views import IndexView, ChannelIndexView, ChannelDetailView, PostIndexView, PostDetailView, TagDetailView, TagIndexView +from .views import IndexView, ChannelIndexView, ChannelDetailView, PostIndexView, PostDetailView, TagDetailView, TagIndexView, MediaDetailView, MediaIndexView, OembedDetailView from django_mesh import views urlpatterns = patterns('', @@ -29,4 +29,7 @@ url(r'^tags/(?P.*)/$' , TagDetailView.as_view(), name="mesh_tag_view"), url(r'^posts/(?P.*)/$', PostDetailView.as_view(), name="mesh_post_view"), url(r'^tags/$', TagIndexView.as_view(), name="mesh_tag_index"), + url(r'^media/$', MediaIndexView.as_view(), name="mesh_media_index"), + url(r'^media/(?P.*)/$', MediaDetailView.as_view(), name="mesh_media_view"), + url(r'^oembed/media/(?P.*)/$', OembedDetailView.as_view(), name="mesh_oembed"), ) diff --git a/django_mesh/views.py b/django_mesh/views.py index 4ecf3cd..fcab5df 100644 --- a/django_mesh/views.py +++ b/django_mesh/views.py @@ -23,10 +23,19 @@ from django.shortcuts import get_object_or_404 from django.db.models import Q from django.core.urlresolvers import reverse -from django.http import HttpResponseRedirect, Http404 +from django.http import HttpResponseRedirect, Http404, HttpResponse +from django.contrib.sites.shortcuts import get_current_site +from django.forms.models import model_to_dict + +import json # App imports -from .models import Channel, Post, Tag +from .models import Channel, Post, Tag, Media + +#test upload +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.core.urlresolvers import reverse, resolve logger = logging.getLogger(__name__) @@ -94,15 +103,6 @@ def get_queryset(self, *args, **kwargs): ret = super(PostDetailView, self).get_queryset(*args, **kwargs) return ret.get_for_user(user=self.request.user).active() -def self_enrollment(request, *args, **kwargs): - user = request.user - if request.method == 'POST': - channel = get_object_or_404(Channel.objects.get_for_user(user), slug=kwargs['slug']) - channel.followers.add(user) - return HttpResponseRedirect(reverse('mesh_channel_index')) - else: - return HttpResponseRedirect(reverse('mesh_channel_index')) - class TagDetailView(ListView): model = Post template_name = 'django_mesh/tag_view.html' @@ -135,4 +135,68 @@ class TagIndexView(ListView): def get_queryset(self, *args, **kwargs): qs = super(TagIndexView, self).get_queryset(*args, **kwargs) - return qs.get_for_user(self.request.user) \ No newline at end of file + return qs.get_for_user(self.request.user) + +class MediaDetailView(DetailView): + model = Media + template_name = 'django_mesh/media_view.html' + + def get_context_data(self, **kwargs): + context = super(MediaDetailView, self).get_context_data(**kwargs) + context['Media'] = self.model + return context + + def get_queryset(self, *args, **kwargs): + qs = super(MediaDetailView, self).get_queryset(*args, **kwargs) + return qs.get_for_user(self.request.user) + +class MediaIndexView(ListView): + model = Media + template_name = 'django_mesh/media_index_view.html' + + def get_queryset(self, *args, **kwargs): + qs = super(MediaIndexView, self).get_queryset(*args, **kwargs) + return qs.get_for_user(self.request.user) + +def self_enrollment(request, *args, **kwargs): + user = request.user + if request.method == 'POST': + channel = get_object_or_404(Channel.objects.get_for_user(user), slug=kwargs['slug']) + channel.followers.add(user) + return HttpResponseRedirect(reverse('mesh_channel_index')) + else: + return HttpResponseRedirect(reverse('mesh_channel_index')) + +class OembedDetailView(DetailView): + + model = Media + + @property + def get_site_domain(request): + return get_current_site(request).domain + + + def dispatch(self, request, *args, **kwargs): + self.media = get_object_or_404(Media.objects.get_for_user(user=self.request.user), slug=self.kwargs['slug']) + + user = request.user + + file_dict = {} + + file_dict['version'] = 1.0 + file_dict['title'] = self.media.title + file_dict['url'] = self.media.file_url + file_dict['provider_name'] = self.get_site_domain + + if self.media.media_type == Media.MEDIA_TYPES.IMAGE: + file_dict['type'] = 'photo' + file_dict['width'] = 240 + file_dict['height'] = 160 + + elif self.media.media_type == Media.MEDIA_TYPES.VIDEO: + file_dict['type'] = 'video' + file_dict['html'] = self.media.oembed_html + file_dict['height'] = 350 + file_dict['width'] = 700 + + return HttpResponse(json.dumps([file_dict]), content_type="application/json") diff --git a/django_mesh_test_project/settings.py b/django_mesh_test_project/settings.py index 529f062..317c6d6 100644 --- a/django_mesh_test_project/settings.py +++ b/django_mesh_test_project/settings.py @@ -44,6 +44,7 @@ 'django.contrib.sitemaps', 'django.contrib.sites', #app that we want to test + 'django.contrib.webdesign', 'django_mesh', 'settings_context_processor', ) @@ -89,12 +90,18 @@ # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.6/howto/static-files/ -STATIC_URL = '/static/' STATIC_PATH = os.path.join(PROJECT_ROOT, 'static') - +STATIC_URL = '/static/' STATICFILES_DIRS = ( - STATIC_PATH, - ) + STATIC_PATH, +) + +MEDIA_PATH = os.path.join(PROJECT_ROOT, 'media') +MEDIA_URL = '/media/' +MEDIAFILES_DIRS = ( + MEDIA_PATH, +) + SITE_ID = 1 TEMPLATE_DIRS = ( diff --git a/django_mesh_test_project/urls.py b/django_mesh_test_project/urls.py index 22e1fd4..5dfcdd7 100644 --- a/django_mesh_test_project/urls.py +++ b/django_mesh_test_project/urls.py @@ -1,4 +1,6 @@ from django.conf.urls import patterns, include, url +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin admin.autodiscover() @@ -6,4 +8,4 @@ urlpatterns = patterns('', url(r'^blog/', include('django_mesh.urls')), url(r'^admin/', include(admin.site.urls)), -) \ No newline at end of file +) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file From 303df274dfa4c0b83feb7c95138cea13a339fce6 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Sun, 8 Mar 2015 03:08:00 -0400 Subject: [PATCH 4/6] media_height, media_width --- django_mesh/models.py | 8 +++++--- django_mesh/tests/util.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/django_mesh/models.py b/django_mesh/models.py index afcc2b0..3504437 100644 --- a/django_mesh/models.py +++ b/django_mesh/models.py @@ -207,13 +207,15 @@ class Media(_Abstract): channel = models.ForeignKey(Channel) objects = MediaQuerySet.as_manager() - upload_file = models.FileField(upload_to='uploads/%Y/%m/%d') media_type = models.IntegerField(max_length=1, default=MEDIA_TYPES.NONE, choices=MEDIA_TYPES) oembed_html = models.TextField(default='', blank=True) + media_height = models.SmallIntegerField(null=True) + media_width = models.SmallIntegerField(null=True) + @property def file_url(self): return self.upload_file.url @@ -223,10 +225,10 @@ def render(self): embed = ' %s ' % (self.file_url, self.file_url) if self.media_type == Media.MEDIA_TYPES.IMAGE: - embed = ' Your browser does not support the video tag. ' % (self.file_url) + embed = '' % (self.media_width, self.media_height, self.file_url) self.oembed_html = embed diff --git a/django_mesh/tests/util.py b/django_mesh/tests/util.py index 4e41948..d1251b2 100644 --- a/django_mesh/tests/util.py +++ b/django_mesh/tests/util.py @@ -185,14 +185,16 @@ def setUp(self): slug = 'file', title='f1', upload_file = '/upload/f1', - + media_height = 100, + media_width = 100, media_type = 1 ) self.f2 = Media( slug = 'file2', upload_file = '/upload/f2', - + media_height = 100, + media_width = 100, media_type = 1, title='f2', ) @@ -200,7 +202,8 @@ def setUp(self): self.f3 = Media( slug = 'file3', upload_file = '/upload/f3', - + media_height = 100, + media_width = 100, media_type = 1, title='f3', ) @@ -208,7 +211,8 @@ def setUp(self): self.f4 = Media( slug = 'video-type', upload_file = '/upload/f4', - + media_height = 100, + media_width = 100, media_type = 2, title='f4', ) From db702c6f7291ec70c94a69d6d9c3b59dfaa8e9c8 Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Sun, 8 Mar 2015 03:21:20 -0400 Subject: [PATCH 5/6] thumbnail dimensions for index page set by user. --- django_mesh/models.py | 3 +++ django_mesh/templates/django_mesh/media_index_view.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/django_mesh/models.py b/django_mesh/models.py index 3504437..a5adb70 100644 --- a/django_mesh/models.py +++ b/django_mesh/models.py @@ -216,6 +216,9 @@ class Media(_Abstract): media_height = models.SmallIntegerField(null=True) media_width = models.SmallIntegerField(null=True) + thumbnail_height = models.SmallIntegerField(default=120) + thumbnail_width = models.SmallIntegerField(default=200) + @property def file_url(self): return self.upload_file.url diff --git a/django_mesh/templates/django_mesh/media_index_view.html b/django_mesh/templates/django_mesh/media_index_view.html index 352737e..c8ff124 100644 --- a/django_mesh/templates/django_mesh/media_index_view.html +++ b/django_mesh/templates/django_mesh/media_index_view.html @@ -10,7 +10,7 @@ {{ media.title|safe }}
    -

    +

    {% endfor %} From 23486ff00fcb7abfa2d4c81242078f6cc05a7a9d Mon Sep 17 00:00:00 2001 From: Hung Nguyen Date: Sun, 8 Mar 2015 04:02:01 -0400 Subject: [PATCH 6/6] fixed media detail page to display based on chosen size, but with link to actual file --- django_mesh/models.py | 2 +- django_mesh/templates/django_mesh/media_view.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/django_mesh/models.py b/django_mesh/models.py index a5adb70..108f3c3 100644 --- a/django_mesh/models.py +++ b/django_mesh/models.py @@ -228,7 +228,7 @@ def render(self): embed = ' %s ' % (self.file_url, self.file_url) if self.media_type == Media.MEDIA_TYPES.IMAGE: - embed = '%s' % (self.file_url, self.title, self.media_width, self.media_height) elif self.media_type == Media.MEDIA_TYPES.VIDEO: embed = '' % (self.media_width, self.media_height, self.file_url) diff --git a/django_mesh/templates/django_mesh/media_view.html b/django_mesh/templates/django_mesh/media_view.html index 7a415f6..6abe257 100644 --- a/django_mesh/templates/django_mesh/media_view.html +++ b/django_mesh/templates/django_mesh/media_view.html @@ -9,7 +9,7 @@
    {% if Media.MEDIA_TYPES.IMAGE %} - {{ media.title }} + {{ media.oembed_html|safe }} {% else %} {{ media.oembed_html | safe}} {% endif %}