From 0ddc5a6ff60f95efc69b7c608a7efa6e89789a0f Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Wed, 12 Feb 2014 22:21:18 +0200 Subject: [PATCH 1/5] Added the endpoints.json and modified embeds.py Now embeds.py has the infrastructre to select a different get_embed function depending on if embedly should be used or not. the different get_embed (get_embed_oembed) function should be written now... --- wagtail/wagtailembeds/embeds.py | 25 +++++- wagtail/wagtailembeds/endpoints.json | 114 +++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 wagtail/wagtailembeds/endpoints.json diff --git a/wagtail/wagtailembeds/embeds.py b/wagtail/wagtailembeds/embeds.py index 59d349a26..a2cff4d62 100644 --- a/wagtail/wagtailembeds/embeds.py +++ b/wagtail/wagtailembeds/embeds.py @@ -1,12 +1,18 @@ from datetime import datetime -from embedly import Embedly + from django.conf import settings from .models import Embed +import os +module_dir = os.path.dirname(__file__) # get current directory +file_path = os.path.join(module_dir, 'endpoints.json') +print file_path +print open(file_path).read() -def get_embed(url, max_width=None): + +def get_embed_embedly(url, max_width=None): # Check database try: return Embed.objects.get(url=url, max_width=max_width) @@ -52,3 +58,18 @@ def get_embed(url, max_width=None): # Return new embed return row + +def get_embed_oembed(url, max_width=None): + pass + +get_embed = get_embed_oembed +try: + from embedly import Embedly + if hasattr(settings,'EMBEDLY_KEY'): + get_embed = get_embed_embedly +except: + pass + +print get_embed + + \ No newline at end of file diff --git a/wagtail/wagtailembeds/endpoints.json b/wagtail/wagtailembeds/endpoints.json new file mode 100644 index 000000000..7cee0cec7 --- /dev/null +++ b/wagtail/wagtailembeds/endpoints.json @@ -0,0 +1,114 @@ +[ +{ +"url": "http://*.blip.tv/*", +"url_re": "blip\\.tv/.+", +"example_url": "http://pycon.blip.tv/file/2058801/", +"endpoint_url": "http://blip.tv/oembed/", +"title": "blip.tv" +}, +{ +"url": "http://*.dailymotion.com/*", +"url_re": "dailymotion\\.com/.+", +"example_url": "http://www.dailymotion.com/video/x5ioet_phoenix-mars-lander_tech", +"endpoint_url": "http://www.dailymotion.com/api/oembed/", +"title": "Dailymotion" +}, +{ +"url": "http://*.flickr.com/photos/*", +"url_re": "flickr\\.com/photos/[-.\\w@]+/\\d+/?", +"example_url": "http://www.flickr.com/photos/fuffer2005/2435339994/", +"endpoint_url": "http://www.flickr.com/services/oembed/", +"title": "Flickr Photos" +}, +{ +"url": "http://www.hulu.com/watch/*", +"url_re": "hulu\\.com/watch/.*", +"example_url": "http://www.hulu.com/watch/20807/late-night-with-conan", +"endpoint_url": "http://www.hulu.com/api/oembed.json", +"title": "Hulu" +}, +{ +"url": "http://*.nfb.ca/film/*", +"url_re": "nfb\\.ca/film/[-\\w]+/?", +"example_url": "http://www.nfb.ca/film/blackfly/", +"endpoint_url": "http://www.nfb.ca/remote/services/oembed/", +"title": "National Film Board of Canada" +}, +{ +"url": "http://qik.com/*", +"url_re": "qik\\.com/\\w+", +"example_url": "http://qik.com/video/86776", +"endpoint_url": "http://qik.com/api/oembed.json", +"title": "Qik Video" +}, +{ +"url": "http://*.revision3.com/*", +"url_re": "revision3\\.com/.+", +"example_url": "http://revision3.com/diggnation/2008-04-17xsanned/", +"endpoint_url": "http://revision3.com/api/oembed/", +"title": "Revision3" +}, +{ +"url": "http://*.scribd.com/*", +"url_re": "scribd\\.com/.+", +"example_url": "http://www.scribd.com/doc/17896323/Indian-Automobile-industryPEST", +"endpoint_url": "http://www.scribd.com/services/oembed", +"title": "Scribd" +}, +{ +"url": "http://*.viddler.com/explore/*", +"url_re": "viddler\\.com/explore/.*/videos/\\w+/?", +"example_url": "http://www.viddler.com/explore/engadget/videos/14/", +"endpoint_url": "http://lab.viddler.com/services/oembed/", +"title": "Viddler Video" +}, +{ +"url": "http://www.vimeo.com/* and http://www.vimeo.com/groups/*/videos/*", +"url_re": "vimeo\\.com/.*", +"example_url": "http://www.vimeo.com/1211060", +"endpoint_url": "http://www.vimeo.com/api/oembed.json", +"title": "Vimeo" +}, +{ +"url": "http://*.youtube.com/watch*", +"url_re": "youtube\\.com/watch.+v=[\\w-]+&?", +"example_url": "http://www.youtube.com/watch?v=vk1HvP7NO5w", +"endpoint_url": "http://www.youtube.com/oembed", +"title": "YouTube" +}, +{ +"url": "http://dotsub.com/view/*", +"url_re": "dotsub\\.com/view/[-\\da-zA-Z]+$", +"example_url": "http://dotsub.com/view/10e3cb5e-96c7-4cfb-bcea-8ab11e04e090", +"endpoint_url": "http://dotsub.com/services/oembed", +"title": "dotSUB.com" +}, +{ +"url": "http://yfrog.(com|ru|com.tr|it|fr|co.il|co.uk|com.pl|pl|eu|us)/*", +"url_re": "yfrog\\.(com|ru|com\\.tr|it|fr|co\\.il|co\\.uk|com\\.pl|pl|eu|us)/[a-zA-Z0-9]+$", +"example_url": "http://yfrog.com/0wgvcpj", +"endpoint_url": "http://www.yfrog.com/api/oembed", +"title": "YFrog" +}, +{ +"url": "http://*.clikthrough.com/theater/video/*", +"url_re": "clikthrough\\.com/theater/video/\\d+$", +"example_url": "http://www.clikthrough.com/theater/video/55", +"endpoint_url": "http://clikthrough.com/services/oembed", +"title": "Clikthrough" +}, +{ +"url": "http://*.kinomap.com/*", +"url_re": "kinomap\\.com/.+", +"example_url": "http://www.kinomap.com/kms-vzkpc7", +"endpoint_url": "http://www.kinomap.com/oembed", +"title": "Kinomap" +}, +{ +"url": "http://*.photobucket.com/albums/*|http://*.photobucket.com/groups/*", +"url_re": "photobucket\\.com/(albums|groups)/.+$", +"example_url": "http://img.photobucket.com/albums/v211/JAV123/Michael%20Holland%20Candle%20Burning/_MG_5661.jpg", +"endpoint_url": "http://photobucket.com/oembed", +"title": "Photobucket" +} +] From 74b9f4340120962d099a346ef3422f19db2918b2 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Thu, 13 Feb 2014 04:01:51 +0200 Subject: [PATCH 2/5] Make embedly optional and refactor code This fixes #26. First of all there is some refactoring: All low level embed functions have been moved to the wagtail.wagtailembeds.embeds package. There you will see: . embed.py (which is more or less a copy of the old embeds.py) . oembed_api.py which includes some low level code for using embedding with the help of oembed, without any external dependencies (python-oembed was not working very well and since oembed is just a URL get to a specific URL I implemented it with urllib2 and json), . endpoints.json which is a list of oembed endpoints I got from https://github.com/panzi/oembedendpoints/blob/master/endpoints-regexp.json . unittests.py with some tests to check that well known sites like youtube, vimeo etc work fine with the oembed_api The code refactoring also includes a number of exceptions. The get_embed function now is included in try / except blocks and if an exception occurs then the output will be an emtpy string (this was the behavior before the refactor). However, in the chooser.py function the type of the exception will be checked and a nice message will be shown to the editor. Finally, to choose between embedly and the oembed a check is made to see if the embedly library has been installed and also check if EMBEDLY_KEY has been set in the settings. If these two checks are both true then the get_embed will be assigned to get_embed_embedly -- else it will be assigned to get_embed_oembed. --- wagtail/wagtailcore/rich_text.py | 5 +- wagtail/wagtailembeds/__init__.py | 2 +- wagtail/wagtailembeds/embeds/__init__.py | 0 wagtail/wagtailembeds/embeds/embed.py | 80 +++++ wagtail/wagtailembeds/embeds/endpoints.json | 295 ++++++++++++++++++ wagtail/wagtailembeds/embeds/oembed_api.py | 52 +++ wagtail/wagtailembeds/embeds/unittests.py | 66 ++++ wagtail/wagtailembeds/format.py | 25 +- .../templatetags/embed_filters.py | 11 +- wagtail/wagtailembeds/tests.py | 2 +- wagtail/wagtailembeds/views/chooser.py | 25 +- 11 files changed, 539 insertions(+), 24 deletions(-) create mode 100644 wagtail/wagtailembeds/embeds/__init__.py create mode 100644 wagtail/wagtailembeds/embeds/embed.py create mode 100644 wagtail/wagtailembeds/embeds/endpoints.json create mode 100644 wagtail/wagtailembeds/embeds/oembed_api.py create mode 100644 wagtail/wagtailembeds/embeds/unittests.py diff --git a/wagtail/wagtailcore/rich_text.py b/wagtail/wagtailcore/rich_text.py index bd00ee503..aa8f25336 100644 --- a/wagtail/wagtailcore/rich_text.py +++ b/wagtail/wagtailcore/rich_text.py @@ -52,7 +52,10 @@ class ImageEmbedHandler(object): format = get_image_format(attrs['format']) if for_editor: - return format.image_to_editor_html(image, attrs['alt']) + try: + return format.image_to_editor_html(image, attrs['alt']) + except: + return '' else: return format.image_to_html(image, attrs['alt']) diff --git a/wagtail/wagtailembeds/__init__.py b/wagtail/wagtailembeds/__init__.py index b75cbc491..8e3708ca9 100644 --- a/wagtail/wagtailembeds/__init__.py +++ b/wagtail/wagtailembeds/__init__.py @@ -1,2 +1,2 @@ from .models import Embed -from .embeds import get_embed +from .embeds.embed import get_embed diff --git a/wagtail/wagtailembeds/embeds/__init__.py b/wagtail/wagtailembeds/embeds/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wagtail/wagtailembeds/embeds/embed.py b/wagtail/wagtailembeds/embeds/embed.py new file mode 100644 index 000000000..3d91a7fae --- /dev/null +++ b/wagtail/wagtailembeds/embeds/embed.py @@ -0,0 +1,80 @@ +from datetime import datetime +from django.conf import settings +from ..models import Embed +import oembed_api + +class EmbedlyException(Exception): pass +class AccessDeniedEmbedlyException(Exception): pass +class NotFoundEmbedlyException(Exception): pass + +def get_embed_embedly(url, max_width=None): + # Check database + try: + return Embed.objects.get(url=url, max_width=max_width) + except Embed.DoesNotExist: + pass + + client = Embedly(key=settings.EMBEDLY_KEY) + + if max_width is not None: + oembed = client.oembed(url, maxwidth=max_width, better=False) + else: + oembed = client.oembed(url, better=False) + + # Check for error + if oembed.get('error'): + if oembed['error_code'] in [401,403]: + raise AccessDeniedEmbedlyException + elif oembed['error_code'] == 404: + raise NotFoundEmbedlyException + else: + raise EmbedlyException + + return save_embed(url, max_width, oembed) + + +def get_embed_oembed(url, max_width=None): + # Check database + try: + return Embed.objects.get(url=url, max_width=max_width) + except Embed.DoesNotExist: + pass + + oembed = oembed_api.get_embed_oembed(url, max_width) + return save_embed(url, max_width, oembed) + + +def save_embed(url, max_width, oembed): + row, created = Embed.objects.get_or_create( + url=url, + max_width=max_width, + defaults={ + 'type': oembed['type'], + 'title': oembed['title'], + 'thumbnail_url': oembed.get('thumbnail_url'), + 'width': oembed.get('width'), + 'height': oembed.get('height') + } + ) + + if oembed['type'] == 'photo': + html = '' % (oembed['url'], ) + else: + html = oembed.get('html') + + if html: + row.html = html + row.last_updated = datetime.now() + row.save() + + return row + +# As a default use oembed +get_embed = get_embed_oembed +try: + from embedly import Embedly + # if EMBEDLY_KEY is set and embedly library found the use embedly + if hasattr(settings,'EMBEDLY_KEY'): + get_embed = get_embed_embedly +except: + pass diff --git a/wagtail/wagtailembeds/embeds/endpoints.json b/wagtail/wagtailembeds/embeds/endpoints.json new file mode 100644 index 000000000..c289b7183 --- /dev/null +++ b/wagtail/wagtailembeds/embeds/endpoints.json @@ -0,0 +1,295 @@ +{ + "https://speakerdeck.com/oembed.{format}": [ + "^http(?:s)?://speakerdeck\\.com/.+$" + ], + "https://alpha-api.app.net/oembed": [ + "^http(?:s)?://alpha\\.app\\.net/[^#?/]+/post/.+$", + "^http(?:s)?://photos\\.app\\.net/[^#?/]+/.+$" + ], + "http://www.youtube.com/oembed": [ + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/watch.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/v/.+$", + "^http(?:s)?://youtu\\.be/.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/user/.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/[^#?/]+#[^#?/]+/.+$", + "^http(?:s)?://m\\.youtube\\.com/index.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/profile.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/view_play_list.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?youtube\\.com/playlist.+$" + ], + "http://backend.deviantart.com/oembed": [ + "^http://(?:[-\\w]+\\.)?deviantart\\.com/art/.+$", + "^http://fav\\.me/.+$", + "^http://sta\\.sh/.+$", + "^http://(?:[-\\w]+\\.)?deviantart\\.com/[^#?/]+#/d.+$" + ], + "http://blip.tv/oembed/": [ + "^http://[-\\w]+\\.blip\\.tv/.+$" + ], + "http://www.dailymotion.com/api/oembed/": [ + "^http://[-\\w]+\\.dailymotion\\.com/.+$" + ], + "http://www.flickr.com/services/oembed/": [ + "^http://[-\\w]+\\.flickr\\.com/photos/.+$", + "^http://flic\\.kr\\.com/.+$" + ], + "http://www.hulu.com/api/oembed.{format}": [ + "^http://www\\.hulu\\.com/watch/.+$" + ], + "http://www.nfb.ca/remote/services/oembed/": [ + "^http://(?:[-\\w]+\\.)?nfb\\.ca/film/.+$" + ], + "http://qik.com/api/oembed.{format}": [ + "^http://qik\\.com/.+$", + "^http://qik\\.ly/.+$" + ], + "http://revision3.com/api/oembed/": [ + "^http://[-\\w]+\\.revision3\\.com/.+$" + ], + "http://www.scribd.com/services/oembed": [ + "^http://[-\\w]+\\.scribd\\.com/.+$" + ], + "http://www.viddler.com/oembed/": [ + "^http://[-\\w]+\\.viddler\\.com/v/.+$", + "^http://[-\\w]+\\.viddler\\.com/explore/.+$" + ], + "http://www.vimeo.com/api/oembed.{format}": [ + "^http(?:s)?://(?:www\\.)?vimeo\\.com/.+$", + "^http(?:s)?://player\\.vimeo\\.com/.+$" + ], + "http://dotsub.com/services/oembed": [ + "^http://dotsub\\.com/view/.+$" + ], + "http://www.yfrog.com/api/oembed": [ + "^http(?:s)?://(?:www\\.)?yfrog\\.com/.+$", + "^http(?:s)?://(?:www\\.)?yfrog\\.us/.+$" + ], + "http://clikthrough.com/services/oembed": [ + "^http(?:s)?://(?:[-\\w]+\\.)?clikthrough\\.com/.+$" + ], + "http://www.kinomap.com/oembed": [ + "^http://[-\\w]+\\.kinomap\\.com/.+$" + ], + "https://photobucket.com/oembed": [ + "^http://(?:[-\\w]+\\.)?photobucket\\.com/albums/.+$", + "^http://(?:[-\\w]+\\.)?photobucket\\.com/groups/.+$" + ], + "http://api.instagram.com/oembed": [ + "^http://instagr\\.am/p/.+$", + "^http://instagram\\.com/p/.+$" + ], + "https://www.slideshare.net/api/oembed/2": [ + "^http://www\\.slideshare\\.net/.+$" + ], + "http://tv.majorleaguegaming.com/oembed": [ + "^http://mlg\\.tv/.+$", + "^http://tv\\.majorleaguegaming\\.com/.+$" + ], + "http://my.opera.com/service/oembed": [ + "^http://my\\.opera\\.com/.+$" + ], + "http://skitch.com/oembed": [ + "^http(?:s)?://(?:www\\.)?skitch\\.com/.+$", + "^http://skit\\.ch/.+$" + ], + "https://api.twitter.com/1/statuses/oembed.{format}": [ + "^http(?:s)?://twitter\\.com/(?:#!)?[^#?/]+/status/.+$" + ], + "https://soundcloud.com/oembed": [ + "^https://soundcloud\\.com/[^#?/]+/.+$" + ], + "http://www.collegehumor.com/oembed.{format}": [ + "^http://(?:www\\.)?collegehumor\\.com/video/.+$", + "^http://(?:www\\.)?collegehumor\\.com/video:.+$" + ], + "http://www.polleverywhere.com/services/oembed/": [ + "^http://www\\.polleverywhere\\.com/polls/.+$", + "^http://www\\.polleverywhere\\.com/multiple_choice_polls/.+$", + "^http://www\\.polleverywhere\\.com/free_text_polls/.+$" + ], + "http://www.ifixit.com/Embed": [ + "^http://www\\.ifixit\\.com/[^#?/]+/[^#?/]+/.+$" + ], + "http://api.smugmug.com/services/oembed/": [ + "^http(?:s)?://(?:www\\.)?smugmug\\.com/[^#?/]+/.+$" + ], + "https://github.com/api/oembed": [ + "^http(?:s)?://gist\\.github\\.com/.+$" + ], + "http://animoto.com/services/oembed": [ + "^http://animoto\\.com/play/.+$" + ], + "http://www.rdio.com/api/oembed": [ + "^http://(?:wwww\\.)?rdio\\.com/people/[^#?/]+/playlists/.+$", + "^http://[-\\w]+\\.rdio\\.com/artist/[^#?/]+/album/.+$" + ], + "http://api.5min.com/oembed.{format}": [ + "^http://www\\.5min\\.com/video/.+$" + ], + "http://500px.com/photo/{1}/oembed.{format}": [ + "^http://500px\\.com/photo/([^#?/]+)(?:.+)?$" + ], + "http://api.dipdive.com/oembed.{format}": [ + "^http://[-\\w]+\\.dipdive\\.com/media/.+$" + ], + "http://video.yandex.ru/oembed.{format}": [ + "^http://video\\.yandex\\.ru/users/[^#?/]+/view/.+$" + ], + "http://www.mixcloud.com/oembed/": [ + "^http://www\\.mixcloud\\.com/oembed/[^#?/]+/.+$" + ], + "http://www.kickstarter.com/services/oembed": [ + "^http(?:s)://[-\\w]+\\.kickstarter\\.com/projects/.+$" + ], + "http://coub.com/api/oembed.{format}": [ + "^http(?:s)?://coub\\.com/view/.+$", + "^http(?:s)?://coub\\.com/embed/.+$" + ], + "http://www.screenr.com/api/oembed.{format}": [ + "^http://www\\.screenr\\.com/.+$" + ], + "http://www.funnyordie.com/oembed.{format}": [ + "^http://www\\.funnyordie\\.com/videos/.+$" + ], + "http://fast.wistia.com/oembed.{format}": [ + "^http://[-\\w]+\\.wista\\.com/medias/.+$" + ], + "http://www.ustream.tv/oembed": [ + "^http(?:s)?://(?:www\\.)?ustream\\.tv/.+$", + "^http(?:s)?://(?:www\\.)?ustream\\.com/.+$", + "^http://ustre\\.am/.+$" + ], + "http://wordpress.tv/oembed/": [ + "^http://wordpress\\.tv/.+$" + ], + "http://polldaddy.com/oembed/": [ + "^http(?:s)?://(?:[-\\w]+\\.)?polldaddy\\.com/.+$" + ], + "http://api.bambuser.com/oembed.{format}": [ + "^http://bambuser\\.com/channel/[^#?/]+/broadcast/.+$", + "^http://bambuser\\.com/channel/.+$", + "^http://bambuser\\.com/v/.+$" + ], + "http://www.ted.com/talks/oembed.{format}": [ + "^http(?:s)?://(?:www\\.)?ted\\.com/talks/.+$", + "^http(?:s)?://(?:www\\.)?ted\\.com/talks/lang/[^#?/]+/.+$", + "^http(?:s)?://(?:www\\.)?ted\\.com/index\\.php/talks/.+$", + "^http(?:s)?://(?:www\\.)?ted\\.com/index\\.php/talks/lang/[^#?/]+/.+$" + ], + "http://chirb.it/oembed.{format}": [ + "^http://chirb\\.it/.+$" + ], + "https://www.circuitlab.com/circuit/oembed/": [ + "^http(?:s)?://(?:www\\.)?circuitlab\\.com/circuit/.+$" + ], + "http://api.geograph.org.uk/api/oembed": [ + "^http://(?:[-\\w]+\\.)?geograph\\.org\\.uk/.+$", + "^http://(?:[-\\w]+\\.)?geograph\\.co\\.uk/.+$", + "^http://(?:[-\\w]+\\.)?geograph\\.ie/.+$" + ], + "http://geo.hlipp.de/restapi.php/api/oembed": [ + "^http://geo-en\\.hlipp\\.de/.+$", + "^http://geo\\.hlipp\\.de/.+$", + "^http://germany\\.geograph\\.org/.+$" + ], + "http://www.geograph.org.gg/api/oembed": [ + "^http://(?:[-\\w]+\\.)?geograph\\.org\\.gg/.+$", + "^http://(?:[-\\w]+\\.)?geograph\\.org\\.je/.+$", + "^http://channel-islands\\.geograph\\.org/.+$", + "^http://channel-islands\\.geographs\\.org/.+$", + "^http://(?:[-\\w]+\\.)?channel\\.geographs\\.org/.+$" + ], + "http://vzaar.com/api/videos/{1}.{format}": [ + "^http://(?:www\\.)?vzaar\\.com/videos/([^#?/]+)(?:.+)?$", + "^http://www\\.vzaar\\.tv/([^#?/]+)(?:.+)?$", + "^http://vzaar\\.tv/([^#?/]+)(?:.+)?$", + "^http://vzaar\\.me/([^#?/]+)(?:.+)?$", + "^http://[-\\w]+\\.vzaar\\.me/([^#?/]+)(?:.+)?$" + ], + "http://api.minoto-video.com/services/oembed.{format}": [ + "^http://api\\.minoto-video\\.com/publishers/[^#?/]+/videos/.+$", + "^http://dashboard\\.minoto-video\\.com/main/video/details/.+$", + "^http://embed\\.minoto-video\\.com/.+$" + ], + "http://www.videojug.com/oembed.{format}": [ + "^http(?:s)?://(?:[-\\w]+\\.)?videojug\\.com/film/.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?videojug\\.com/payer/.+$", + "^http(?:s)?://(?:[-\\w]+\\.)?videojug\\.com/interview/.+$" + ], + "http://videos.sapo.pt/oembed": [ + "^http(?:s)?://videos\\.sapo\\.pt/.+$" + ], + "http://vhx.tv/services/oembed.{format}": [ + "^http(?:s)?://(?:www\\.)?vhx\\.tv/.+$" + ], + "http://api.justin.tv/api/embed/from_url.{format}": [ + "^http(?:s)?://(?:www\\.)?justin\\.tv/.+$" + ], + "http://official.fm/services/oembed.{format}": [ + "^http(?:s)?://official\\.fm/.+$" + ], + "http://huffduffer.com/oembed": [ + "^http(?:s)?://(?:www\\.)?huffduffer\\.com/[^#?/]+/.+$" + ], + "https://embed.spotify.com/oembed/": [ + "^http(?:s)?://open\\.spotify\\.com/.+$", + "^http(?:s)?://spoti\\.fi/.+$" + ], + "http://shoudio.com/api/oembed": [ + "^http://shoudio\\.com/.+$", + "^http://shoud\\.io/.+$" + ], + "http://api.mobypicture.com/oEmbed": [ + "^http(?:s)?://(?:www\\.)?mobypicture\\.com/user/[^#?/]+/view/.+$", + "^http(?:s)?://(?:www\\.)?moby\\.to/.+$" + ], + "http://www.23hq.com/23/oembed": [ + "^http(?:s)?://(?:www\\.)?23hq\\.com/[^#?/]+/photo/.+$" + ], + "http://gmep.org/oembed.{format}": [ + "^http(?:s)?://(?:www\\.)?gmep\\.org/.+$", + "^http(?:s)?://gmep\\.imeducate\\.com/.+$" + ], + "http://oembed.urtak.com/1/oembed": [ + "^http(?:s)?://(?:[-\\w]+\\.)?urtak\\.com/.+$" + ], + "http://cacoo.com/oembed.{format}": [ + "^http(?:s)?://cacoo\\.com/.+$" + ], + "http://api.dailymile.com/oembed": [ + "^http(?:s)?://(?:www\\.)?dailymile\\.com/people/[^#?/]+/entries/.+$" + ], + "http://www.dipity.com/oembed/timeline/": [ + "^http(?:s)?://(?:www\\.)?dipity\\.com/timeline/.+$", + "^http(?:s)?://(?:www\\.)?dipity\\.com/voaweb/.+$" + ], + "https://sketchfab.com/oembed": [ + "^http(?:s)?://sketchfab\\.com/show/.+$" + ], + "https://api.meetup.com/oembed": [ + "^http(?:s)?://(?:www\\.)?meetup\\.com/.+$", + "^http(?:s)?://(?:www\\.)?meetup\\.ps/.+$" + ], + "https://roomshare.jp/oembed.{format}": [ + "^http(?:s)?://(?:www\\.)?roomshare\\.jp/(?:en/)?post/.+$" + ], + "http://crowdranking.com/api/oembed.{format}": [ + "^http(?:s)?://crowdranking\\.com/crowdrankings/.+$", + "^http(?:s)?://crowdranking\\.com/rankings/.+$", + "^http(?:s)?://crowdranking\\.com/topics/.+$", + "^http(?:s)?://crowdranking\\.com/widgets/.+$", + "^http(?:s)?://crowdranking\\.com/r/.+$" + ], + "http://openapi.etsy.com/svc/oembed/": [ + "^http(?:s)?://(?:www\\.)?etsy\\.com/listing/.+$" + ], + "https://audioboo.fm/publishing/oembed.{format}": [ + "^http(?:s)?://audioboo\\.fm/boos/.+$" + ], + "http://demo.clikthrough.com/services/oembed/": [ + "^http(?:s)?://demo\\.clikthrough\\.com/theater/video/.+$" + ], + "http://www.ifttt.com/oembed/": [ + "^http(?:s)?://ifttt\\.com/recipes/.+$" + ] +} \ No newline at end of file diff --git a/wagtail/wagtailembeds/embeds/oembed_api.py b/wagtail/wagtailembeds/embeds/oembed_api.py new file mode 100644 index 000000000..8f7945577 --- /dev/null +++ b/wagtail/wagtailembeds/embeds/oembed_api.py @@ -0,0 +1,52 @@ +import os, re +import urllib2, urllib +from datetime import datetime +import json + +class NotImplementedOembedException(Exception): + pass + +ENDPOINTS = {} + +def get_embed_oembed(url, max_width=None): + provider = None + for endpoint in ENDPOINTS.keys(): + for pattern in ENDPOINTS[endpoint]: + if re.match(pattern, url): + provider = endpoint + break + if not provider: + raise NotImplementedOembedException + params = {'url': url, 'format': 'json', } + if max_width: + params['maxwidth'] = max_width + req = provider+'?' +urllib.urlencode(params) + request = urllib2.Request(req) + opener = urllib2.build_opener() + # Some provicers were not working without a user agent + request.add_header('User-Agent','Mozilla/5.0') + return json.loads(opener.open(request).read()) + + +# Uses the public domain collection of oembed endpoints by Mathias Panzenbpeck (panzi) +# at https://github.com/panzi/oembedendpoints/blob/master/endpoints-regexp.json + +def load_oembed_endpoints(): + module_dir = os.path.dirname(__file__) + endpoints_path = os.path.join(module_dir, 'endpoints.json') + with open( endpoints_path) as f: + endpoints = json.loads(f.read()) + + for endpoint in endpoints.keys(): + endpoint_key = endpoint.replace('{format}', 'json') + + ENDPOINTS[endpoint_key]=[] + for pattern in endpoints[endpoint]: + ENDPOINTS[endpoint_key].append(re.compile(pattern)) + + + +load_oembed_endpoints() + + + \ No newline at end of file diff --git a/wagtail/wagtailembeds/embeds/unittests.py b/wagtail/wagtailembeds/embeds/unittests.py new file mode 100644 index 000000000..4c7653b99 --- /dev/null +++ b/wagtail/wagtailembeds/embeds/unittests.py @@ -0,0 +1,66 @@ +import unittest +import oembed + +# Test that a bunch of oembed examples is working +# If any of these is removed or changed then the unit test will fail +# This is a unittest TestCase (and not a django.test one) since django +# database is not actually needed for these tests + +TEST_DATA = [ + { + 'url':'http://www.youtube.com/watch?v=S3xAeTmsJfg', + 'title':'Animation: Ferret dance (A series of tubes)' + }, + { + 'url':'http://vimeo.com/86036070', + 'title':'Wagtail: A new Django CMS' + }, + { + 'url':'https://speakerdeck.com/harmstyler/an-introduction-to-django', + 'title':'An Introduction to Django' + }, + { + 'url':'https://ifttt.com/recipes/144705-new-twitter-followers-in-a-google-spreadsheet', + 'title':'New Twitter followers in a Google spreadsheet' + }, + { + 'url':'http://www.hulu.com/watch/20807/late-night-with-conan-obrien-wed-may-21-2008', + 'title':'Wed, May 21, 2008 (Late Night With Conan O\'Brien)' + }, + { + 'url':'http://www.flickr.com/photos/dfluke/5995957175/', + 'title':'Django pony!?' + }, + { + 'url':'http://www.slideshare.net/simon/the-django-web-application-framework', + 'title':'The Django Web Application Framework' + }, + { + 'url':'http://www.rdio.com/artist/The_Black_Keys/album/Brothers/', + 'title':'Brothers' + }, + { + 'url':'http://instagram.com/p/kFKCcEKmBq/', + 'title':'Family holidays in #Greece!' + }, + { + 'url':'https://www.kickstarter.com/projects/noujaimfilms/the-square-a-film-about-the-egyptian-revolution', + 'title':'Sundance Award Winning Film on the Egyptian Revolution' + }, + { + 'url':'http://www.dailymotion.com/video/xoxulz_babysitter_animals', + 'title':'Babysitter!' + } +] + +class TestEmbeds(unittest.TestCase): + def test_get_embed_oembed(self): + for td in TEST_DATA: + embed = oembed.get_embed_oembed_low(td['url']) + self.assertEqual(embed['title'], td['title'] ) + self.assertIsNotNone(embed['type'] ) + self.assertIsNotNone(embed['width'] ) + self.assertIsNotNone(embed['height'] ) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/wagtail/wagtailembeds/format.py b/wagtail/wagtailembeds/format.py index 3c050a83d..6fac83bb3 100644 --- a/wagtail/wagtailembeds/format.py +++ b/wagtail/wagtailembeds/format.py @@ -2,21 +2,24 @@ from __future__ import division # Use true division from django.utils.html import escape -from .embeds import get_embed +from .embeds.embed import get_embed def embed_to_frontend_html(url): - embed = get_embed(url) - if embed is not None: - # Work out ratio - if embed.width and embed.height: - ratio = str(embed.height / embed.width * 100) + "%" - else: - ratio = "0" + try: + embed = get_embed(url) + if embed is not None: + # Work out ratio + if embed.width and embed.height: + ratio = str(embed.height / embed.width * 100) + "%" + else: + ratio = "0" - # Build html - return '
%s
' % (ratio, embed.html) - else: + # Build html + return '
%s
' % (ratio, embed.html) + else: + return '' + except: return '' diff --git a/wagtail/wagtailembeds/templatetags/embed_filters.py b/wagtail/wagtailembeds/templatetags/embed_filters.py index e526bb9b6..dac91cda0 100644 --- a/wagtail/wagtailembeds/templatetags/embed_filters.py +++ b/wagtail/wagtailembeds/templatetags/embed_filters.py @@ -1,7 +1,7 @@ from django import template from django.utils.safestring import mark_safe -from wagtail.wagtailembeds.embeds import get_embed +from wagtail.wagtailembeds.embed.embeds import get_embed register = template.Library() @@ -10,9 +10,12 @@ register = template.Library() @register.filter def embed(url, max_width=None): embed = get_embed(url, max_width=max_width) - if embed is not None: - return mark_safe(embed.html) - else: + try: + if embed is not None: + return mark_safe(embed.html) + else: + return '' + except: return '' diff --git a/wagtail/wagtailembeds/tests.py b/wagtail/wagtailembeds/tests.py index 5cb8fb874..2a613cf74 100644 --- a/wagtail/wagtailembeds/tests.py +++ b/wagtail/wagtailembeds/tests.py @@ -1,4 +1,4 @@ -from django.test import TestCase +from django.test import TestCasez from .embeds import get_embed diff --git a/wagtail/wagtailembeds/views/chooser.py b/wagtail/wagtailembeds/views/chooser.py index 13a835acb..94695f1a3 100644 --- a/wagtail/wagtailembeds/views/chooser.py +++ b/wagtail/wagtailembeds/views/chooser.py @@ -5,6 +5,10 @@ from wagtail.wagtailadmin.modal_workflow import render_modal_workflow from wagtail.wagtailembeds.forms import EmbedForm from wagtail.wagtailembeds.format import embed_to_editor_html +from wagtail.wagtailembeds.embeds.oembed_api import NotImplementedOembedException +from wagtail.wagtailembeds.embeds.embed import EmbedlyException, AccessDeniedEmbedlyException, NotFoundEmbedlyException + + def chooser(request): form = EmbedForm() @@ -19,18 +23,27 @@ def chooser_upload(request): form = EmbedForm(request.POST, request.FILES) if form.is_valid(): - embed_html = embed_to_editor_html(form.cleaned_data['url']) - if embed_html != "": + try: + embed_html = embed_to_editor_html(form.cleaned_data['url']) return render_modal_workflow( request, None, 'wagtailembeds/chooser/embed_chosen.js', {'embed_html': embed_html} ) - else: + except Exception as e : + #print e + #import traceback + #traceback.print_exc() errors = form._errors.setdefault('url', ErrorList()) - if not hasattr(settings, 'EMBEDLY_KEY'): - errors.append('Please set EMBEDLY_KEY in your settings') + if type(e) == NotImplementedOembedException: + errors.append("This URL is not supported by an oembed provider. You may try embedding it using Embedly by setting a propery EMBEDLY_KEY in your settings.") + elif type(e) == AccessDeniedEmbedlyException: + errors.append("There seems to be a problem with your embedly API key. Please check your settings.") + elif type(e) == NotFoundEmbedlyException: + errors.append("The URL you are trying to embed cannot be found.") + elif type(e) == EmbedlyException: + errors.append("There seems to be an error with Embedly while trying to embed this URL. Please try again later.") else: - errors.append('This URL is not recognised') + errors.append(str(e) ) return render_modal_workflow(request, 'wagtailembeds/chooser/chooser.html', 'wagtailembeds/chooser/chooser.js', { 'form': form, }) From ead0ebeb4eff5ee2056f4c75114a14432861bc00 Mon Sep 17 00:00:00 2001 From: Serafeim Papastefanos Date: Thu, 13 Feb 2014 04:09:18 +0200 Subject: [PATCH 3/5] Remove embedly from setup.pu requirements --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index e5cd3d5f5..09994f7e3 100644 --- a/setup.py +++ b/setup.py @@ -45,7 +45,6 @@ setup( "django-modelcluster>=0.1", "elasticutils>=0.8.2", "pyelasticsearch>=0.6.1", - "Embedly>=0.5.0", "django-taggit==0.10", "Pillow>=2.3.0", "beautifulsoup4>=4.3.2", From a51e609f3f4d261379bce40b793cdc7b9e59aac7 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Thu, 13 Feb 2014 11:41:56 +0000 Subject: [PATCH 4/5] Update README.rst with feature list --- README.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index f4b54f428..0a6350570 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,19 @@ Wagtail CMS =========== -Wagtail is a Django content management system focused on flexibility and user experience. Find out more at `wagtail.io `_ -and `torchbox.github.io/wagtail `_. +Wagtail is a Django content management system built originally for the `Royal College of Art `_ and focused on flexibility and user experience. Its features include: + +* A fast, attractive editor interface +* Complete control over design with standard Django templates +* Configure content types through standard Django models +* Tightly integrated search (with an `Elasticsearch `_ backend for production) +* Strong document and image management +* Wide support for embedded content +* Optional preview->submit->approve workflow +* Fast out of the box. `Varnish `_-friendly if you need it +* Tests! But not enough; we're working hard to improve this + +Find out more at `wagtail.io `_. Getting started ~~~~~~~~~~~~~~~ @@ -10,4 +21,4 @@ To get you up and running quickly, we've provided a demonstration site with all Contributing ~~~~~~~~~~~~ -If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt `_. +If you're a Python or Django developer, fork the repo and get stuck in! Send us a useful pull request and we'll post you a `t-shirt `_. Our immediate priorities are better docs, more tests, internationalisation and localisation. From 7d4696c83ae3c50f2f90e604444cf2535fba0be1 Mon Sep 17 00:00:00 2001 From: Tom Dyson Date: Thu, 13 Feb 2014 12:08:43 +0000 Subject: [PATCH 5/5] Tree support in feature list. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 0a6350570..49231a78c 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,8 @@ Wagtail is a Django content management system built originally for the `Royal Co * Tightly integrated search (with an `Elasticsearch `_ backend for production) * Strong document and image management * Wide support for embedded content +* Simple, configurable permissions +* Support for tree-based content organisation * Optional preview->submit->approve workflow * Fast out of the box. `Varnish `_-friendly if you need it * Tests! But not enough; we're working hard to improve this