├── .gitignore ├── CONTRIBUTORS.txt ├── INSTALL.txt ├── LICENSE.txt ├── MANIFEST.in ├── PKG-INFO ├── README.rst ├── doc ├── Makefile ├── _build │ └── html │ │ ├── .buildinfo │ │ ├── _sources │ │ ├── django-oembed │ │ │ ├── getting_started.txt │ │ │ ├── oembed_resources.txt │ │ │ ├── providing_resources.txt │ │ │ └── spec.txt │ │ ├── djangoembed │ │ │ ├── getting_started.txt │ │ │ ├── oembed_resources.txt │ │ │ ├── providing_resources.txt │ │ │ └── spec.txt │ │ └── index.txt │ │ ├── _static │ │ ├── basic.css │ │ ├── default.css │ │ ├── doctools.js │ │ ├── jquery.js │ │ ├── pygments.css │ │ └── searchtools.js │ │ ├── django-oembed │ │ ├── getting_started.html │ │ ├── oembed_resources.html │ │ ├── providing_resources.html │ │ └── spec.html │ │ ├── djangoembed │ │ ├── getting_started.html │ │ ├── oembed_resources.html │ │ ├── providing_resources.html │ │ └── spec.html │ │ ├── genindex.html │ │ ├── index.html │ │ ├── objects.inv │ │ ├── search.html │ │ └── searchindex.js ├── conf.py ├── djangoembed │ ├── getting_started.rst │ ├── oembed_resources.rst │ ├── providing_resources.rst │ └── spec.rst └── index.rst ├── oembed ├── __init__.py ├── admin.py ├── constants.py ├── consumer.py ├── contrib │ ├── __init__.py │ ├── models.py │ └── oembed_providers.py ├── exceptions.py ├── fields.py ├── fixtures │ └── initial_data.json ├── image_processors │ ├── __init__.py │ └── pil.py ├── listeners.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── parsers │ ├── __init__.py │ ├── base.py │ ├── html.py │ └── text.py ├── providers.py ├── resources.py ├── sites.py ├── templates │ └── oembed │ │ ├── inline │ │ ├── link.html │ │ ├── photo.html │ │ ├── rich.html │ │ └── video.html │ │ ├── link.html │ │ ├── photo.html │ │ ├── rich.html │ │ └── video.html ├── templatetags │ ├── __init__.py │ └── oembed_tags.py ├── tests │ ├── __init__.py │ ├── admin.py │ ├── fixtures │ │ └── oembed_testdata.json │ ├── models.py │ ├── oembed_providers.py │ ├── settings.py │ ├── storage.py │ ├── templates │ │ ├── 404.html │ │ └── oembed │ │ │ ├── inline │ │ │ └── video.html │ │ │ └── provider │ │ │ └── tests_rich.html │ ├── tests │ │ ├── __init__.py │ │ ├── base.py │ │ ├── consumer.py │ │ ├── models.py │ │ ├── parsers.py │ │ ├── providers.py │ │ ├── resources.py │ │ ├── sites.py │ │ ├── templatetags.py │ │ ├── utils.py │ │ └── views.py │ └── urls.py ├── urls.py ├── utils.py └── views.py ├── pip_requirements.txt ├── runtests.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | doctrees/ 3 | build/ 4 | src/ 5 | pip-log.txt 6 | *DS_Store 7 | *~ 8 | dist/ 9 | *.egg-info/ 10 | oembed/tests/media/ -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Charles Leifer 2 | Eric Florenzano 3 | Idan Gazit 4 | Christian Metts 5 | Cody Soyland 6 | -------------------------------------------------------------------------------- /INSTALL.txt: -------------------------------------------------------------------------------- 1 | Thanks for downloading oembed. 2 | 3 | To install it, run the following command inside this directory: 4 | 5 | python setup.py install 6 | 7 | Or if you'd prefer you can simply place the included ``oembed`` 8 | directory somewhere on your Python path, or symlink to it from 9 | somewhere on your Python path; this is useful if you're working from a 10 | git clone. 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Charles Leifer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CONTRIBUTORS.txt 2 | include INSTALL.txt 3 | include LICENSE.txt 4 | include MANIFEST.in 5 | include README.rst 6 | recursive-include doc * 7 | recursive-include oembed/templates * 8 | recursive-include oembed/fixtures *.json 9 | recursive-include oembed/tests/templates * 10 | recursive-include oembed/tests/fixtures *.json 11 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: oembed 3 | Version: 0.1 4 | Summary: OEmbed provider/consumer for django 5 | Home-page: http://www.charlesleifer.com/blog/ 6 | Author: Charles Leifer 7 | Author-email: coleifer@gmail.com 8 | License: UNKNOWN 9 | Description: UNKNOWN 10 | Platform: UNKNOWN 11 | Classifier: Development Status :: 4 - Beta 12 | Classifier: Environment :: Web Environment 13 | Classifier: Framework :: Django 14 | Classifier: Intended Audience :: Developers 15 | Classifier: License :: OSI Approved :: BSD License 16 | Classifier: Operating System :: OS Independent 17 | Classifier: Programming Language :: Python 18 | Classifier: Topic :: Utilities 19 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **THIS PROJECT IS UNMAINTAINED** 2 | 3 | Please use `micawber `_ -- it has much 4 | of the same functionality along with many improvements. 5 | 6 | 7 | Getting Started with OEmbed 8 | =========================== 9 | 10 | Installation 11 | ------------ 12 | 13 | First, you need to install OEmbed. It is available at http://github.com/worldcompany/djangoembed/ 14 | 15 | :: 16 | 17 | git clone git://github.com/worldcompany/djangoembed/ 18 | cd djangoembed 19 | python setup.py install 20 | 21 | Adding to your Django Project 22 | -------------------------------- 23 | 24 | After installing, adding OEmbed consumption to your projects is a snap. First, 25 | add it to your projects' INSTALLED_APPs and run 'syncdb':: 26 | 27 | # settings.py 28 | INSTALLED_APPS = [ 29 | ... 30 | 'oembed' 31 | ] 32 | 33 | djangoembed uses a registration pattern like the admin's. In order to be 34 | sure all apps have been loaded, djangoembed should run autodiscover() in the 35 | urls.py. If you like, you can place this code right below your admin.autodiscover() 36 | bits:: 37 | 38 | # urls.py 39 | import oembed 40 | oembed.autodiscover() 41 | 42 | Consuming Resources 43 | ------------------- 44 | 45 | Now you're ready to start consuming OEmbed-able objects. There are a couple of 46 | options depending on what you want to do. The most straightforward way to get 47 | up-and-running is to add it to your templates:: 48 | 49 | {% load oembed_tags %} 50 | 51 | {% oembed %}blog.content{% endoembed %} 52 | 53 | {# or use the filter #} 54 | 55 | {{ blog.content|oembed }} 56 | 57 | {# maybe you're working with some dimensional constraints #} 58 | 59 | {% oembed "600x600" %}blog.content{% endoembed %} 60 | 61 | {{ blog.content|oembed:"600x600" }} 62 | 63 | You can consume oembed objects in python as well:: 64 | 65 | import oembed 66 | oembed.autodiscover() 67 | 68 | # just get the metadata 69 | resource = oembed.site.embed('http://www.youtube.com/watch?v=nda_OSWeyn8') 70 | resource.get_data() 71 | 72 | {u'author_name': u'botmib', 73 | u'author_url': u'http://www.youtube.com/user/botmib', 74 | u'height': 313, 75 | u'html': u'', 76 | u'provider_name': u'YouTube', 77 | u'provider_url': u'http://www.youtube.com/', 78 | u'title': u'Leprechaun in Mobile, Alabama', 79 | u'type': u'video', 80 | u'version': u'1.0', 81 | u'width': 384} 82 | 83 | # get the metadata and run it through a template for pretty presentation 84 | from oembed.consumer import OEmbedConsumer 85 | client = OEmbedConsumer() 86 | embedded = client.parse_text("http://www.youtube.com/watch?v=nda_OSWeyn8") 87 | 88 |
89 | 90 | 91 | 92 | 93 | 99 | 100 | 101 |

102 | Leprechaun in Mobile, Alabama 103 | by 104 | botmib 105 |

106 |
' 107 | 108 | Troubleshooting 109 | --------------- 110 | 111 | Problem: You try the youtube embed example, but all you get is a link to the youtube video. 112 | 113 | Solution: Djangoembed uses fixtures to load data about oembed providors like Youtube in to the database. Try fooling around with syncdb (or migrations, if you're running South) until there are objects of type oembed.storedprovider. 114 | 115 | If you have another problem, consider looking through the more extensive docs in the project's doc subdirectory. 116 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in _build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in _build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in _build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in _build/qthelp, like this:" 63 | @echo "# qcollectiongenerator _build/qthelp/django-oembed.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile _build/qthelp/django-oembed.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in _build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 76 | @echo 77 | @echo "The overview file is in _build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in _build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in _build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /doc/_build/html/.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: b2b992112dd0a0bef161028630de002c 4 | tags: fbb0d17656682115ca4d033fb2f83ba1 5 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/django-oembed/getting_started.txt: -------------------------------------------------------------------------------- 1 | Getting Started with OEmbed 2 | =========================== 3 | 4 | Installation 5 | ------------ 6 | 7 | First, you need to install OEmbed. It is available at http://www.github.com/worldcompany/django-oembed/ 8 | 9 | :: 10 | 11 | git clone git://github.com/worldcompany/django-oembed/ 12 | cd django-oembed 13 | python setup.py install 14 | 15 | Adding to your Django Project 16 | -------------------------------- 17 | 18 | After installing, adding OEmbed consumption to your projects is a snap. First, 19 | add it to your projects' INSTALLED_APPs and run 'syncdb':: 20 | 21 | # settings.py 22 | INSTALLED_APPS = [ 23 | ... 24 | 'oembed' 25 | ] 26 | 27 | django-oembed uses a registration pattern like the admin's. In order to be 28 | sure all apps have been loaded, django-oembed should run autodiscover() in the 29 | urls.py. If you like, you can place this code right below your admin.autodiscover() 30 | bits:: 31 | 32 | # urls.py 33 | import oembed 34 | oembed.autodiscover() 35 | 36 | Consuming Resources 37 | ------------------- 38 | 39 | Now you're ready to start consuming OEmbed-able objects. There are a couple of 40 | options depending on what you want to do. The most straightforward way to get 41 | up-and-running is to add it to your templates:: 42 | 43 | {% load oembed_tags %} 44 | 45 | {% oembed %}blog.content{% endoembed %} 46 | 47 | {# or use the filter #} 48 | 49 | {{ blog.content|oembed }} 50 | 51 | {# maybe you're working with some dimensional constraints #} 52 | 53 | {% oembed "600x600" %}blog.content{% endoembed %} 54 | 55 | {{ blog.content|oembed:"600x600" }} 56 | 57 | You can consume oembed objects in python as well:: 58 | 59 | import oembed 60 | oembed.autodiscover() 61 | 62 | # just get the metadata 63 | resource = oembed.site.embed('http://www.youtube.com/watch?v=nda_OSWeyn8') 64 | resource.get_data() 65 | 66 | {u'author_name': u'botmib', 67 | u'author_url': u'http://www.youtube.com/user/botmib', 68 | u'height': 313, 69 | u'html': u'', 70 | u'provider_name': u'YouTube', 71 | u'provider_url': u'http://www.youtube.com/', 72 | u'title': u'Leprechaun in Mobile, Alabama', 73 | u'type': u'video', 74 | u'version': u'1.0', 75 | u'width': 384} 76 | 77 | # get the metadata and run it through a template for pretty presentation 78 | from oembed.consumer import OEmbedConsumer 79 | client = OEmbedConsumer() 80 | embedded = client.parse_text("http://www.youtube.com/watch?v=nda_OSWeyn8") 81 | 82 |
83 | 84 | 85 | 86 | 87 | 93 | 94 | 95 |

96 | Leprechaun in Mobile, Alabama 97 | by 98 | botmib 99 |

100 |
' 101 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/django-oembed/oembed_resources.txt: -------------------------------------------------------------------------------- 1 | OEmbed Resources 2 | ================ 3 | 4 | Resources are the objects returned by an OEmbed endpoint. The OEmbed spec 5 | defines 4 resource types: 6 | 7 | * Video 8 | * Photo 9 | * Rich 10 | * Link 11 | 12 | 13 | Basic Metadata 14 | -------------- 15 | 16 | Regardless of which resource type you are dealing with, the same metadata may be 17 | provided: 18 | 19 | * type (required) 20 | The resource type. Valid values, along with value-specific parameters, are described below. 21 | * version (required) 22 | The oEmbed version number. This must be 1.0. 23 | * title (optional) 24 | A text title, describing the resource. 25 | * author_name (optional) 26 | The name of the author/owner of the resource. 27 | * author_url (optional) 28 | A URL for the author/owner of the resource. 29 | * provider_name (optional) 30 | The name of the resource provider. 31 | * provider_url (optional) 32 | The url of the resource provider. 33 | * cache_age (optional) 34 | The suggested cache lifetime for this resource, in seconds. Consumers may choose to use this value or not. 35 | * thumbnail_url (optional) 36 | A URL to a thumbnail image representing the resource. The thumbnail must respect any maxwidth and maxheight parameters. If this paramater is present, thumbnail_width and thumbnail_height must also be present. 37 | * thumbnail_width (optional) 38 | The width of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_height must also be present. 39 | * thumbnail_height (optional) 40 | The height of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_width must also be present. 41 | 42 | 43 | Video Resources 44 | --------------- 45 | 46 | Video resources are embeddable video players, and are returned by providers like 47 | YouTube and Vimeo. Every video resource **must** provide the following 48 | metadata: 49 | 50 | * html (required) 51 | The HTML required to embed a video player. The HTML should have no padding 52 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 53 | avoid XSS vulnerabilities. 54 | * width (required) 55 | The width in pixels required to display the HTML. 56 | * height (required) 57 | The height in pixels required to display the HTML. 58 | 59 | Responses of this type must obey the maxwidth and maxheight request parameters. 60 | If a provider wishes the consumer to just provide a thumbnail, rather than an 61 | embeddable player, they should instead return a photo response type. 62 | 63 | 64 | Photo Resources 65 | --------------- 66 | 67 | Photo resources are static photos. The following parameters are defined: 68 | 69 | * url (required) 70 | The source URL of the image. Consumers should be able to insert this URL 71 | into an element. Only HTTP and HTTPS URLs are valid. 72 | * width (required) 73 | The width in pixels of the image specified in the url parameter. 74 | * height (required) 75 | The height in pixels of the image specified in the url parameter. 76 | 77 | Responses of this type must obey the maxwidth and maxheight request parameters. 78 | 79 | 80 | Rich Resources 81 | -------------- 82 | 83 | This type is used for rich HTML content that does not fall under one of the 84 | other categories. The following parameters are defined: 85 | 86 | * html (required) 87 | The HTML required to display the resource. The HTML should have no padding 88 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 89 | avoid XSS vulnerabilities. The markup should be valid XHTML 1.0 Basic. 90 | * width (required) 91 | The width in pixels required to display the HTML. 92 | * height (required) 93 | The height in pixels required to display the HTML. 94 | 95 | Responses of this type must obey the maxwidth and maxheight request parameters. 96 | 97 | 98 | Link Resources 99 | -------------- 100 | 101 | Responses of this type allow a provider to return any generic embed data 102 | (such as title and author_name), without providing either the url or html 103 | parameters. The consumer may then link to the resource, using the URL specified 104 | in the original request. 105 | 106 | 107 | Resource Validation 108 | ------------------- 109 | 110 | django-oembed validates resources for you! 111 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/django-oembed/providing_resources.txt: -------------------------------------------------------------------------------- 1 | Providing Resources 2 | =================== 3 | 4 | Suppose you would like your own content to be OEmbed-able. Making your own site 5 | a provider also allows you to consume your own content in a uniform fashion. 6 | Say you have a blog app and occasionally link to your own articles. By defining 7 | a provider for your blog entries, you can consume them transparently! 8 | 9 | django-oembed comes with base classes you can extend to write your own providers 10 | depending on what you want to embed. 11 | 12 | BaseProvider 13 | ------------ 14 | 15 | Taking a look at BaseProvider, we see that it defines two attributes and one 16 | method:: 17 | 18 | class BaseProvider(object): 19 | """ 20 | Base class for OEmbed resources. 21 | """ 22 | regex_list = [] # list of regexes to match 23 | provides = True # allow this provider to be accessed by third parties 24 | 25 | def endpoint(self, url, **kwargs): 26 | """ 27 | Get an OEmbedResource from one of the providers configured in this 28 | provider according to the resource url. 29 | 30 | Args: 31 | url: The url of the resource to get. 32 | **kwargs: Optional parameters to pass in to the provider. 33 | 34 | Returns: 35 | OEmbedResource object. 36 | 37 | If no object returned, raises OEmbedException 38 | """ 39 | raise NotImplementedError 40 | 41 | When the consumer processes a URL, it checks to see if it's URL matches any of 42 | the regexes provided by registered OEmbedProviders. When a match is found, that 43 | provider's ``endpoint()`` method is called, with the URL and any arguments 44 | passed in, such as 'format', 'maxwidth', or 'maxheight'. The ``endpoint()`` 45 | method returns a valid OEmbedResource or raises an ``OEmbedException``. 46 | 47 | 48 | HTTPProvider 49 | ------------ 50 | 51 | Many popular content sites already provide OEmbed endpoints. The ``HTTPProvider`` 52 | acts as a proxy layer that handles fetching resources and validating them. 53 | 54 | Looking in ``oembed_providers.py``, there are numerous providers already set-up 55 | for you. Here is what the Flickr provider looks like:: 56 | 57 | class FlickrProvider(HTTPProvider): 58 | endpoint_url = 'http://www.flickr.com/services/oembed/' 59 | regex_list = ['http://(?:www\.)?flickr\.com/photos/\S+?/(?:sets/)?\d+/?'] 60 | 61 | 62 | DjangoProvider 63 | -------------- 64 | 65 | The ``DjangoProvider`` class is intended to make writing providers for your 66 | Django models super easy. It knows how to introspect your models for 67 | ImageFields, it can intelligently resize your objects based on user-specified 68 | dimensions, and it allows you to return HTML rendered through your own templates 69 | for rich content like videos. 70 | 71 | Let's look at a basic example you might use for a Blog:: 72 | 73 | # assume the following is our Blog model 74 | from django.db import models 75 | from django.core.urlresolvers import reverse 76 | 77 | class Entry(models.Model): 78 | title = models.CharField(max_length=255) 79 | slug = models.SlugField() 80 | body = models.TextField() 81 | author = models.ForeignKey(User) 82 | published = models.BooleanField() 83 | pub_date = models.DateTimeField(auto_now_add=True) 84 | 85 | def get_absolute_url(self): 86 | return reverse('blog_detail', args=[self.slug]) 87 | 88 | The ``urls.py`` defines a list and detail view:: 89 | 90 | from django.conf.urls.defaults import * 91 | 92 | urlpatterns = patterns('blog.views', 93 | url(r'^(?P[-a-z0-9]+)/$', 94 | view='blog_detail', 95 | name='blog_detail'), 96 | url(r'^$', 97 | view='blog_list', 98 | name='blog_index'), 99 | ) 100 | 101 | Now let's write a provider for it. This lives in an ``oembed_providers.py`` 102 | file in your blog app's directory:: 103 | 104 | import oembed 105 | from blog.models import Entry 106 | from oembed.providers import DjangoProvider 107 | 108 | class EntryProvider(DjangoProvider): 109 | resource_type = 'link' # this is required 110 | model = Entry 111 | named_view = 'blog_detail' 112 | fields_to_match = {'entry_slug': 'slug'} # map url field to model field 113 | 114 | def author_name(self, obj): 115 | return obj.author.username 116 | 117 | def author_url(self, obj): 118 | return obj.author.get_absolute_url() 119 | 120 | def title(self, obj): 121 | return obj.title 122 | 123 | def get_queryset(self): 124 | # this defaults to self.model._default_manager.all(), but we don't 125 | # want to provide unpublished entries, so do a bit of filtering 126 | return Entry.objects.filter(published=True) 127 | 128 | # don't forget to register your provider 129 | oembed.site.register(EntryProvider) 130 | 131 | You should now be able to hit your API endpoint (by default /oembed/json/) with 132 | a published entry URL and get a JSON response! 133 | 134 | One caveat: django provider URLs build their regexes using site domains from the 135 | sites app. If your site is ``http://www.mysite.com`` and you are running locally, 136 | using ``127.0.0.1:8000``, you will want to give your endpoint URLs as they would 137 | appear on your live site, so: 138 | 139 | http://www.mysite.com/blog/this-is-a-great-entry/ 140 | 141 | instead of 142 | 143 | http://127.0.0.1/blog/this-is-a-great-entry/ 144 | 145 | 146 | DjangoDateBasedProvider 147 | ----------------------- 148 | 149 | Oftentimes, your content may live a date-based URL. Writing providers for these 150 | models is simplified by using the ``DjangoDateBasedProvider`` class. Returning 151 | to the Blog example from above, let's assume the detail view looks like this:: 152 | 153 | url(r'^(?P\d{4})/(?P\w{3})/(?P\d{1,2})/(?P[\w-]+)/$', 154 | view='blog_detail', 155 | name='blog_detail'), 156 | 157 | The only modification we will make to our ``EntryProvider`` will be to subclass 158 | the date-based provider class:: 159 | 160 | from oembed.providers import DjangoDateBasedProvider 161 | 162 | class EntryProvider(DjangoDateBasedProvider): 163 | ... 164 | 165 | The date-based provider introspects your model and uses the first DateField or 166 | DateTimeField. If you have multiple fields of this type, you can explicitly 167 | define a date field:: 168 | 169 | from oembed.providers import DjangoDateBasedProvider 170 | 171 | class EntryProvider(DjangoDateBasedProvider): 172 | date_field = 'pub_date' 173 | ... 174 | 175 | 176 | How are images handled? 177 | ----------------------- 178 | 179 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/django-oembed/spec.txt: -------------------------------------------------------------------------------- 1 | The OEmbed Spec 2 | =============== 3 | 4 | The full spec is available at http://www.oembed.com - this overview will touch 5 | on everything without going into too much detail to get you up and running with 6 | OEmbed quickly! 7 | 8 | What is OEmbed? 9 | --------------- 10 | 11 | "oEmbed is a format for allowing an embedded representation of a URL on 12 | third party sites. The simple API allows a website to display embedded 13 | content (such as photos or videos) when a user posts a link to that 14 | resource, without having to parse the resource directly." 15 | 16 | What problem does it solve? 17 | --------------------------- 18 | 19 | One of the tasks we as web developers run into a lot is the need to integrate 20 | rich third-party content into our own sites. Numerous REST APIs exist for this 21 | purpose, but suppose we are only concerned with metadata? 22 | 23 | REST APIs make it difficult to extract metadata in a generic way: 24 | * URL structures vary (/statuses/update.json, /users/show.json) 25 | * attribute names are not standardized 26 | * metadata provided is content-dependant (twitter returns tweets, flickr photos) 27 | * authentication can be a pain 28 | * response formats vary 29 | 30 | OEmbed aims at solving these problems by: 31 | * Endpoint lives at one place, like /oembed/json/ 32 | * attribute names are standard, including 'title', 'author', 'thumbnail_url' 33 | * resource types are standard, being 'video', 'photo', 'link', 'rich' 34 | * response format must be JSON or XML 35 | 36 | OEmbed is not a REST API. It is a *READ* API. It allows you to retrieve 37 | metadata about the objects you're interested in, using a single endpoint. 38 | 39 | The best part? **All you need to provide the endpoint is the URL you want 40 | metadata about**. 41 | 42 | An Example 43 | ---------- 44 | 45 | :: 46 | 47 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/ 48 | 49 | 50 | 51 | 1.0 52 | photo 53 | ZB8T0193 54 | ‮‭‬bees‬ 55 | http://www.flickr.com/photos/bees/ 56 | 3600 57 | Flickr 58 | http://www.flickr.com/ 59 | 500 60 | 333 61 | http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf.jpg 62 | 63 | 64 | In the example above, we pass a flickr photo-detail URL to their OEmbed endpoint. 65 | Flickr then returns a wealth of metadata about the object, including the image's 66 | URL, width and height. 67 | 68 | OEmbed endpoints also can accept other arguments, like a **maxwidth**, or **format**: 69 | 70 | :: 71 | 72 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/\&maxwidth=300\&format=json 73 | 74 | { 75 | "version":"1.0", 76 | "type":"photo", 77 | "title":"ZB8T0193", 78 | "author_name":"\u202e\u202d\u202cbees\u202c", 79 | "author_url":"http:\/\/www.flickr.com\/photos\/bees\/", 80 | "cache_age":3600, 81 | "provider_name":"Flickr", 82 | "provider_url":"http:\/\/www.flickr.com\/", 83 | "width":"240", 84 | "height":"160", 85 | "url":"http:\/\/farm4.static.flickr.com\/3123\/2341623661_7c99f48bbf_m.jpg" 86 | } 87 | 88 | As you can see from the response, the returned image width is now 240. If a 89 | maximum width (or height) is specified, the provider must respect that. 90 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/djangoembed/getting_started.txt: -------------------------------------------------------------------------------- 1 | Getting Started with OEmbed 2 | =========================== 3 | 4 | Installation 5 | ------------ 6 | 7 | First, you need to install OEmbed. It is available at http://github.com/worldcompany/djangoembed/ 8 | 9 | :: 10 | 11 | git clone git://github.com/worldcompany/djangoembed/ 12 | cd djangoembed 13 | python setup.py install 14 | 15 | Adding to your Django Project 16 | -------------------------------- 17 | 18 | After installing, adding OEmbed consumption to your projects is a snap. First, 19 | add it to your projects' INSTALLED_APPs and run 'syncdb':: 20 | 21 | # settings.py 22 | INSTALLED_APPS = [ 23 | ... 24 | 'oembed' 25 | ] 26 | 27 | djangoembed uses a registration pattern like the admin's. In order to be 28 | sure all apps have been loaded, djangoembed should run autodiscover() in the 29 | urls.py. If you like, you can place this code right below your admin.autodiscover() 30 | bits:: 31 | 32 | # urls.py 33 | import oembed 34 | oembed.autodiscover() 35 | 36 | Consuming Resources 37 | ------------------- 38 | 39 | Now you're ready to start consuming OEmbed-able objects. There are a couple of 40 | options depending on what you want to do. The most straightforward way to get 41 | up-and-running is to add it to your templates:: 42 | 43 | {% load oembed_tags %} 44 | 45 | {% oembed %}blog.content{% endoembed %} 46 | 47 | {# or use the filter #} 48 | 49 | {{ blog.content|oembed }} 50 | 51 | {# maybe you're working with some dimensional constraints #} 52 | 53 | {% oembed "600x600" %}blog.content{% endoembed %} 54 | 55 | {{ blog.content|oembed:"600x600" }} 56 | 57 | You can consume oembed objects in python as well:: 58 | 59 | import oembed 60 | oembed.autodiscover() 61 | 62 | # just get the metadata 63 | resource = oembed.site.embed('http://www.youtube.com/watch?v=nda_OSWeyn8') 64 | resource.get_data() 65 | 66 | {u'author_name': u'botmib', 67 | u'author_url': u'http://www.youtube.com/user/botmib', 68 | u'height': 313, 69 | u'html': u'', 70 | u'provider_name': u'YouTube', 71 | u'provider_url': u'http://www.youtube.com/', 72 | u'title': u'Leprechaun in Mobile, Alabama', 73 | u'type': u'video', 74 | u'version': u'1.0', 75 | u'width': 384} 76 | 77 | # get the metadata and run it through a template for pretty presentation 78 | from oembed.consumer import OEmbedConsumer 79 | client = OEmbedConsumer() 80 | embedded = client.parse_text("http://www.youtube.com/watch?v=nda_OSWeyn8") 81 | 82 |
83 | 84 | 85 | 86 | 87 | 93 | 94 | 95 |

96 | Leprechaun in Mobile, Alabama 97 | by 98 | botmib 99 |

100 |
' 101 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/djangoembed/oembed_resources.txt: -------------------------------------------------------------------------------- 1 | OEmbed Resources 2 | ================ 3 | 4 | Resources are the objects returned by an OEmbed endpoint. The OEmbed spec 5 | defines 4 resource types: 6 | 7 | * Video 8 | * Photo 9 | * Rich 10 | * Link 11 | 12 | 13 | Basic Metadata 14 | -------------- 15 | 16 | Regardless of which resource type you are dealing with, the same metadata may be 17 | provided: 18 | 19 | * type (required) 20 | The resource type. Valid values, along with value-specific parameters, are described below. 21 | * version (required) 22 | The oEmbed version number. This must be 1.0. 23 | * title (optional) 24 | A text title, describing the resource. 25 | * author_name (optional) 26 | The name of the author/owner of the resource. 27 | * author_url (optional) 28 | A URL for the author/owner of the resource. 29 | * provider_name (optional) 30 | The name of the resource provider. 31 | * provider_url (optional) 32 | The url of the resource provider. 33 | * cache_age (optional) 34 | The suggested cache lifetime for this resource, in seconds. Consumers may choose to use this value or not. 35 | * thumbnail_url (optional) 36 | A URL to a thumbnail image representing the resource. The thumbnail must respect any maxwidth and maxheight parameters. If this paramater is present, thumbnail_width and thumbnail_height must also be present. 37 | * thumbnail_width (optional) 38 | The width of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_height must also be present. 39 | * thumbnail_height (optional) 40 | The height of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_width must also be present. 41 | 42 | 43 | Video Resources 44 | --------------- 45 | 46 | Video resources are embeddable video players, and are returned by providers like 47 | YouTube and Vimeo. Every video resource **must** provide the following 48 | metadata: 49 | 50 | * html (required) 51 | The HTML required to embed a video player. The HTML should have no padding 52 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 53 | avoid XSS vulnerabilities. 54 | * width (required) 55 | The width in pixels required to display the HTML. 56 | * height (required) 57 | The height in pixels required to display the HTML. 58 | 59 | Responses of this type must obey the maxwidth and maxheight request parameters. 60 | If a provider wishes the consumer to just provide a thumbnail, rather than an 61 | embeddable player, they should instead return a photo response type. 62 | 63 | 64 | Photo Resources 65 | --------------- 66 | 67 | Photo resources are static photos. The following parameters are defined: 68 | 69 | * url (required) 70 | The source URL of the image. Consumers should be able to insert this URL 71 | into an element. Only HTTP and HTTPS URLs are valid. 72 | * width (required) 73 | The width in pixels of the image specified in the url parameter. 74 | * height (required) 75 | The height in pixels of the image specified in the url parameter. 76 | 77 | Responses of this type must obey the maxwidth and maxheight request parameters. 78 | 79 | 80 | Rich Resources 81 | -------------- 82 | 83 | This type is used for rich HTML content that does not fall under one of the 84 | other categories. The following parameters are defined: 85 | 86 | * html (required) 87 | The HTML required to display the resource. The HTML should have no padding 88 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 89 | avoid XSS vulnerabilities. The markup should be valid XHTML 1.0 Basic. 90 | * width (required) 91 | The width in pixels required to display the HTML. 92 | * height (required) 93 | The height in pixels required to display the HTML. 94 | 95 | Responses of this type must obey the maxwidth and maxheight request parameters. 96 | 97 | 98 | Link Resources 99 | -------------- 100 | 101 | Responses of this type allow a provider to return any generic embed data 102 | (such as title and author_name), without providing either the url or html 103 | parameters. The consumer may then link to the resource, using the URL specified 104 | in the original request. 105 | 106 | 107 | Resource Validation 108 | ------------------- 109 | 110 | djangoembed validates resources for you! 111 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/djangoembed/providing_resources.txt: -------------------------------------------------------------------------------- 1 | Providing Resources 2 | =================== 3 | 4 | Suppose you would like your own content to be OEmbed-able. Making your own site 5 | a provider also allows you to consume your own content in a uniform fashion. 6 | Say you have a blog app and occasionally link to your own articles. By defining 7 | a provider for your blog entries, you can consume them transparently! 8 | 9 | django-oembed comes with base classes you can extend to write your own providers 10 | depending on what you want to embed. 11 | 12 | BaseProvider 13 | ------------ 14 | 15 | Taking a look at BaseProvider, we see that it defines two attributes and one 16 | method:: 17 | 18 | class BaseProvider(object): 19 | """ 20 | Base class for OEmbed resources. 21 | """ 22 | regex = r'' 23 | provides = True # allow this provider to be accessed by third parties 24 | 25 | def request_resource(self, url, **kwargs): 26 | """ 27 | Get an OEmbedResource from one of the providers configured in this 28 | provider according to the resource url. 29 | 30 | Args: 31 | url: The url of the resource to get. 32 | **kwargs: Optional parameters to pass in to the provider. 33 | 34 | Returns: 35 | OEmbedResource object. 36 | 37 | If no object returned, raises OEmbedException 38 | """ 39 | raise NotImplementedError 40 | 41 | When the consumer processes a URL, it checks to see if it's URL matches any of 42 | the regexes provided by registered OEmbedProviders. When a match is found, that 43 | provider's ``endpoint()`` method is called, with the URL and any arguments 44 | passed in, such as 'format', 'maxwidth', or 'maxheight'. The ``endpoint()`` 45 | method returns a valid OEmbedResource or raises an ``OEmbedException``. 46 | 47 | 48 | HTTPProvider 49 | ------------ 50 | 51 | Many popular content sites already provide OEmbed endpoints. The ``HTTPProvider`` 52 | acts as a proxy layer that handles fetching resources and validating them. 53 | 54 | Looking in ``oembed_providers.py``, there are numerous providers already set-up 55 | for you. Here is what the Flickr provider looks like:: 56 | 57 | class FlickrProvider(HTTPProvider): 58 | endpoint_url = 'http://www.flickr.com/services/oembed/' 59 | regex = 'http://(?:www\.)?flickr\.com/photos/\S+?/(?:sets/)?\d+/?' 60 | 61 | 62 | DjangoProvider 63 | -------------- 64 | 65 | The ``DjangoProvider`` class is intended to make writing providers for your 66 | Django models super easy. It knows how to introspect your models for 67 | ImageFields, it can intelligently resize your objects based on user-specified 68 | dimensions, and it allows you to return HTML rendered through your own templates 69 | for rich content like videos. 70 | 71 | Let's look at a basic example you might use for a Blog:: 72 | 73 | # assume the following is our Blog model 74 | from django.db import models 75 | from django.core.urlresolvers import reverse 76 | 77 | class Entry(models.Model): 78 | title = models.CharField(max_length=255) 79 | slug = models.SlugField() 80 | body = models.TextField() 81 | author = models.ForeignKey(User) 82 | published = models.BooleanField() 83 | pub_date = models.DateTimeField(auto_now_add=True) 84 | 85 | def get_absolute_url(self): 86 | return reverse('blog_detail', args=[self.slug]) 87 | 88 | The ``urls.py`` defines a list and detail view:: 89 | 90 | from django.conf.urls.defaults import * 91 | 92 | urlpatterns = patterns('blog.views', 93 | url(r'^(?P[-a-z0-9]+)/$', 94 | view='blog_detail', 95 | name='blog_detail'), 96 | url(r'^$', 97 | view='blog_list', 98 | name='blog_index'), 99 | ) 100 | 101 | Now let's write a provider for it. This lives in an ``oembed_providers.py`` 102 | file in your blog app's directory:: 103 | 104 | import oembed 105 | from blog.models import Entry 106 | from oembed.providers import DjangoProvider 107 | 108 | class EntryProvider(DjangoProvider): 109 | resource_type = 'link' # this is required 110 | 111 | class Meta: 112 | queryset = Entry.objects.filter(published=True) 113 | named_view = 'blog_detail' 114 | fields_to_match = {'entry_slug': 'slug'} # map url field to model field 115 | 116 | def author_name(self, obj): 117 | return obj.author.username 118 | 119 | def author_url(self, obj): 120 | return obj.author.get_absolute_url() 121 | 122 | def title(self, obj): 123 | return obj.title 124 | 125 | # don't forget to register your provider 126 | oembed.site.register(EntryProvider) 127 | 128 | You should now be able to hit your API endpoint (by default /oembed/json/) with 129 | a published entry URL and get a JSON response! 130 | 131 | One caveat: django provider URLs build their regexes using site domains from the 132 | sites app. If your site is ``http://www.mysite.com`` and you are running locally, 133 | using ``127.0.0.1:8000``, you will want to give your endpoint URLs as they would 134 | appear on your live site, so: 135 | 136 | http://www.mysite.com/blog/this-is-a-great-entry/ 137 | 138 | instead of 139 | 140 | http://127.0.0.1/blog/this-is-a-great-entry/ 141 | 142 | 143 | DjangoDateBasedProvider 144 | ----------------------- 145 | 146 | Oftentimes, your content may live a date-based URL. Writing providers for these 147 | models is simplified by using the ``DjangoDateBasedProvider`` class. Returning 148 | to the Blog example from above, let's assume the detail view looks like this:: 149 | 150 | url(r'^(?P\d{4})/(?P\w{3})/(?P\d{1,2})/(?P[\w-]+)/$', 151 | view='blog_detail', 152 | name='blog_detail'), 153 | 154 | The only modification we will make to our ``EntryProvider`` will be to subclass 155 | the date-based provider class:: 156 | 157 | from oembed.providers import DjangoDateBasedProvider 158 | 159 | class EntryProvider(DjangoDateBasedProvider): 160 | ... 161 | 162 | The date-based provider introspects your model and uses the first DateField or 163 | DateTimeField. If you have multiple fields of this type, you can explicitly 164 | define a date field:: 165 | 166 | from oembed.providers import DjangoDateBasedProvider 167 | 168 | class EntryProvider(DjangoDateBasedProvider): 169 | ... 170 | class Meta: 171 | ... 172 | date_field = 'pub_date' 173 | 174 | 175 | How are images handled? 176 | ----------------------- 177 | 178 | By default djangoembed uses PIL to resize images within the dimensional 179 | constraints requested. The built-in DjangoProvider has a resize_photo() method 180 | and a thumbnail() method that take as their parameters an object and some 181 | dimensions. These methods call a general-purpose resize() method which 182 | hooks into the image processing backend (by default PIL, but you can write 183 | your own!) and resizes the photo, returning the url of the resized image and 184 | the new dimensions. 185 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/djangoembed/spec.txt: -------------------------------------------------------------------------------- 1 | The OEmbed Spec 2 | =============== 3 | 4 | The full spec is available at http://www.oembed.com - this overview will touch 5 | on everything without going into too much detail to get you up and running with 6 | OEmbed quickly! 7 | 8 | What is OEmbed? 9 | --------------- 10 | 11 | "oEmbed is a format for allowing an embedded representation of a URL on 12 | third party sites. The simple API allows a website to display embedded 13 | content (such as photos or videos) when a user posts a link to that 14 | resource, without having to parse the resource directly." 15 | 16 | What problem does it solve? 17 | --------------------------- 18 | 19 | One of the tasks we as web developers run into a lot is the need to integrate 20 | rich third-party content into our own sites. Numerous REST APIs exist for this 21 | purpose, but suppose we are only concerned with metadata? 22 | 23 | REST APIs make it difficult to extract metadata in a generic way: 24 | * URL structures vary (/statuses/update.json, /users/show.json) 25 | * attribute names are not standardized 26 | * metadata provided is content-dependant (twitter returns tweets, flickr photos) 27 | * authentication can be a pain 28 | * response formats vary 29 | 30 | OEmbed aims at solving these problems by: 31 | * Endpoint lives at one place, like /oembed/json/ 32 | * attribute names are standard, including 'title', 'author', 'thumbnail_url' 33 | * resource types are standard, being 'video', 'photo', 'link', 'rich' 34 | * response format must be JSON or XML 35 | 36 | OEmbed is not a REST API. It is a *READ* API. It allows you to retrieve 37 | metadata about the objects you're interested in, using a single endpoint. 38 | 39 | The best part? **All you need to provide the endpoint is the URL you want 40 | metadata about**. 41 | 42 | An Example 43 | ---------- 44 | 45 | :: 46 | 47 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/ 48 | 49 | 50 | 51 | 1.0 52 | photo 53 | ZB8T0193 54 | ‮‭‬bees‬ 55 | http://www.flickr.com/photos/bees/ 56 | 3600 57 | Flickr 58 | http://www.flickr.com/ 59 | 500 60 | 333 61 | http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf.jpg 62 | 63 | 64 | In the example above, we pass a flickr photo-detail URL to their OEmbed endpoint. 65 | Flickr then returns a wealth of metadata about the object, including the image's 66 | URL, width and height. 67 | 68 | OEmbed endpoints also can accept other arguments, like a **maxwidth**, or **format**: 69 | 70 | :: 71 | 72 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/\&maxwidth=300\&format=json 73 | 74 | { 75 | "version":"1.0", 76 | "type":"photo", 77 | "title":"ZB8T0193", 78 | "author_name":"\u202e\u202d\u202cbees\u202c", 79 | "author_url":"http:\/\/www.flickr.com\/photos\/bees\/", 80 | "cache_age":3600, 81 | "provider_name":"Flickr", 82 | "provider_url":"http:\/\/www.flickr.com\/", 83 | "width":"240", 84 | "height":"160", 85 | "url":"http:\/\/farm4.static.flickr.com\/3123\/2341623661_7c99f48bbf_m.jpg" 86 | } 87 | 88 | As you can see from the response, the returned image width is now 240. If a 89 | maximum width (or height) is specified, the provider must respect that. 90 | -------------------------------------------------------------------------------- /doc/_build/html/_sources/index.txt: -------------------------------------------------------------------------------- 1 | .. djangoembed documentation master file, created by 2 | sphinx-quickstart on Sat Feb 13 14:41:46 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to djangoembed's documentation! 7 | ======================================= 8 | 9 | Why OEmbed? To add rich content to your sites *painlessly* 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 3 15 | :glob: 16 | 17 | djangoembed/* 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /doc/_build/html/_static/default.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Sphinx stylesheet -- default theme 3 | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 4 | */ 5 | 6 | @import url("basic.css"); 7 | 8 | /* -- page layout ----------------------------------------------------------- */ 9 | 10 | body { 11 | font-family: sans-serif; 12 | font-size: 100%; 13 | background-color: #11303d; 14 | color: #000; 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | div.document { 20 | background-color: #1c4e63; 21 | } 22 | 23 | div.documentwrapper { 24 | float: left; 25 | width: 100%; 26 | } 27 | 28 | div.bodywrapper { 29 | margin: 0 0 0 230px; 30 | } 31 | 32 | div.body { 33 | background-color: #ffffff; 34 | color: #000000; 35 | padding: 0 20px 30px 20px; 36 | } 37 | 38 | div.footer { 39 | color: #ffffff; 40 | width: 100%; 41 | padding: 9px 0 9px 0; 42 | text-align: center; 43 | font-size: 75%; 44 | } 45 | 46 | div.footer a { 47 | color: #ffffff; 48 | text-decoration: underline; 49 | } 50 | 51 | div.related { 52 | background-color: #133f52; 53 | line-height: 30px; 54 | color: #ffffff; 55 | } 56 | 57 | div.related a { 58 | color: #ffffff; 59 | } 60 | 61 | div.sphinxsidebar { 62 | } 63 | 64 | div.sphinxsidebar h3 { 65 | font-family: 'Trebuchet MS', sans-serif; 66 | color: #ffffff; 67 | font-size: 1.4em; 68 | font-weight: normal; 69 | margin: 0; 70 | padding: 0; 71 | } 72 | 73 | div.sphinxsidebar h3 a { 74 | color: #ffffff; 75 | } 76 | 77 | div.sphinxsidebar h4 { 78 | font-family: 'Trebuchet MS', sans-serif; 79 | color: #ffffff; 80 | font-size: 1.3em; 81 | font-weight: normal; 82 | margin: 5px 0 0 0; 83 | padding: 0; 84 | } 85 | 86 | div.sphinxsidebar p { 87 | color: #ffffff; 88 | } 89 | 90 | div.sphinxsidebar p.topless { 91 | margin: 5px 10px 10px 10px; 92 | } 93 | 94 | div.sphinxsidebar ul { 95 | margin: 10px; 96 | padding: 0; 97 | color: #ffffff; 98 | } 99 | 100 | div.sphinxsidebar a { 101 | color: #98dbcc; 102 | } 103 | 104 | div.sphinxsidebar input { 105 | border: 1px solid #98dbcc; 106 | font-family: sans-serif; 107 | font-size: 1em; 108 | } 109 | 110 | /* -- body styles ----------------------------------------------------------- */ 111 | 112 | a { 113 | color: #355f7c; 114 | text-decoration: none; 115 | } 116 | 117 | a:hover { 118 | text-decoration: underline; 119 | } 120 | 121 | div.body p, div.body dd, div.body li { 122 | text-align: justify; 123 | line-height: 130%; 124 | } 125 | 126 | div.body h1, 127 | div.body h2, 128 | div.body h3, 129 | div.body h4, 130 | div.body h5, 131 | div.body h6 { 132 | font-family: 'Trebuchet MS', sans-serif; 133 | background-color: #f2f2f2; 134 | font-weight: normal; 135 | color: #20435c; 136 | border-bottom: 1px solid #ccc; 137 | margin: 20px -20px 10px -20px; 138 | padding: 3px 0 3px 10px; 139 | } 140 | 141 | div.body h1 { margin-top: 0; font-size: 200%; } 142 | div.body h2 { font-size: 160%; } 143 | div.body h3 { font-size: 140%; } 144 | div.body h4 { font-size: 120%; } 145 | div.body h5 { font-size: 110%; } 146 | div.body h6 { font-size: 100%; } 147 | 148 | a.headerlink { 149 | color: #c60f0f; 150 | font-size: 0.8em; 151 | padding: 0 4px 0 4px; 152 | text-decoration: none; 153 | } 154 | 155 | a.headerlink:hover { 156 | background-color: #c60f0f; 157 | color: white; 158 | } 159 | 160 | div.body p, div.body dd, div.body li { 161 | text-align: justify; 162 | line-height: 130%; 163 | } 164 | 165 | div.admonition p.admonition-title + p { 166 | display: inline; 167 | } 168 | 169 | div.note { 170 | background-color: #eee; 171 | border: 1px solid #ccc; 172 | } 173 | 174 | div.seealso { 175 | background-color: #ffc; 176 | border: 1px solid #ff6; 177 | } 178 | 179 | div.topic { 180 | background-color: #eee; 181 | } 182 | 183 | div.warning { 184 | background-color: #ffe4e4; 185 | border: 1px solid #f66; 186 | } 187 | 188 | p.admonition-title { 189 | display: inline; 190 | } 191 | 192 | p.admonition-title:after { 193 | content: ":"; 194 | } 195 | 196 | pre { 197 | padding: 5px; 198 | background-color: #eeffcc; 199 | color: #333333; 200 | line-height: 120%; 201 | border: 1px solid #ac9; 202 | border-left: none; 203 | border-right: none; 204 | } 205 | 206 | tt { 207 | background-color: #ecf0f3; 208 | padding: 0 1px 0 1px; 209 | font-size: 0.95em; 210 | } 211 | 212 | .warning tt { 213 | background: #efc2c2; 214 | } 215 | 216 | .note tt { 217 | background: #d6d6d6; 218 | } -------------------------------------------------------------------------------- /doc/_build/html/_static/doctools.js: -------------------------------------------------------------------------------- 1 | /// XXX: make it cross browser 2 | 3 | /** 4 | * make the code below compatible with browsers without 5 | * an installed firebug like debugger 6 | */ 7 | if (!window.console || !console.firebug) { 8 | var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", 9 | "group", "groupEnd", "time", "timeEnd", "count", "trace", "profile", "profileEnd"]; 10 | window.console = {}; 11 | for (var i = 0; i < names.length; ++i) 12 | window.console[names[i]] = function() {} 13 | } 14 | 15 | /** 16 | * small helper function to urldecode strings 17 | */ 18 | jQuery.urldecode = function(x) { 19 | return decodeURIComponent(x).replace(/\+/g, ' '); 20 | } 21 | 22 | /** 23 | * small helper function to urlencode strings 24 | */ 25 | jQuery.urlencode = encodeURIComponent; 26 | 27 | /** 28 | * This function returns the parsed url parameters of the 29 | * current request. Multiple values per key are supported, 30 | * it will always return arrays of strings for the value parts. 31 | */ 32 | jQuery.getQueryParameters = function(s) { 33 | if (typeof s == 'undefined') 34 | s = document.location.search; 35 | var parts = s.substr(s.indexOf('?') + 1).split('&'); 36 | var result = {}; 37 | for (var i = 0; i < parts.length; i++) { 38 | var tmp = parts[i].split('=', 2); 39 | var key = jQuery.urldecode(tmp[0]); 40 | var value = jQuery.urldecode(tmp[1]); 41 | if (key in result) 42 | result[key].push(value); 43 | else 44 | result[key] = [value]; 45 | } 46 | return result; 47 | } 48 | 49 | /** 50 | * small function to check if an array contains 51 | * a given item. 52 | */ 53 | jQuery.contains = function(arr, item) { 54 | for (var i = 0; i < arr.length; i++) { 55 | if (arr[i] == item) 56 | return true; 57 | } 58 | return false; 59 | } 60 | 61 | /** 62 | * highlight a given string on a jquery object by wrapping it in 63 | * span elements with the given class name. 64 | */ 65 | jQuery.fn.highlightText = function(text, className) { 66 | function highlight(node) { 67 | if (node.nodeType == 3) { 68 | var val = node.nodeValue; 69 | var pos = val.toLowerCase().indexOf(text); 70 | if (pos >= 0 && !jQuery.className.has(node.parentNode, className)) { 71 | var span = document.createElement("span"); 72 | span.className = className; 73 | span.appendChild(document.createTextNode(val.substr(pos, text.length))); 74 | node.parentNode.insertBefore(span, node.parentNode.insertBefore( 75 | document.createTextNode(val.substr(pos + text.length)), 76 | node.nextSibling)); 77 | node.nodeValue = val.substr(0, pos); 78 | } 79 | } 80 | else if (!jQuery(node).is("button, select, textarea")) { 81 | jQuery.each(node.childNodes, function() { 82 | highlight(this) 83 | }); 84 | } 85 | } 86 | return this.each(function() { 87 | highlight(this); 88 | }); 89 | } 90 | 91 | /** 92 | * Small JavaScript module for the documentation. 93 | */ 94 | var Documentation = { 95 | 96 | init : function() { 97 | this.fixFirefoxAnchorBug(); 98 | this.highlightSearchWords(); 99 | this.initModIndex(); 100 | }, 101 | 102 | /** 103 | * i18n support 104 | */ 105 | TRANSLATIONS : {}, 106 | PLURAL_EXPR : function(n) { return n == 1 ? 0 : 1; }, 107 | LOCALE : 'unknown', 108 | 109 | // gettext and ngettext don't access this so that the functions 110 | // can savely bound to a different name (_ = Documentation.gettext) 111 | gettext : function(string) { 112 | var translated = Documentation.TRANSLATIONS[string]; 113 | if (typeof translated == 'undefined') 114 | return string; 115 | return (typeof translated == 'string') ? translated : translated[0]; 116 | }, 117 | 118 | ngettext : function(singular, plural, n) { 119 | var translated = Documentation.TRANSLATIONS[singular]; 120 | if (typeof translated == 'undefined') 121 | return (n == 1) ? singular : plural; 122 | return translated[Documentation.PLURALEXPR(n)]; 123 | }, 124 | 125 | addTranslations : function(catalog) { 126 | for (var key in catalog.messages) 127 | this.TRANSLATIONS[key] = catalog.messages[key]; 128 | this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); 129 | this.LOCALE = catalog.locale; 130 | }, 131 | 132 | /** 133 | * add context elements like header anchor links 134 | */ 135 | addContextElements : function() { 136 | $('div[id] > :header:first').each(function() { 137 | $('\u00B6'). 138 | attr('href', '#' + this.id). 139 | attr('title', _('Permalink to this headline')). 140 | appendTo(this); 141 | }); 142 | $('dt[id]').each(function() { 143 | $('\u00B6'). 144 | attr('href', '#' + this.id). 145 | attr('title', _('Permalink to this definition')). 146 | appendTo(this); 147 | }); 148 | }, 149 | 150 | /** 151 | * workaround a firefox stupidity 152 | */ 153 | fixFirefoxAnchorBug : function() { 154 | if (document.location.hash && $.browser.mozilla) 155 | window.setTimeout(function() { 156 | document.location.href += ''; 157 | }, 10); 158 | }, 159 | 160 | /** 161 | * highlight the search words provided in the url in the text 162 | */ 163 | highlightSearchWords : function() { 164 | var params = $.getQueryParameters(); 165 | var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; 166 | if (terms.length) { 167 | var body = $('div.body'); 168 | window.setTimeout(function() { 169 | $.each(terms, function() { 170 | body.highlightText(this.toLowerCase(), 'highlight'); 171 | }); 172 | }, 10); 173 | $('') 175 | .appendTo($('.sidebar .this-page-menu')); 176 | } 177 | }, 178 | 179 | /** 180 | * init the modindex toggle buttons 181 | */ 182 | initModIndex : function() { 183 | var togglers = $('img.toggler').click(function() { 184 | var src = $(this).attr('src'); 185 | var idnum = $(this).attr('id').substr(7); 186 | console.log($('tr.cg-' + idnum).toggle()); 187 | if (src.substr(-9) == 'minus.png') 188 | $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); 189 | else 190 | $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); 191 | }).css('display', ''); 192 | if (DOCUMENTATION_OPTIONS.COLLAPSE_MODINDEX) { 193 | togglers.click(); 194 | } 195 | }, 196 | 197 | /** 198 | * helper function to hide the search marks again 199 | */ 200 | hideSearchWords : function() { 201 | $('.sidebar .this-page-menu li.highlight-link').fadeOut(300); 202 | $('span.highlight').removeClass('highlight'); 203 | }, 204 | 205 | /** 206 | * make the url absolute 207 | */ 208 | makeURL : function(relativeURL) { 209 | return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; 210 | }, 211 | 212 | /** 213 | * get the current relative url 214 | */ 215 | getCurrentURL : function() { 216 | var path = document.location.pathname; 217 | var parts = path.split(/\//); 218 | $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { 219 | if (this == '..') 220 | parts.pop(); 221 | }); 222 | var url = parts.join('/'); 223 | return path.substring(url.lastIndexOf('/') + 1, path.length - 1); 224 | } 225 | }; 226 | 227 | // quick alias for translations 228 | _ = Documentation.gettext; 229 | 230 | $(document).ready(function() { 231 | Documentation.init(); 232 | }); 233 | -------------------------------------------------------------------------------- /doc/_build/html/_static/pygments.css: -------------------------------------------------------------------------------- 1 | .hll { background-color: #ffffcc } 2 | .c { color: #408090; font-style: italic } /* Comment */ 3 | .err { border: 1px solid #FF0000 } /* Error */ 4 | .k { color: #007020; font-weight: bold } /* Keyword */ 5 | .o { color: #666666 } /* Operator */ 6 | .cm { color: #408090; font-style: italic } /* Comment.Multiline */ 7 | .cp { color: #007020 } /* Comment.Preproc */ 8 | .c1 { color: #408090; font-style: italic } /* Comment.Single */ 9 | .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */ 10 | .gd { color: #A00000 } /* Generic.Deleted */ 11 | .ge { font-style: italic } /* Generic.Emph */ 12 | .gr { color: #FF0000 } /* Generic.Error */ 13 | .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 14 | .gi { color: #00A000 } /* Generic.Inserted */ 15 | .go { color: #303030 } /* Generic.Output */ 16 | .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ 17 | .gs { font-weight: bold } /* Generic.Strong */ 18 | .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 19 | .gt { color: #0040D0 } /* Generic.Traceback */ 20 | .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ 21 | .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ 22 | .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */ 23 | .kp { color: #007020 } /* Keyword.Pseudo */ 24 | .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ 25 | .kt { color: #902000 } /* Keyword.Type */ 26 | .m { color: #208050 } /* Literal.Number */ 27 | .s { color: #4070a0 } /* Literal.String */ 28 | .na { color: #4070a0 } /* Name.Attribute */ 29 | .nb { color: #007020 } /* Name.Builtin */ 30 | .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ 31 | .no { color: #60add5 } /* Name.Constant */ 32 | .nd { color: #555555; font-weight: bold } /* Name.Decorator */ 33 | .ni { color: #d55537; font-weight: bold } /* Name.Entity */ 34 | .ne { color: #007020 } /* Name.Exception */ 35 | .nf { color: #06287e } /* Name.Function */ 36 | .nl { color: #002070; font-weight: bold } /* Name.Label */ 37 | .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ 38 | .nt { color: #062873; font-weight: bold } /* Name.Tag */ 39 | .nv { color: #bb60d5 } /* Name.Variable */ 40 | .ow { color: #007020; font-weight: bold } /* Operator.Word */ 41 | .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .mf { color: #208050 } /* Literal.Number.Float */ 43 | .mh { color: #208050 } /* Literal.Number.Hex */ 44 | .mi { color: #208050 } /* Literal.Number.Integer */ 45 | .mo { color: #208050 } /* Literal.Number.Oct */ 46 | .sb { color: #4070a0 } /* Literal.String.Backtick */ 47 | .sc { color: #4070a0 } /* Literal.String.Char */ 48 | .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ 49 | .s2 { color: #4070a0 } /* Literal.String.Double */ 50 | .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ 51 | .sh { color: #4070a0 } /* Literal.String.Heredoc */ 52 | .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ 53 | .sx { color: #c65d09 } /* Literal.String.Other */ 54 | .sr { color: #235388 } /* Literal.String.Regex */ 55 | .s1 { color: #4070a0 } /* Literal.String.Single */ 56 | .ss { color: #517918 } /* Literal.String.Symbol */ 57 | .bp { color: #007020 } /* Name.Builtin.Pseudo */ 58 | .vc { color: #bb60d5 } /* Name.Variable.Class */ 59 | .vg { color: #bb60d5 } /* Name.Variable.Global */ 60 | .vi { color: #bb60d5 } /* Name.Variable.Instance */ 61 | .il { color: #208050 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /doc/_build/html/genindex.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Index — djangoembed v0.1 documentation 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |

Index

42 | 43 | 44 | 45 |
46 | 47 | 48 | 49 | 50 |
51 |
52 |
53 |
54 |
55 | 56 | 57 | 58 | 70 | 71 |
72 |
73 |
74 |
75 | 84 | 88 | 89 | -------------------------------------------------------------------------------- /doc/_build/html/index.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Welcome to djangoembed’s documentation! — djangoembed v0.1 documentation 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 |

Welcome to djangoembed’s documentation!

46 |

Why OEmbed? To add rich content to your sites painlessly

47 |

Contents:

48 | 79 |
80 |
81 |

Indices and tables

82 | 87 |
88 | 89 | 90 |
91 |
92 |
93 |
94 |
95 |

Table Of Contents

96 | 102 | 103 |

Next topic

104 |

Getting Started with OEmbed

106 |

This Page

107 | 111 | 123 | 124 |
125 |
126 |
127 |
128 | 140 | 144 | 145 | -------------------------------------------------------------------------------- /doc/_build/html/objects.inv: -------------------------------------------------------------------------------- 1 | # Sphinx inventory version 1 2 | # Project: djangoembed 3 | # Version: 0.1 4 | -------------------------------------------------------------------------------- /doc/_build/html/search.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | Search — djangoembed v0.1 documentation 9 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 35 | 36 |
37 |
38 |
39 |
40 | 41 |

Search

42 |
43 | 44 |

45 | Please activate JavaScript to enable the search 46 | functionality. 47 |

48 |
49 |

50 | From here you can search these documents. Enter your search 51 | words into the box below and click "search". Note that the search 52 | function will automatically search for all of the words. Pages 53 | containing fewer words won't appear in the result list. 54 |

55 |
56 | 57 | 58 | 59 |
60 | 61 |
62 | 63 |
64 | 65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | 83 | 84 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /doc/_build/html/searchindex.js: -------------------------------------------------------------------------------- 1 | Search.setIndex({desctypes:{},terms:{represent:4,all:[0,4],code:0,forget:2,consum:[0,1,2,3],month:2,regex_list:[],viddler:[],follow:[2,3],content:[0,1,2,3,4],depend:[0,2,4],tweet:4,flash:0,articl:2,under:3,spec:[1,3,4],sourc:3,everi:3,straightforward:0,farm4:4,fall:3,baseprovid:[1,2],list:2,entryprovid:2,div:0,dimens:2,nby:[],second:3,video:[0,1,2,3,4],pass:[2,4],urlpattern:2,index:1,what:[0,1,2,4],appear:2,introspect:2,abl:[0,2,3],uniform:2,access:2,allowscriptaccess:0,version:[0,3,4],"new":2,method:2,metadata:[0,1,3,4],full:4,gener:[2,3,4],here:2,bodi:2,let:2,along:3,unpublish:[],valu:[0,3],oembedprovid:2,search:1,mysit:2,credit:0,cache_ag:[3,4],action:[],pub_dat:2,botmib:0,provider_url:[0,3,4],regardless:3,modul:1,api:[2,4],zb8t0193:4,instal:[0,1],mobil:0,regex:2,from:[0,2,4],describ:3,would:2,regist:2,two:2,websit:4,process:2,live:[2,4],call:2,oftentim:2,type:[0,2,3,4],desir:[],get_queryset:[],templat:[0,2],cach:3,must:[3,4],endpoint:[2,3,4],retriev:4,setup:0,work:0,can:[0,2,4],caveat:2,purpos:[2,4],fetch:2,def:2,give:2,oembedresourc:2,mayb:0,registr:0,accept:4,want:[0,2,4],blog_list:2,alwai:0,multipl:2,rather:3,write:2,how:[1,2],instead:[2,3],xhtml:3,simpl:4,updat:4,map:2,resourc:[0,1,2,3,4],clone:0,after:0,date:2,data:3,github:0,u202d:4,third:[2,4],django:[0,1,2],allow:[2,3,4],indic:1,order:0,movi:0,through:[0,2],"_default_manag":[],vari:4,paramet:[2,3],render:2,img:3,might:2,pixel:3,them:2,"return":[2,3,4],thei:[2,3],python:0,dai:2,now:[0,2,4],blog_index:2,name:[0,2,3,4],revers:2,authent:4,thumbnail_url:[3,4],found:2,everyth:4,domain:[2,3],mai:[2,3],intellig:2,meta:2,"static":[3,4],year:2,our:[2,4],extract:4,parse_text:0,author_nam:[0,2,3,4],publish:2,categori:3,shockwav:0,resource_typ:2,proxi:2,standard:4,standalon:4,base:2,fields_to_match:2,revision3:[],filter:[0,2],thing:[],place:[0,4],charfield:2,lifetim:3,first:[0,2],origin:3,directli:4,number:3,hook:2,alreadi:2,owner:3,installed_app:0,endoemb:0,differ:[],oembedexcept:2,start:[0,1],flickrprovid:2,max_length:2,too:4,urlresolv:2,imagefield:2,option:[0,2,3],get_absolute_url:2,specifi:[2,3,4],part:4,pars:4,textfield:2,than:3,provid:[0,1,2,3,4],djangodatebasedprovid:[1,2],structur:4,bee:4,project:[0,1],consumpt:0,sai:2,fashion:2,arg:2,queryset:2,argument:[2,4],have:[0,2,3,4],tabl:1,need:[0,4],notimplementederror:2,element:3,autodiscov:0,built:2,self:2,client:0,also:[2,3,4],without:[3,4],take:2,which:[2,3],singl:4,simplifi:2,sure:0,pain:4,usernam:2,object:[0,2,3,4],most:0,oemb:[0,1,2,3,4],"class":[0,2],don:2,url:[0,2,3,4],request:[2,3],doe:[1,3,4],occasion:2,show:4,text:3,painlessli:1,djangoemb:[0,1,2,3],xml:4,onli:[2,3,4],explicitli:2,pretti:0,configur:2,should:[0,2,3],suppos:[2,4],rich:[1,2,3,4],local:2,hit:2,get:[0,1,2,4],watch:0,youtub:[0,3],resize_photo:2,requir:[2,3],embedd:3,emb:[0,2,3],integr:4,entry_slug:2,view:2,set:[0,2],thumbnail_width:3,maximum:4,see:[2,4],u202:4,oembed_provid:2,respons:[2,3,4],best:4,concern:4,statu:[],maxwidth:[2,3,4],pattern:[0,2],"import":[0,2],flickr:[2,4],thumbnail:[2,3],attribut:[2,4],accord:2,extend:2,numer:[2,4],foreignkei:2,solv:[1,4],come:2,problem:[1,4],addit:[],admin:0,oembed_tag:0,oembedconsum:0,date_field:2,provider_nam:[0,3,4],modif:2,com:[0,2,4],named_view:2,load:[0,3],overview:4,slugfield:2,height:[0,3,4],respect:[3,4],assum:2,backend:2,httpprovid:[1,2],coupl:0,been:0,json:[2,4],much:4,interest:4,basic:[1,2,3],quickli:4,worldcompani:0,imag:[1,2,3,4],great:2,ani:[2,3],"2341623661_7c99f48bbf_m":4,present:[0,3],look:2,vimeo:3,servic:[2,4],aim:4,defin:[2,3],abov:[2,4],margin:3,layer:2,readi:0,site:[0,1,2,4],jpg:4,player:3,u202cbe:4,kwarg:2,conf:2,develop:4,welcom:1,author:[2,3,4],suggest:3,make:[2,4],format:[2,4],same:3,handl:[1,2],html:[0,2,3],pad:3,author_url:[0,2,3,4],document:1,difficult:4,status:4,ifram:3,http:[0,2,3,4],href:0,rais:2,user:[0,2,4],mani:2,auto_now_add:2,task:4,off:3,entri:2,markup:3,well:0,exampl:[1,2,4],thi:[0,2,3,4],choos:3,model:2,dimension:[0,2],datefield:2,just:[0,3],photo:[1,2,3,4],rest:4,parti:[2,4],touch:4,bit:0,web:4,blog_detail:2,easi:2,nda_osweyn8:0,param:0,blog:[0,2],add:[0,1],valid:[1,2,3],snap:0,vulner:3,app:[0,2],match:2,build:2,applic:0,u202c:4,transpar:2,read:4,know:2,insert:3,thumbnail_height:3,xss:3,like:[0,2,3,4],specif:3,paramat:3,either:3,popular:2,resiz:2,page:1,alabama:0,www:[0,2,4],right:0,deal:3,twitter:4,some:[0,2],syncdb:0,avoid:3,subclass:2,core:2,run:[0,2,4],obei:3,post:4,"super":2,slug:2,wealth:4,src:0,about:4,obj:2,constraint:[0,2],"2341623661_7c99f48bbf":4,allowfullscreen:0,act:2,leprechaun:0,own:[2,4],within:2,encod:4,your:[0,1,2],git:0,wai:[0,4],why:1,avail:[0,4],width:[0,3,4],includ:4,lot:4,request_resourc:2,link:[1,2,3,4],"true":[0,2],utf:4,"default":2,wish:3,displai:[3,4],below:[0,3],djangoprovid:[1,2],embed:[0,4],booleanfield:2,pil:2,qik:[],maxheight:[2,3],repres:3,exist:4,file:2,curl:4,check:2,titl:[0,2,3,4],get_data:0,when:[2,4],detail:[2,4],field:2,other:[3,4],you:[0,2,3,4],intend:2,"600x600":0,endpoint_url:2,directori:2,datetimefield:2},titles:["Getting Started with OEmbed","Welcome to djangoembed’s documentation!","Providing Resources","OEmbed Resources","The OEmbed Spec"],modules:{},descrefs:{},filenames:["djangoembed/getting_started","index","djangoembed/providing_resources","djangoembed/oembed_resources","djangoembed/spec"]}) -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # django-oembed documentation build configuration file, created by 4 | # sphinx-quickstart on Sat Feb 13 14:41:46 2010. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.append(os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # Add any Sphinx extension module names here, as strings. They can be extensions 24 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 25 | extensions = [] 26 | 27 | # Add any paths that contain templates here, relative to this directory. 28 | templates_path = ['_templates'] 29 | 30 | # The suffix of source filenames. 31 | source_suffix = '.rst' 32 | 33 | # The encoding of source files. 34 | #source_encoding = 'utf-8' 35 | 36 | # The master toctree document. 37 | master_doc = 'index' 38 | 39 | # General information about the project. 40 | project = u'djangoembed' 41 | copyright = u'2010, The World Company' 42 | 43 | # The version info for the project you're documenting, acts as replacement for 44 | # |version| and |release|, also used in various other places throughout the 45 | # built documents. 46 | # 47 | # The short X.Y version. 48 | version = '0.1' 49 | # The full version, including alpha/beta/rc tags. 50 | release = '0.1' 51 | 52 | # The language for content autogenerated by Sphinx. Refer to documentation 53 | # for a list of supported languages. 54 | #language = None 55 | 56 | # There are two options for replacing |today|: either, you set today to some 57 | # non-false value, then it is used: 58 | #today = '' 59 | # Else, today_fmt is used as the format for a strftime call. 60 | #today_fmt = '%B %d, %Y' 61 | 62 | # List of documents that shouldn't be included in the build. 63 | #unused_docs = [] 64 | 65 | # List of directories, relative to source directory, that shouldn't be searched 66 | # for source files. 67 | exclude_trees = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. Major themes that come with 93 | # Sphinx are currently 'default' and 'sphinxdoc'. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_use_modindex = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, an OpenSearch description file will be output, and all pages will 153 | # contain a tag referring to it. The value of this option must be the 154 | # base URL from which the finished HTML is served. 155 | #html_use_opensearch = '' 156 | 157 | # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). 158 | #html_file_suffix = '' 159 | 160 | # Output file base name for HTML help builder. 161 | htmlhelp_basename = 'django-oembeddoc' 162 | 163 | 164 | # -- Options for LaTeX output -------------------------------------------------- 165 | 166 | # The paper size ('letter' or 'a4'). 167 | #latex_paper_size = 'letter' 168 | 169 | # The font size ('10pt', '11pt' or '12pt'). 170 | #latex_font_size = '10pt' 171 | 172 | # Grouping the document tree into LaTeX files. List of tuples 173 | # (source start file, target name, title, author, documentclass [howto/manual]). 174 | latex_documents = [ 175 | ('index', 'django-oembed.tex', u'django-oembed Documentation', 176 | u'The World Company', 'manual'), 177 | ] 178 | 179 | # The name of an image file (relative to this directory) to place at the top of 180 | # the title page. 181 | #latex_logo = None 182 | 183 | # For "manual" documents, if this is true, then toplevel headings are parts, 184 | # not chapters. 185 | #latex_use_parts = False 186 | 187 | # Additional stuff for the LaTeX preamble. 188 | #latex_preamble = '' 189 | 190 | # Documents to append as an appendix to all manuals. 191 | #latex_appendices = [] 192 | 193 | # If false, no module index is generated. 194 | #latex_use_modindex = True 195 | -------------------------------------------------------------------------------- /doc/djangoembed/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started with OEmbed 2 | =========================== 3 | 4 | Installation 5 | ------------ 6 | 7 | First, you need to install OEmbed. It is available at http://github.com/worldcompany/djangoembed/ 8 | 9 | :: 10 | 11 | git clone git://github.com/worldcompany/djangoembed/ 12 | cd djangoembed 13 | python setup.py install 14 | 15 | Adding to your Django Project 16 | -------------------------------- 17 | 18 | After installing, adding OEmbed consumption to your projects is a snap. First, 19 | add it to your projects' INSTALLED_APPs and run 'syncdb':: 20 | 21 | # settings.py 22 | INSTALLED_APPS = [ 23 | ... 24 | 'oembed' 25 | ] 26 | 27 | djangoembed uses a registration pattern like the admin's. In order to be 28 | sure all apps have been loaded, djangoembed should run autodiscover() in the 29 | urls.py. If you like, you can place this code right below your admin.autodiscover() 30 | bits:: 31 | 32 | # urls.py 33 | import oembed 34 | oembed.autodiscover() 35 | 36 | Consuming Resources 37 | ------------------- 38 | 39 | Now you're ready to start consuming OEmbed-able objects. There are a couple of 40 | options depending on what you want to do. The most straightforward way to get 41 | up-and-running is to add it to your templates:: 42 | 43 | {% load oembed_tags %} 44 | 45 | {% oembed %}blog.content{% endoembed %} 46 | 47 | {# or use the filter #} 48 | 49 | {{ blog.content|oembed }} 50 | 51 | {# maybe you're working with some dimensional constraints #} 52 | 53 | {% oembed "600x600" %}blog.content{% endoembed %} 54 | 55 | {{ blog.content|oembed:"600x600" }} 56 | 57 | You can consume oembed objects in python as well:: 58 | 59 | import oembed 60 | oembed.autodiscover() 61 | 62 | # just get the metadata 63 | resource = oembed.site.embed('http://www.youtube.com/watch?v=nda_OSWeyn8') 64 | resource.get_data() 65 | 66 | {u'author_name': u'botmib', 67 | u'author_url': u'http://www.youtube.com/user/botmib', 68 | u'height': 313, 69 | u'html': u'', 70 | u'provider_name': u'YouTube', 71 | u'provider_url': u'http://www.youtube.com/', 72 | u'title': u'Leprechaun in Mobile, Alabama', 73 | u'type': u'video', 74 | u'version': u'1.0', 75 | u'width': 384} 76 | 77 | # get the metadata and run it through a template for pretty presentation 78 | from oembed.consumer import OEmbedConsumer 79 | client = OEmbedConsumer() 80 | embedded = client.parse_text("http://www.youtube.com/watch?v=nda_OSWeyn8") 81 | 82 |
83 | 84 | 85 | 86 | 87 | 93 | 94 | 95 |

96 | Leprechaun in Mobile, Alabama 97 | by 98 | botmib 99 |

100 |
' 101 | -------------------------------------------------------------------------------- /doc/djangoembed/oembed_resources.rst: -------------------------------------------------------------------------------- 1 | OEmbed Resources 2 | ================ 3 | 4 | Resources are the objects returned by an OEmbed endpoint. The OEmbed spec 5 | defines 4 resource types: 6 | 7 | * Video 8 | * Photo 9 | * Rich 10 | * Link 11 | 12 | 13 | Basic Metadata 14 | -------------- 15 | 16 | Regardless of which resource type you are dealing with, the same metadata may be 17 | provided: 18 | 19 | * type (required) 20 | The resource type. Valid values, along with value-specific parameters, are described below. 21 | * version (required) 22 | The oEmbed version number. This must be 1.0. 23 | * title (optional) 24 | A text title, describing the resource. 25 | * author_name (optional) 26 | The name of the author/owner of the resource. 27 | * author_url (optional) 28 | A URL for the author/owner of the resource. 29 | * provider_name (optional) 30 | The name of the resource provider. 31 | * provider_url (optional) 32 | The url of the resource provider. 33 | * cache_age (optional) 34 | The suggested cache lifetime for this resource, in seconds. Consumers may choose to use this value or not. 35 | * thumbnail_url (optional) 36 | A URL to a thumbnail image representing the resource. The thumbnail must respect any maxwidth and maxheight parameters. If this paramater is present, thumbnail_width and thumbnail_height must also be present. 37 | * thumbnail_width (optional) 38 | The width of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_height must also be present. 39 | * thumbnail_height (optional) 40 | The height of the optional thumbnail. If this paramater is present, thumbnail_url and thumbnail_width must also be present. 41 | 42 | 43 | Video Resources 44 | --------------- 45 | 46 | Video resources are embeddable video players, and are returned by providers like 47 | YouTube and Vimeo. Every video resource **must** provide the following 48 | metadata: 49 | 50 | * html (required) 51 | The HTML required to embed a video player. The HTML should have no padding 52 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 53 | avoid XSS vulnerabilities. 54 | * width (required) 55 | The width in pixels required to display the HTML. 56 | * height (required) 57 | The height in pixels required to display the HTML. 58 | 59 | Responses of this type must obey the maxwidth and maxheight request parameters. 60 | If a provider wishes the consumer to just provide a thumbnail, rather than an 61 | embeddable player, they should instead return a photo response type. 62 | 63 | 64 | Photo Resources 65 | --------------- 66 | 67 | Photo resources are static photos. The following parameters are defined: 68 | 69 | * url (required) 70 | The source URL of the image. Consumers should be able to insert this URL 71 | into an element. Only HTTP and HTTPS URLs are valid. 72 | * width (required) 73 | The width in pixels of the image specified in the url parameter. 74 | * height (required) 75 | The height in pixels of the image specified in the url parameter. 76 | 77 | Responses of this type must obey the maxwidth and maxheight request parameters. 78 | 79 | 80 | Rich Resources 81 | -------------- 82 | 83 | This type is used for rich HTML content that does not fall under one of the 84 | other categories. The following parameters are defined: 85 | 86 | * html (required) 87 | The HTML required to display the resource. The HTML should have no padding 88 | or margins. Consumers may wish to load the HTML in an off-domain iframe to 89 | avoid XSS vulnerabilities. The markup should be valid XHTML 1.0 Basic. 90 | * width (required) 91 | The width in pixels required to display the HTML. 92 | * height (required) 93 | The height in pixels required to display the HTML. 94 | 95 | Responses of this type must obey the maxwidth and maxheight request parameters. 96 | 97 | 98 | Link Resources 99 | -------------- 100 | 101 | Responses of this type allow a provider to return any generic embed data 102 | (such as title and author_name), without providing either the url or html 103 | parameters. The consumer may then link to the resource, using the URL specified 104 | in the original request. 105 | 106 | 107 | Resource Validation 108 | ------------------- 109 | 110 | djangoembed validates resources for you! 111 | -------------------------------------------------------------------------------- /doc/djangoembed/providing_resources.rst: -------------------------------------------------------------------------------- 1 | Providing Resources 2 | =================== 3 | 4 | Suppose you would like your own content to be OEmbed-able. Making your own site 5 | a provider also allows you to consume your own content in a uniform fashion. 6 | Say you have a blog app and occasionally link to your own articles. By defining 7 | a provider for your blog entries, you can consume them transparently! 8 | 9 | django-oembed comes with base classes you can extend to write your own providers 10 | depending on what you want to embed. 11 | 12 | BaseProvider 13 | ------------ 14 | 15 | Taking a look at BaseProvider, we see that it defines two attributes and one 16 | method:: 17 | 18 | class BaseProvider(object): 19 | """ 20 | Base class for OEmbed resources. 21 | """ 22 | regex = r'' 23 | provides = True # allow this provider to be accessed by third parties 24 | 25 | def request_resource(self, url, **kwargs): 26 | """ 27 | Get an OEmbedResource from one of the providers configured in this 28 | provider according to the resource url. 29 | 30 | Args: 31 | url: The url of the resource to get. 32 | **kwargs: Optional parameters to pass in to the provider. 33 | 34 | Returns: 35 | OEmbedResource object. 36 | 37 | If no object returned, raises OEmbedException 38 | """ 39 | raise NotImplementedError 40 | 41 | When the consumer processes a URL, it checks to see if it's URL matches any of 42 | the regexes provided by registered OEmbedProviders. When a match is found, that 43 | provider's ``endpoint()`` method is called, with the URL and any arguments 44 | passed in, such as 'format', 'maxwidth', or 'maxheight'. The ``endpoint()`` 45 | method returns a valid OEmbedResource or raises an ``OEmbedException``. 46 | 47 | 48 | HTTPProvider 49 | ------------ 50 | 51 | Many popular content sites already provide OEmbed endpoints. The ``HTTPProvider`` 52 | acts as a proxy layer that handles fetching resources and validating them. 53 | 54 | Looking in ``oembed_providers.py``, there are numerous providers already set-up 55 | for you. Here is what the Flickr provider looks like:: 56 | 57 | class FlickrProvider(HTTPProvider): 58 | endpoint_url = 'http://www.flickr.com/services/oembed/' 59 | regex = 'http://(?:www\.)?flickr\.com/photos/\S+?/(?:sets/)?\d+/?' 60 | 61 | 62 | DjangoProvider 63 | -------------- 64 | 65 | The ``DjangoProvider`` class is intended to make writing providers for your 66 | Django models super easy. It knows how to introspect your models for 67 | ImageFields, it can intelligently resize your objects based on user-specified 68 | dimensions, and it allows you to return HTML rendered through your own templates 69 | for rich content like videos. 70 | 71 | Let's look at a basic example you might use for a Blog:: 72 | 73 | # assume the following is our Blog model 74 | from django.db import models 75 | from django.core.urlresolvers import reverse 76 | 77 | class Entry(models.Model): 78 | title = models.CharField(max_length=255) 79 | slug = models.SlugField() 80 | body = models.TextField() 81 | author = models.ForeignKey(User) 82 | published = models.BooleanField() 83 | pub_date = models.DateTimeField(auto_now_add=True) 84 | 85 | def get_absolute_url(self): 86 | return reverse('blog_detail', args=[self.slug]) 87 | 88 | The ``urls.py`` defines a list and detail view:: 89 | 90 | from django.conf.urls.defaults import * 91 | 92 | urlpatterns = patterns('blog.views', 93 | url(r'^(?P[-a-z0-9]+)/$', 94 | view='blog_detail', 95 | name='blog_detail'), 96 | url(r'^$', 97 | view='blog_list', 98 | name='blog_index'), 99 | ) 100 | 101 | Now let's write a provider for it. This lives in an ``oembed_providers.py`` 102 | file in your blog app's directory:: 103 | 104 | import oembed 105 | from blog.models import Entry 106 | from oembed.providers import DjangoProvider 107 | 108 | class EntryProvider(DjangoProvider): 109 | resource_type = 'link' # this is required 110 | 111 | class Meta: 112 | model = Entry 113 | named_view = 'blog_detail' 114 | fields_to_match = {'entry_slug': 'slug'} # map url field to model field 115 | 116 | def get_queryset(self): 117 | queryset = Entry.objects.filter(published=True) 118 | 119 | def author_name(self, obj): 120 | return obj.author.username 121 | 122 | def author_url(self, obj): 123 | return obj.author.get_absolute_url() 124 | 125 | def title(self, obj): 126 | return obj.title 127 | 128 | # don't forget to register your provider 129 | oembed.site.register(EntryProvider) 130 | 131 | You should now be able to hit your API endpoint (by default /oembed/json/) with 132 | a published entry URL and get a JSON response! 133 | 134 | One caveat: django provider URLs build their regexes using site domains from the 135 | sites app. If your site is ``http://www.mysite.com`` and you are running locally, 136 | using ``127.0.0.1:8000``, you will want to give your endpoint URLs as they would 137 | appear on your live site, so: 138 | 139 | http://www.mysite.com/blog/this-is-a-great-entry/ 140 | 141 | instead of 142 | 143 | http://127.0.0.1/blog/this-is-a-great-entry/ 144 | 145 | 146 | DjangoDateBasedProvider 147 | ----------------------- 148 | 149 | Oftentimes, your content may live a date-based URL. Writing providers for these 150 | models is simplified by using the ``DjangoDateBasedProvider`` class. Returning 151 | to the Blog example from above, let's assume the detail view looks like this:: 152 | 153 | url(r'^(?P\d{4})/(?P\w{3})/(?P\d{1,2})/(?P[\w-]+)/$', 154 | view='blog_detail', 155 | name='blog_detail'), 156 | 157 | The only modification we will make to our ``EntryProvider`` will be to subclass 158 | the date-based provider class:: 159 | 160 | from oembed.providers import DjangoDateBasedProvider 161 | 162 | class EntryProvider(DjangoDateBasedProvider): 163 | ... 164 | 165 | The date-based provider introspects your model and uses the first DateField or 166 | DateTimeField. If you have multiple fields of this type, you can explicitly 167 | define a date field:: 168 | 169 | from oembed.providers import DjangoDateBasedProvider 170 | 171 | class EntryProvider(DjangoDateBasedProvider): 172 | ... 173 | class Meta: 174 | ... 175 | date_field = 'pub_date' 176 | 177 | 178 | How are images handled? 179 | ----------------------- 180 | 181 | By default djangoembed uses PIL to resize images within the dimensional 182 | constraints requested. The built-in DjangoProvider has a resize_photo() method 183 | and a thumbnail() method that take as their parameters an object and some 184 | dimensions. These methods call a general-purpose resize() method which 185 | hooks into the image processing backend (by default PIL, but you can write 186 | your own!) and resizes the photo, returning the url of the resized image and 187 | the new dimensions. 188 | 189 | If you want to manually specify which field to use for an image, simply override 190 | the ``get_image()`` method on your provider. This method returns an instance 191 | of an ImageField:: 192 | 193 | class EntryProvider(DjangoProvider): 194 | # ... meta, etc ... 195 | 196 | def get_image(self, obj): 197 | return obj.primary_photo 198 | -------------------------------------------------------------------------------- /doc/djangoembed/spec.rst: -------------------------------------------------------------------------------- 1 | The OEmbed Spec 2 | =============== 3 | 4 | The full spec is available at http://www.oembed.com - this overview will touch 5 | on everything without going into too much detail to get you up and running with 6 | OEmbed quickly! 7 | 8 | What is OEmbed? 9 | --------------- 10 | 11 | "oEmbed is a format for allowing an embedded representation of a URL on 12 | third party sites. The simple API allows a website to display embedded 13 | content (such as photos or videos) when a user posts a link to that 14 | resource, without having to parse the resource directly." 15 | 16 | What problem does it solve? 17 | --------------------------- 18 | 19 | One of the tasks we as web developers run into a lot is the need to integrate 20 | rich third-party content into our own sites. Numerous REST APIs exist for this 21 | purpose, but suppose we are only concerned with metadata? 22 | 23 | REST APIs make it difficult to extract metadata in a generic way: 24 | * URL structures vary (/statuses/update.json, /users/show.json) 25 | * attribute names are not standardized 26 | * metadata provided is content-dependant (twitter returns tweets, flickr photos) 27 | * authentication can be a pain 28 | * response formats vary 29 | 30 | OEmbed aims at solving these problems by: 31 | * Endpoint lives at one place, like /oembed/json/ 32 | * attribute names are standard, including 'title', 'author', 'thumbnail_url' 33 | * resource types are standard, being 'video', 'photo', 'link', 'rich' 34 | * response format must be JSON or XML 35 | 36 | OEmbed is not a REST API. It is a *READ* API. It allows you to retrieve 37 | metadata about the objects you're interested in, using a single endpoint. 38 | 39 | The best part? **All you need to provide the endpoint is the URL you want 40 | metadata about**. 41 | 42 | An Example 43 | ---------- 44 | 45 | :: 46 | 47 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/ 48 | 49 | 50 | 51 | 1.0 52 | photo 53 | ZB8T0193 54 | ‮‭‬bees‬ 55 | http://www.flickr.com/photos/bees/ 56 | 3600 57 | Flickr 58 | http://www.flickr.com/ 59 | 500 60 | 333 61 | http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf.jpg 62 | 63 | 64 | In the example above, we pass a flickr photo-detail URL to their OEmbed endpoint. 65 | Flickr then returns a wealth of metadata about the object, including the image's 66 | URL, width and height. 67 | 68 | OEmbed endpoints also can accept other arguments, like a **maxwidth**, or **format**: 69 | 70 | :: 71 | 72 | curl http://www.flickr.com/services/oembed/?url=http%3A//www.flickr.com/photos/bees/2341623661/\&maxwidth=300\&format=json 73 | 74 | { 75 | "version":"1.0", 76 | "type":"photo", 77 | "title":"ZB8T0193", 78 | "author_name":"\u202e\u202d\u202cbees\u202c", 79 | "author_url":"http:\/\/www.flickr.com\/photos\/bees\/", 80 | "cache_age":3600, 81 | "provider_name":"Flickr", 82 | "provider_url":"http:\/\/www.flickr.com\/", 83 | "width":"240", 84 | "height":"160", 85 | "url":"http:\/\/farm4.static.flickr.com\/3123\/2341623661_7c99f48bbf_m.jpg" 86 | } 87 | 88 | As you can see from the response, the returned image width is now 240. If a 89 | maximum width (or height) is specified, the provider must respect that. 90 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. djangoembed documentation master file, created by 2 | sphinx-quickstart on Sat Feb 13 14:41:46 2010. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to djangoembed's documentation! 7 | ======================================= 8 | 9 | Why OEmbed? To add rich content to your sites *painlessly* 10 | 11 | Contents: 12 | 13 | .. toctree:: 14 | :maxdepth: 3 15 | :glob: 16 | 17 | djangoembed/* 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | -------------------------------------------------------------------------------- /oembed/__init__.py: -------------------------------------------------------------------------------- 1 | # .--. 2 | # /( @ > ,-. DJANGOEMBED 3 | # / ' .'--._/ / 4 | # : , , .' 5 | # '. (___.'_/ 6 | # ((-((-'' 7 | VERSION = (0, 1, 1) 8 | 9 | from oembed.sites import site 10 | 11 | 12 | def autodiscover(): 13 | """ 14 | Automatically build the provider index. 15 | """ 16 | import imp 17 | from django.conf import settings 18 | 19 | for app in settings.INSTALLED_APPS: 20 | try: 21 | app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__ 22 | except AttributeError: 23 | continue 24 | 25 | try: 26 | imp.find_module('oembed_providers', app_path) 27 | except ImportError: 28 | continue 29 | 30 | __import__("%s.oembed_providers" % app) 31 | -------------------------------------------------------------------------------- /oembed/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from oembed.models import StoredProvider, StoredOEmbed 3 | 4 | 5 | class StoredProviderAdmin(admin.ModelAdmin): 6 | list_display = ('wildcard_regex', 'endpoint_url', 'active', 'provides') 7 | list_filter = ('active', 'provides') 8 | 9 | actions = ['activate', 'deactivate'] 10 | 11 | def activate(self, request, queryset): 12 | for item in queryset: 13 | item.active = True 14 | item.save() 15 | activate.short_description = "Activate selected Stored Providers" 16 | 17 | def deactivate(self, request, queryset): 18 | for item in queryset: 19 | item.active = False 20 | item.save() 21 | deactivate.short_description = "Deactivate selected Stored Providers" 22 | 23 | 24 | class StoredOEmbedAdmin(admin.ModelAdmin): 25 | list_display = ('match', 'date_added') 26 | 27 | 28 | admin.site.register(StoredProvider, StoredProviderAdmin) 29 | admin.site.register(StoredOEmbed, StoredOEmbedAdmin) 30 | -------------------------------------------------------------------------------- /oembed/constants.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.conf import settings 4 | 5 | 6 | # the oembed consumer can work with different parsers! 7 | OEMBED_TEXT_PARSER = getattr(settings, 'OEMBED_TEXT_PARSER', 'oembed.parsers.text.TextParser') 8 | OEMBED_HTML_PARSER = getattr(settings, 'OEMBED_HTML_PARSER', 'oembed.parsers.html.HTMLParser') 9 | 10 | 11 | # the oembed image processor supports different backends! 12 | OEMBED_IMAGE_PROCESSOR = getattr(settings, 'OEMBED_IMAGE_PROCESSOR', 'oembed.image_processors.pil.PIL_Resizer') 13 | 14 | 15 | # oembed-ed objects can specify a TTL, after which they should be re-fetched 16 | # from the providing site. these settings allow you to control both the 17 | # minimum amount of time to store an oembed and a default in the event that 18 | # the provider does not supply a TTL 19 | DEFAULT_OEMBED_TTL = getattr(settings, 'DEFAULT_OEMBED_TTL', 604800) # 7 days 20 | MIN_OEMBED_TTL = getattr(settings, 'MIN_OEMBED_TTL', 86400) # 1 day 21 | 22 | 23 | # the oembed spec defines 4 resource types 24 | RESOURCE_PHOTO = 'photo' 25 | RESOURCE_VIDEO = 'video' 26 | RESOURCE_RICH = 'rich' 27 | RESOURCE_LINK = 'link' 28 | RESOURCE_TYPES = ( 29 | RESOURCE_PHOTO, 30 | RESOURCE_VIDEO, 31 | RESOURCE_RICH, 32 | RESOURCE_LINK, 33 | ) 34 | RESOURCE_CHOICES = ( 35 | (RESOURCE_PHOTO, 'Photo'), 36 | (RESOURCE_VIDEO, 'Video'), 37 | (RESOURCE_RICH, 'Rich'), 38 | (RESOURCE_LINK, 'Link'), 39 | ) 40 | 41 | 42 | # url for matching inline urls, which is a fairly tricky business 43 | URL_PATTERN = '(https?://[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_|])' 44 | URL_RE = re.compile(URL_PATTERN) 45 | STANDALONE_URL_RE = re.compile('^\s*' + URL_PATTERN + '\s*$') 46 | 47 | 48 | # oembed can parse HTML! 49 | OEMBED_DEFAULT_PARSE_HTML = getattr(settings, 'OEMBED_DEFAULT_PARSE_HTML', True) 50 | CONSUMER_URLIZE_ALL = getattr(settings, 'CONSUMER_URLIZE_ALL', True) 51 | 52 | 53 | OEMBED_BLOCK_ELEMENTS = [ 54 | 'address', 'blockquote', 'center', 'dir', 'div', 'dl', 'fieldset', 'form', 55 | 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'isindex', 'menu', 'noframes', 56 | 'noscript', 'ol', 'p', 'pre', 'table', 'ul', 'dd', 'dt', 'frameset', 'li', 57 | 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'button', 'del', 'iframe', 58 | 'ins', 'map', 'object', 'script', '[document]' 59 | ] 60 | 61 | 62 | # some default sizes to use for scaling 63 | OEMBED_ALLOWED_SIZES = getattr(settings, 'OEMBED_ALLOWED_SIZES', [(x, x) for x in xrange(100, 900, 100)]) 64 | OEMBED_THUMBNAIL_SIZE = getattr(settings, 'OEMBED_THUMBNAIL_SIZE', ((200, 200),)) 65 | 66 | SOCKET_TIMEOUT = getattr(settings, 'SOCKET_TIMEOUT', 5) 67 | 68 | 69 | # regex for extracting domain names 70 | DOMAIN_RE = re.compile('((https?://)[^/]+)*') 71 | -------------------------------------------------------------------------------- /oembed/consumer.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import oembed 4 | from oembed.constants import OEMBED_DEFAULT_PARSE_HTML, URL_RE 5 | from oembed.exceptions import OEmbedException 6 | from oembed.parsers import text_parser, html_parser 7 | 8 | 9 | class OEmbedConsumer(object): 10 | def parse(self, text, *args, **kwargs): 11 | if OEMBED_DEFAULT_PARSE_HTML: 12 | return self.parse_html(text, *args, **kwargs) 13 | else: 14 | return self.parse_text(text, *args, **kwargs) 15 | 16 | def parse_html(self, text, *args, **kwargs): 17 | parser = html_parser() 18 | return parser.parse(text, *args, **kwargs) 19 | 20 | def parse_text(self, text, *args, **kwargs): 21 | parser = text_parser() 22 | return parser.parse(text, *args, **kwargs) 23 | 24 | def extract(self, text, *args, **kwargs): 25 | if OEMBED_DEFAULT_PARSE_HTML: 26 | return self.extract_oembeds_html(text, *args, **kwargs) 27 | else: 28 | return self.extract_oembeds(text, *args, **kwargs) 29 | 30 | def extract_oembeds(self, text, maxwidth=None, maxheight=None, resource_type=None): 31 | """ 32 | Scans a block of text and extracts oembed data on any urls, 33 | returning it in a list of dictionaries 34 | """ 35 | parser = text_parser() 36 | urls = parser.extract_urls(text) 37 | return self.handle_extracted_urls(urls, maxwidth, maxheight, resource_type) 38 | 39 | def extract_oembeds_html(self, text, maxwidth=None, maxheight=None, resource_type=None): 40 | parser = html_parser() 41 | urls = parser.extract_urls(text) 42 | return self.handle_extracted_urls(urls, maxwidth, maxheight, resource_type) 43 | 44 | def handle_extracted_urls(self, url_set, maxwidth=None, maxheight=None, resource_type=None): 45 | embeds = [] 46 | 47 | for user_url in url_set: 48 | try: 49 | resource = oembed.site.embed(user_url, maxwidth=maxwidth, maxheight=maxheight) 50 | except OEmbedException: 51 | continue 52 | else: 53 | if not resource_type or resource.type == resource_type: 54 | data = resource.get_data() 55 | data['original_url'] = user_url 56 | embeds.append(data) 57 | 58 | return embeds 59 | 60 | def strip(self, text, *args, **kwargs): 61 | """ 62 | Try to maintain parity with what is extracted by extract since strip 63 | will most likely be used in conjunction with extract 64 | """ 65 | if OEMBED_DEFAULT_PARSE_HTML: 66 | extracted = self.extract_oembeds_html(text, *args, **kwargs) 67 | else: 68 | extracted = self.extract_oembeds(text, *args, **kwargs) 69 | 70 | matches = [r['original_url'] for r in extracted] 71 | match_handler = lambda m: m.group() not in matches and m.group() or '' 72 | 73 | return re.sub(URL_RE, match_handler, text) 74 | -------------------------------------------------------------------------------- /oembed/contrib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcompany/djangoembed/f3f2be283441d91d1f89db780444dc75f7b51902/oembed/contrib/__init__.py -------------------------------------------------------------------------------- /oembed/contrib/models.py: -------------------------------------------------------------------------------- 1 | # ,___, 2 | # (6v6) 3 | # (_^(_\ 4 | # ^^^"^" \\^^^^ 5 | # ^^^^^^^^^^^^^ 6 | -------------------------------------------------------------------------------- /oembed/contrib/oembed_providers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | try: 4 | from cStringIO import StringIO 5 | except ImportError: 6 | from StringIO import StringIO 7 | try: 8 | import Image 9 | except ImportError: 10 | from PIL import Image 11 | 12 | from django.conf import settings 13 | from django.contrib.sites.models import Site 14 | from django.core.files.base import ContentFile 15 | from django.core.files.storage import default_storage 16 | 17 | import oembed 18 | from oembed.providers import BaseProvider 19 | from oembed.resources import OEmbedResource 20 | from oembed.utils import size_to_nearest, scale 21 | 22 | 23 | class GoogleMapsProvider(BaseProvider): 24 | regex = r'^http://maps.google.com/maps\?([^\s]+)' 25 | provides = False 26 | resource_type = 'rich' 27 | 28 | MAP_SIZES = [(x, x) for x in xrange(100, 900, 100)] 29 | VALID_PARAMS = ['q', 'z'] 30 | 31 | def request_resource(self, url, **kwargs): 32 | maxwidth = kwargs.get('maxwidth', None) 33 | maxheight = kwargs.get('maxheight', None) 34 | 35 | # calculate the appropriate width and height 36 | w, h = size_to_nearest(maxwidth, maxheight, self.MAP_SIZES, True) 37 | 38 | # prepare the dictionary of data to be returned as an oembed resource 39 | data = { 40 | 'type': 'rich', 'provider_name': 'Google', 'version': '1.0', 41 | 'width': w, 'height': h, 'title': '', 'author_name': '', 42 | 'author_url': '' 43 | } 44 | 45 | url_params = re.match(self.regex, url).groups()[0] 46 | url_params = url_params.replace('&', '&').split('&') 47 | 48 | map_params = ['output=embed'] 49 | 50 | for param in url_params: 51 | k, v = param.split('=', 1) 52 | if k in self.VALID_PARAMS: 53 | map_params.append(param) 54 | 55 | data['html'] = '' % \ 56 | (w, h, '&'.join(map_params)) 57 | 58 | return OEmbedResource.create(data) 59 | 60 | 61 | class StaticMediaProvider(BaseProvider): 62 | media_url = settings.MEDIA_URL.strip('/') 63 | if not media_url.startswith('http'): 64 | all_domains = Site.objects.values_list('domain', flat=True) 65 | media_url = 'http://[^\s]*?(?:%s)/%s' % ('|'.join(all_domains), media_url) 66 | 67 | regex = re.compile(r'^%s/([^\s]+\.(jpg|gif|png))' % media_url, re.I) 68 | provides = False 69 | resource_type = 'photo' 70 | 71 | IMAGE_SIZES = [(x, x) for x in xrange(100, 900, 100)] 72 | 73 | def request_resource(self, url, **kwargs): 74 | maxwidth = kwargs.get('maxwidth', None) 75 | maxheight = kwargs.get('maxheight', None) 76 | 77 | # calculate the appropriate bounds for width and height 78 | w, h = size_to_nearest(maxwidth, maxheight, self.IMAGE_SIZES, True) 79 | 80 | # get the path, i.e. /media/img/kitties.jpg 81 | image_path = re.match(self.regex, url).groups()[0] 82 | 83 | # create the entire url as it would be on site, minus the filename 84 | base_url, ext = url.rsplit('.', 1) 85 | 86 | # create the file path minus the extension 87 | base_path, ext = image_path.rsplit('.', 1) 88 | 89 | append = '_%sx%s.%s' % (w, h, ext) 90 | 91 | new_path = '%s%s' % (base_path, append) 92 | 93 | if not default_storage.exists(new_path): 94 | # open the original to calculate its width and height 95 | source_file = default_storage.open(image_path) 96 | img = Image.open(source_file) 97 | 98 | # retrieve image format and dimensions 99 | format = img.format 100 | img_width, img_height = img.size 101 | 102 | # do the math-y parts 103 | new_width, new_height = scale(img_width, img_height, w, h) 104 | 105 | img = img.resize((new_width, new_height), Image.ANTIALIAS) 106 | 107 | img_buffer = StringIO() 108 | img.MAXBLOCK = 1024*1024 109 | img.save(img_buffer, format=format) 110 | 111 | source_file.close() 112 | default_storage.save(new_path, ContentFile(img_buffer.getvalue())) 113 | 114 | new_url = '%s%s' % (base_url, append) 115 | 116 | # get just the filename, i.e. test.jpg - used for generated the title 117 | # of the returned oembed resource 118 | image_filename = image_path.rsplit('/', 1)[-1] 119 | 120 | data = {'type': 'photo', 'provider_name': '', 'version': '1.0', 121 | 'width': w, 'height': h, 'title': image_filename, 122 | 'url': new_url, 'author_name': '', 'author_url': ''} 123 | 124 | return OEmbedResource.create(data) 125 | 126 | 127 | oembed.site.register(GoogleMapsProvider) 128 | oembed.site.register(StaticMediaProvider) 129 | -------------------------------------------------------------------------------- /oembed/exceptions.py: -------------------------------------------------------------------------------- 1 | class OEmbedException(Exception): 2 | pass 3 | 4 | class OEmbedInvalidResource(OEmbedException): 5 | pass 6 | 7 | class OEmbedMissingEndpoint(OEmbedException): 8 | pass 9 | 10 | class OEmbedBadRequest(OEmbedException): 11 | pass 12 | 13 | class OEmbedHTTPException(OEmbedException): 14 | pass 15 | 16 | class AlreadyRegistered(OEmbedException): 17 | """Raised when a model is already registered with a site.""" 18 | pass 19 | 20 | class NotRegistered(OEmbedException): 21 | """Raised when a model is not registered with a site.""" 22 | pass 23 | -------------------------------------------------------------------------------- /oembed/fields.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.db import models 4 | from django.db.models import signals, Max 5 | from django.db.models.fields import TextField 6 | 7 | import oembed 8 | from oembed.constants import URL_RE 9 | from oembed.exceptions import OEmbedMissingEndpoint 10 | from oembed.models import AggregateMedia 11 | 12 | 13 | class FieldRegistry(object): 14 | """ 15 | FieldRegistry -> modified Borg pattern from Marty Alchin's Pro Django 16 | """ 17 | _registry = {} 18 | 19 | @classmethod 20 | def add_field(cls, model, field): 21 | reg = cls._registry.setdefault(model, []) 22 | reg.append(field) 23 | 24 | @classmethod 25 | def get_fields(cls, model): 26 | return cls._registry.get(model, []) 27 | 28 | @classmethod 29 | def __contains__(cls, model): 30 | return model in cls._registry 31 | 32 | 33 | class EmbeddedSignalCreator(object): 34 | def __init__(self, field): 35 | self.field = field 36 | self.name = '_%s' % self.field.name 37 | 38 | def contribute_to_class(self, cls, name): 39 | register_field(cls, self.field) 40 | 41 | 42 | class EmbeddedMediaField(models.ManyToManyField): 43 | def __init__(self, media_type=None, to=None, **kwargs): 44 | if media_type and not isinstance(media_type, (basestring, list)): 45 | raise TypeError('media_type must be either a list or string') 46 | elif isinstance(media_type, basestring): 47 | media_type = [media_type] 48 | self.media_type = media_type 49 | 50 | super(EmbeddedMediaField, self).__init__(AggregateMedia, **kwargs) 51 | 52 | def contribute_to_class(self, cls, name): 53 | """ 54 | I need a way to ensure that this signal gets created for all child 55 | models, and since model inheritance doesn't have a 'contrubite_to_class' 56 | style hook, I am creating a fake virtual field which will be added to 57 | all subclasses and handles creating the signal 58 | """ 59 | super(EmbeddedMediaField, self).contribute_to_class(cls, name) 60 | register_field(cls, self) 61 | 62 | # add a virtual field that will create signals on any/all subclasses 63 | cls._meta.add_virtual_field(EmbeddedSignalCreator(self)) 64 | 65 | 66 | def register_field(cls, field): 67 | """ 68 | Handles registering the fields with the FieldRegistry and creating a 69 | post-save signal for the model. 70 | """ 71 | FieldRegistry.add_field(cls, field) 72 | 73 | signals.post_save.connect(handle_save_embeds, sender=cls, 74 | dispatch_uid='%s.%s.%s' % \ 75 | (cls._meta.app_label, cls._meta.module_name, field.name)) 76 | 77 | 78 | def handle_save_embeds(sender, instance, **kwargs): 79 | embedded_media_fields = FieldRegistry.get_fields(sender) 80 | if not embedded_media_fields: 81 | return 82 | 83 | urls = [] 84 | for field in instance._meta.fields: 85 | if isinstance(field, TextField): 86 | urls.extend(re.findall(URL_RE, getattr(instance, field.name))) 87 | 88 | urls = set(urls) 89 | for embedded_field in embedded_media_fields: 90 | m2m = getattr(instance, embedded_field.name) 91 | m2m.clear() 92 | for url in urls: 93 | try: 94 | provider = oembed.site.provider_for_url(url) 95 | except OEmbedMissingEndpoint: 96 | pass 97 | else: 98 | if not embedded_field.media_type or \ 99 | provider.resource_type in embedded_field.media_type: 100 | media_obj, created = AggregateMedia.objects.get_or_create(url=url) 101 | m2m.add(media_obj) 102 | 103 | try: 104 | from south.modelsinspector import add_introspection_rules 105 | add_introspection_rules([ 106 | ( 107 | [EmbeddedMediaField], # Class(es) these apply to 108 | [], # Positional arguments (not used) 109 | { # Keyword argument 110 | "media_type": ["media_type", {}], 111 | }, 112 | ), 113 | ], ["^oembed\.fields\.EmbeddedMediaField"]) 114 | 115 | except ImportError: 116 | pass 117 | -------------------------------------------------------------------------------- /oembed/image_processors/__init__.py: -------------------------------------------------------------------------------- 1 | from oembed.constants import OEMBED_IMAGE_PROCESSOR 2 | from oembed.utils import load_class 3 | 4 | 5 | image_processor = load_class(OEMBED_IMAGE_PROCESSOR) 6 | -------------------------------------------------------------------------------- /oembed/image_processors/pil.py: -------------------------------------------------------------------------------- 1 | import os 2 | try: 3 | from cStringIO import StringIO 4 | except ImportError: 5 | from StringIO import StringIO 6 | try: 7 | import Image 8 | except ImportError: 9 | from PIL import Image 10 | 11 | from django.core.files.base import ContentFile 12 | from django.core.files.storage import default_storage 13 | 14 | 15 | class PIL_Resizer(object): 16 | def resize(self, image_field, new_width, new_height): 17 | img_path = image_field.name 18 | base_path, img_ext = os.path.splitext(image_field.name) 19 | base_url, img_ext = os.path.splitext(image_field.url) 20 | 21 | append = '_%sx%s%s' % (new_width, new_height, img_ext) 22 | 23 | new_url = base_url + append 24 | new_path = base_path + append 25 | 26 | if not default_storage.exists(new_path): 27 | # load a file-like object at the image path 28 | source_file = default_storage.open(img_path) 29 | 30 | # load up the image using PIL and retrieve format 31 | img = Image.open(source_file) 32 | format = img.format 33 | 34 | # perform resize 35 | img = img.resize((new_width, new_height), Image.ANTIALIAS) 36 | 37 | # create a file-like object to store resized data 38 | img_buffer = StringIO() 39 | img.MAXBLOCK = 1024*1024 40 | img.save(img_buffer, format=format) 41 | 42 | # save data 43 | default_storage.save(new_path, ContentFile(img_buffer.getvalue())) 44 | 45 | return (new_url, new_width, new_height) 46 | -------------------------------------------------------------------------------- /oembed/listeners.py: -------------------------------------------------------------------------------- 1 | from django.db.models.signals import post_save 2 | 3 | import oembed 4 | from oembed.models import StoredProvider 5 | 6 | def provider_site_invalidate(sender, instance, created, **kwargs): 7 | oembed.site.invalidate_providers() 8 | 9 | def start_listening(): 10 | post_save.connect(provider_site_invalidate, sender=StoredProvider) 11 | -------------------------------------------------------------------------------- /oembed/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | import datetime 3 | 4 | from south.db import db 5 | from south.v2 import SchemaMigration 6 | 7 | from django.conf import settings 8 | from django.db import models 9 | 10 | class Migration(SchemaMigration): 11 | 12 | def forwards(self, orm): 13 | 14 | # Adding model 'StoredOEmbed' 15 | db.create_table('oembed_storedoembed', ( 16 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 17 | ('match', self.gf('django.db.models.fields.TextField')()), 18 | ('response_json', self.gf('django.db.models.fields.TextField')()), 19 | ('resource_type', self.gf('django.db.models.fields.CharField')(max_length=8)), 20 | ('date_added', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), 21 | ('date_expires', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)), 22 | ('maxwidth', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 23 | ('maxheight', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 24 | ('object_id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 25 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='related_storedoembed', null=True, to=orm['contenttypes.ContentType'])), 26 | )) 27 | db.send_create_signal('oembed', ['StoredOEmbed']) 28 | 29 | # Adding unique constraint on 'StoredOEmbed', fields ['match', 'maxwidth', 'maxheight'] 30 | if 'mysql' not in settings.DATABASES['default']['ENGINE']: 31 | db.create_unique('oembed_storedoembed', ['match', 'maxwidth', 'maxheight']) 32 | 33 | # Adding model 'StoredProvider' 34 | db.create_table('oembed_storedprovider', ( 35 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 36 | ('endpoint_url', self.gf('django.db.models.fields.CharField')(max_length=255)), 37 | ('regex', self.gf('django.db.models.fields.CharField')(max_length=255)), 38 | ('wildcard_regex', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), 39 | ('resource_type', self.gf('django.db.models.fields.CharField')(max_length=8)), 40 | ('active', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 41 | ('provides', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True)), 42 | ('scheme_url', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)), 43 | )) 44 | db.send_create_signal('oembed', ['StoredProvider']) 45 | 46 | # Adding model 'AggregateMedia' 47 | db.create_table('oembed_aggregatemedia', ( 48 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), 49 | ('url', self.gf('django.db.models.fields.TextField')()), 50 | ('object_id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)), 51 | ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='aggregate_media', null=True, to=orm['contenttypes.ContentType'])), 52 | )) 53 | db.send_create_signal('oembed', ['AggregateMedia']) 54 | 55 | 56 | def backwards(self, orm): 57 | 58 | # Deleting model 'StoredOEmbed' 59 | db.delete_table('oembed_storedoembed') 60 | 61 | # Removing unique constraint on 'StoredOEmbed', fields ['match', 'maxwidth', 'maxheight'] 62 | if 'mysql' not in settings.DATABASES['default']['ENGINE']: 63 | db.delete_unique('oembed_storedoembed', ['match', 'maxwidth', 'maxheight']) 64 | 65 | # Deleting model 'StoredProvider' 66 | db.delete_table('oembed_storedprovider') 67 | 68 | # Deleting model 'AggregateMedia' 69 | db.delete_table('oembed_aggregatemedia') 70 | 71 | 72 | models = { 73 | 'contenttypes.contenttype': { 74 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 75 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 76 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 77 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), 78 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) 79 | }, 80 | 'oembed.aggregatemedia': { 81 | 'Meta': {'object_name': 'AggregateMedia'}, 82 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'aggregate_media'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), 83 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 84 | 'object_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 85 | 'url': ('django.db.models.fields.TextField', [], {}) 86 | }, 87 | 'oembed.storedoembed': { 88 | 'Meta': {'ordering': "('-date_added',)", 'unique_together': "(('match', 'maxwidth', 'maxheight'),)", 'object_name': 'StoredOEmbed'}, 89 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'related_storedoembed'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}), 90 | 'date_added': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 91 | 'date_expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), 92 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 93 | 'match': ('django.db.models.fields.TextField', [], {}), 94 | 'maxheight': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 95 | 'maxwidth': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 96 | 'object_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), 97 | 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '8'}), 98 | 'response_json': ('django.db.models.fields.TextField', [], {}) 99 | }, 100 | 'oembed.storedprovider': { 101 | 'Meta': {'ordering': "('endpoint_url', 'resource_type', 'wildcard_regex')", 'object_name': 'StoredProvider'}, 102 | 'active': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 103 | 'endpoint_url': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 104 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 105 | 'provides': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}), 106 | 'regex': ('django.db.models.fields.CharField', [], {'max_length': '255'}), 107 | 'resource_type': ('django.db.models.fields.CharField', [], {'max_length': '8'}), 108 | 'scheme_url': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), 109 | 'wildcard_regex': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}) 110 | } 111 | } 112 | 113 | complete_apps = ['oembed'] 114 | -------------------------------------------------------------------------------- /oembed/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcompany/djangoembed/f3f2be283441d91d1f89db780444dc75f7b51902/oembed/migrations/__init__.py -------------------------------------------------------------------------------- /oembed/models.py: -------------------------------------------------------------------------------- 1 | from django import VERSION 2 | from django.conf import settings 3 | from django.contrib.contenttypes.generic import GenericForeignKey 4 | from django.contrib.contenttypes.models import ContentType 5 | from django.db import models 6 | from django.utils import simplejson 7 | 8 | from oembed.constants import RESOURCE_CHOICES 9 | from oembed.providers import HTTPProvider 10 | 11 | 12 | if VERSION < (1, 2): 13 | db_engine = settings.DATABASE_ENGINE 14 | else: 15 | db_engine = settings.DATABASES['default']['ENGINE'] 16 | 17 | 18 | class StoredOEmbed(models.Model): 19 | match = models.TextField() 20 | response_json = models.TextField() 21 | resource_type = models.CharField(choices=RESOURCE_CHOICES, editable=False, max_length=8) 22 | date_added = models.DateTimeField(auto_now_add=True) 23 | date_expires = models.DateTimeField(blank=True, null=True) 24 | maxwidth = models.IntegerField(blank=True, null=True) 25 | maxheight = models.IntegerField(blank=True, null=True) 26 | 27 | # generic bits 28 | object_id = models.IntegerField(blank=True, null=True) 29 | content_type = models.ForeignKey(ContentType, blank=True, null=True, 30 | related_name="related_%(class)s") 31 | content_object = GenericForeignKey() 32 | 33 | class Meta: 34 | ordering = ('-date_added',) 35 | verbose_name = 'stored OEmbed' 36 | verbose_name_plural = 'stored OEmbeds' 37 | if 'mysql' not in db_engine: 38 | unique_together = ('match', 'maxwidth', 'maxheight') 39 | 40 | def __unicode__(self): 41 | return self.match 42 | 43 | @property 44 | def response(self): 45 | return simplejson.loads(self.response_json) 46 | 47 | 48 | class StoredProviderManager(models.Manager): 49 | def active(self): 50 | return self.filter(active=True) 51 | 52 | class StoredProvider(models.Model, HTTPProvider): 53 | """ 54 | Essentially, a stored proxy provider that mimics the interface of a python 55 | HTTPProvider - used for autodiscovery 56 | """ 57 | endpoint_url = models.CharField(max_length=255) 58 | regex = models.CharField(max_length=255) 59 | wildcard_regex = models.CharField(max_length=255, blank=True, 60 | help_text='Wild-card version of regex') 61 | resource_type = models.CharField(choices=RESOURCE_CHOICES, max_length=8) 62 | active = models.BooleanField(default=False) 63 | provides = models.BooleanField(default=False, help_text='Provide upstream') 64 | scheme_url = models.CharField(max_length=255, blank=True) 65 | 66 | objects = StoredProviderManager() 67 | 68 | class Meta: 69 | ordering = ('endpoint_url', 'resource_type', 'wildcard_regex') 70 | 71 | def __unicode__(self): 72 | return self.wildcard_regex 73 | 74 | def save(self, *args, **kwargs): 75 | if not self.regex: 76 | # convert wildcard to non-greedy 'match anything' regex 77 | self.regex = self.wildcard_regex.replace('*', '.+?') 78 | super(StoredProvider, self).save(*args, **kwargs) 79 | 80 | @property 81 | def url_scheme(self): 82 | if self.provides and self.wildcard_regex: 83 | return self.wildcard_regex 84 | 85 | 86 | class AggregateMediaDescriptor(property): 87 | def contribute_to_class(self, cls, name): 88 | self.name = name 89 | setattr(cls, self.name, self) 90 | 91 | def __get__(self, instance, cls=None): 92 | if instance.content_object: 93 | return instance.content_object 94 | try: 95 | import oembed 96 | resource = oembed.site.embed(instance.url) 97 | if resource.content_object: 98 | instance.content_object = resource.content_object 99 | instance.save() 100 | return instance.content_object 101 | else: 102 | stored_oembed = StoredOEmbed.objects.filter( 103 | match=instance.url)[0] 104 | return stored_oembed 105 | except: 106 | pass 107 | 108 | def __set__(self, instance, value): 109 | raise NotImplementedError('%s is read-only' % self.name) 110 | 111 | 112 | class AggregateMedia(models.Model): 113 | url = models.TextField() 114 | object_id = models.IntegerField(blank=True, null=True) 115 | content_type = models.ForeignKey(ContentType, blank=True, null=True, 116 | related_name="aggregate_media") 117 | content_object = GenericForeignKey() 118 | 119 | media = AggregateMediaDescriptor() 120 | 121 | def __unicode__(self): 122 | return self.url 123 | 124 | def get_absolute_url(self): 125 | if self.content_object and hasattr(self.content_object, 'get_absolute_url'): 126 | return self.content_object.get_absolute_url() 127 | return self.url 128 | 129 | 130 | from oembed.listeners import start_listening 131 | start_listening() 132 | -------------------------------------------------------------------------------- /oembed/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from oembed.constants import OEMBED_TEXT_PARSER, OEMBED_HTML_PARSER 2 | from oembed.utils import load_class 3 | 4 | 5 | text_parser = load_class(OEMBED_TEXT_PARSER) 6 | html_parser = load_class(OEMBED_HTML_PARSER) 7 | -------------------------------------------------------------------------------- /oembed/parsers/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.template import RequestContext, Context 4 | from django.template.loader import render_to_string, select_template 5 | 6 | from oembed.constants import CONSUMER_URLIZE_ALL 7 | from oembed.utils import mock_request 8 | 9 | 10 | class BaseParser(object): 11 | def render_oembed(self, oembed_resource, original_url, template_dir=None, 12 | context=None): 13 | """ 14 | Render the oembed resource and return as a string. 15 | 16 | Template directory will always fall back to 'oembed/[type].html', but 17 | a custom template dir can be passed in using the kwargs. 18 | 19 | Templates are given two context variables: 20 | - response: an OEmbedResource 21 | - original_url: the url that was passed to the consumer 22 | """ 23 | provided_context = context or Context() 24 | context = RequestContext(context.get("request") or mock_request()) 25 | context.update(provided_context) 26 | 27 | # templates are named for the resources they display, i.e. video.html 28 | template_name = '%s.html' % oembed_resource.type 29 | 30 | # set up template finder to fall back to the link template 31 | templates = [os.path.join('oembed', template_name), 'oembed/link.html'] 32 | 33 | # if there's a custom template dir, look there first 34 | if template_dir: 35 | templates.insert(0, os.path.join('oembed', template_dir, template_name)) 36 | 37 | template = select_template(templates) 38 | 39 | context.push() 40 | context['response'] = oembed_resource 41 | context['original_url'] = original_url 42 | rendered = template.render(context) 43 | context.pop() 44 | 45 | return rendered.strip() # rendering template may add whitespace 46 | 47 | def parse(self, text, maxwidth=None, maxheight=None, template_dir=None, 48 | context=None, urlize_all_links=CONSUMER_URLIZE_ALL): 49 | """ 50 | Scans a block of text, replacing anything matching a provider pattern 51 | with an OEmbed html snippet, if possible. 52 | 53 | Templates should be stored at oembed/{format}.html, so for example: 54 | 55 | oembed/video.html 56 | 57 | An optional template_dir can be provided, allowing for 58 | 59 | oembed/[template_dir]/video.html 60 | 61 | These templates are passed a context variable, ``response``, which is 62 | an OEmbedResource, as well as the ``original_url`` 63 | """ 64 | context = context or Context() 65 | context['maxwidth'] = maxwidth 66 | context['maxheight'] = maxheight 67 | 68 | try: 69 | text = unicode(text) 70 | except UnicodeDecodeError: 71 | text = unicode(text.decode('utf-8')) 72 | 73 | return self.parse_data(text, maxwidth, maxheight, template_dir, 74 | context, urlize_all_links) 75 | 76 | def parse_data(self, text, maxwidth, maxheight, template_dir, context, 77 | urlize_all_links): 78 | """ 79 | Implemented on all subclasses, this method contains the logic to 80 | process embeddable content in ``text`` 81 | """ 82 | raise NotImplementedError('Subclasses must define a parse_data method') 83 | 84 | def extract_urls(self, text): 85 | """ 86 | Implemented on all subclasses, this method contains the logic to 87 | extract a list or set of urls from text 88 | """ 89 | raise NotImplementedError('Subclasses must define a extract_urls method') 90 | -------------------------------------------------------------------------------- /oembed/parsers/html.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from BeautifulSoup import BeautifulSoup # use BS to parse HTML (it's easy!) 4 | 5 | import oembed 6 | from oembed.constants import OEMBED_BLOCK_ELEMENTS, URL_RE, STANDALONE_URL_RE 7 | from oembed.exceptions import OEmbedException 8 | from oembed.parsers.base import BaseParser 9 | from oembed.parsers.text import TextParser, TextBlockParser 10 | 11 | 12 | class HTMLParser(BaseParser): 13 | """ 14 | Use BeautifulSoup for easy html processing. 15 | """ 16 | def parse_data(self, text, maxwidth, maxheight, template_dir, context, 17 | urlize_all_links): 18 | block_parser = TextBlockParser() 19 | original_template_dir = template_dir 20 | 21 | soup = BeautifulSoup(text) 22 | 23 | for user_url in soup.findAll(text=re.compile(URL_RE)): 24 | if not self.inside_a(user_url): 25 | if self.is_standalone(user_url): 26 | template_dir = original_template_dir 27 | else: 28 | template_dir = 'inline' 29 | 30 | replacement = block_parser.parse( 31 | str(user_url), 32 | maxwidth, 33 | maxheight, 34 | template_dir, 35 | context, 36 | urlize_all_links 37 | ) 38 | user_url.replaceWith(replacement) 39 | 40 | return unicode(soup) 41 | 42 | def is_standalone(self, soupie): 43 | if re.match(STANDALONE_URL_RE, soupie): 44 | if soupie.parent.name in OEMBED_BLOCK_ELEMENTS: 45 | return True 46 | return False 47 | 48 | def inside_a(self, soupie): 49 | parent = soupie.parent 50 | while parent is not None: 51 | if parent.name == 'a': 52 | return True 53 | parent = parent.parent 54 | return False 55 | 56 | def extract_urls(self, text): 57 | block_parser = TextBlockParser() 58 | soup = BeautifulSoup(text) 59 | urls = set() 60 | url_list = [] 61 | 62 | for user_url in soup.findAll(text=re.compile(URL_RE)): 63 | if not self.inside_a(user_url): 64 | block_urls = block_parser.extract_urls(unicode(user_url)) 65 | 66 | for url in block_urls: 67 | if url not in urls: 68 | url_list.append(url) 69 | urls.add(url) 70 | 71 | return url_list 72 | -------------------------------------------------------------------------------- /oembed/parsers/text.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.utils.safestring import mark_safe 4 | 5 | import oembed 6 | from oembed.constants import URL_RE, STANDALONE_URL_RE 7 | from oembed.exceptions import OEmbedException 8 | from oembed.parsers.base import BaseParser 9 | 10 | 11 | class TextBlockParser(BaseParser): 12 | def parse_data(self, text, maxwidth, maxheight, template_dir, context, 13 | urlize_all_links): 14 | """ 15 | Parses a block of text indiscriminately 16 | """ 17 | # create a dictionary of user urls -> rendered responses 18 | replacements = {} 19 | user_urls = set(re.findall(URL_RE, text)) 20 | 21 | for user_url in user_urls: 22 | try: 23 | resource = oembed.site.embed(user_url, maxwidth=maxwidth, maxheight=maxheight) 24 | except OEmbedException: 25 | if urlize_all_links: 26 | replacements[user_url] = '%(LINK)s' % {'LINK': user_url} 27 | else: 28 | context['minwidth'] = min(maxwidth, resource.width) 29 | context['minheight'] = min(maxheight, resource.height) 30 | 31 | replacement = self.render_oembed( 32 | resource, 33 | user_url, 34 | template_dir=template_dir, 35 | context=context 36 | ) 37 | replacements[user_url] = replacement.strip() 38 | 39 | # go through the text recording URLs that can be replaced 40 | # taking note of their start & end indexes 41 | user_urls = re.finditer(URL_RE, text) 42 | matches = [] 43 | for match in user_urls: 44 | if match.group() in replacements: 45 | matches.append([match.start(), match.end(), match.group()]) 46 | 47 | # replace the URLs in order, offsetting the indices each go 48 | for indx, (start, end, user_url) in enumerate(matches): 49 | replacement = replacements[user_url] 50 | difference = len(replacement) - len(user_url) 51 | 52 | # insert the replacement between two slices of text surrounding the 53 | # original url 54 | text = text[:start] + replacement + text[end:] 55 | 56 | # iterate through the rest of the matches offsetting their indices 57 | # based on the difference between replacement/original 58 | for j in xrange(indx + 1, len(matches)): 59 | matches[j][0] += difference 60 | matches[j][1] += difference 61 | return mark_safe(text) 62 | 63 | def extract_urls(self, text): 64 | urls = set() 65 | url_list = [] 66 | for url in re.findall(URL_RE, text): 67 | if url not in urls: 68 | urls.add(url) 69 | url_list.append(url) 70 | 71 | return url_list 72 | 73 | 74 | class TextParser(TextBlockParser): 75 | def parse_data(self, text, maxwidth, maxheight, template_dir, context, 76 | urlize_all_links): 77 | """ 78 | Parses a block of text rendering links that occur on their own line 79 | normally but rendering inline links using a special template dir 80 | """ 81 | block_parser = TextBlockParser() 82 | 83 | lines = text.splitlines() 84 | parsed = [] 85 | 86 | for line in lines: 87 | if STANDALONE_URL_RE.match(line): 88 | user_url = line.strip() 89 | try: 90 | resource = oembed.site.embed(user_url, maxwidth=maxwidth, maxheight=maxheight) 91 | context['minwidth'] = min(maxwidth, resource.width) 92 | context['minheight'] = min(maxheight, resource.height) 93 | except OEmbedException: 94 | if urlize_all_links: 95 | line = '%(LINK)s' % {'LINK': user_url} 96 | else: 97 | context['minwidth'] = min(maxwidth, resource.width) 98 | context['minheight'] = min(maxheight, resource.height) 99 | 100 | line = self.render_oembed( 101 | resource, 102 | user_url, 103 | template_dir=template_dir, 104 | context=context) 105 | else: 106 | line = block_parser.parse(line, maxwidth, maxheight, 'inline', 107 | context, urlize_all_links) 108 | 109 | parsed.append(line) 110 | 111 | return mark_safe('\n'.join(parsed)) 112 | -------------------------------------------------------------------------------- /oembed/resources.py: -------------------------------------------------------------------------------- 1 | from django.utils import simplejson 2 | 3 | from oembed.exceptions import OEmbedException 4 | 5 | class OEmbedResource(object): 6 | """ 7 | OEmbed resource, as well as a factory for creating resource instances 8 | from response json 9 | """ 10 | _data = {} 11 | content_object = None 12 | 13 | def __getattr__(self, name): 14 | return self._data.get(name) 15 | 16 | def get_data(self): 17 | return self._data 18 | 19 | def load_data(self, data): 20 | self._data = data 21 | 22 | @property 23 | def json(self): 24 | return simplejson.dumps(self._data) 25 | 26 | @classmethod 27 | def create(cls, data): 28 | if not 'type' in data or not 'version' in data: 29 | raise OEmbedException('Missing required fields on OEmbed response.') 30 | 31 | data['width'] = data.get('width') and int(data['width']) or None 32 | data['height'] = data.get('height') and int(data['height']) or None 33 | 34 | filtered_data = dict([(k, v) for k, v in data.items() if v]) 35 | 36 | resource = cls() 37 | resource.load_data(filtered_data) 38 | 39 | return resource 40 | 41 | @classmethod 42 | def create_json(cls, raw): 43 | data = simplejson.loads(raw) 44 | return cls.create(data) 45 | -------------------------------------------------------------------------------- /oembed/templates/oembed/inline/link.html: -------------------------------------------------------------------------------- 1 | {% if response.title %}{{ response.title }}{% else %}{{ original_url }}{% endif %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/inline/photo.html: -------------------------------------------------------------------------------- 1 | {% if response.title %}{{ response.title }}{% else %}{{ original_url }}{% endif %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/inline/rich.html: -------------------------------------------------------------------------------- 1 | {% if response.title %}{{ response.title }}{% else %}{{ original_url }}{% endif %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/inline/video.html: -------------------------------------------------------------------------------- 1 | {% if response.title %}{{ response.title }}{% else %}{{ original_url }}{% endif %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/link.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | {% if response.html %} 3 | {{ response.html }} 4 | {% else %} 5 | {% if response.title %}{{ response.title }}{% else %}{{ response.url }}{% endif %} 6 | {% endif %} 7 | {% endautoescape %} 8 | -------------------------------------------------------------------------------- /oembed/templates/oembed/photo.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{{ response.title }}{% endautoescape %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/rich.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{{ response.html }}{% endautoescape %} 2 | -------------------------------------------------------------------------------- /oembed/templates/oembed/video.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{{ response.html }}{% endautoescape %} 2 | -------------------------------------------------------------------------------- /oembed/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /oembed/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from oembed.tests.tests import * 2 | -------------------------------------------------------------------------------- /oembed/tests/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from oembed.tests.models import Blog, Category 4 | 5 | admin.site.register(Blog) 6 | admin.site.register(Category) 7 | -------------------------------------------------------------------------------- /oembed/tests/fixtures/oembed_testdata.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 1, 4 | "model": "tests.blog", 5 | "fields": { 6 | "content": "This is entry one.", 7 | "author": "Charles", 8 | "pub_date": "2010-05-01 09:30:00", 9 | "slug": "entry-1", 10 | "title": "Entry 1" 11 | } 12 | }, 13 | { 14 | "pk": 2, 15 | "model": "tests.blog", 16 | "fields": { 17 | "content": "This is entry two.", 18 | "author": "Charles", 19 | "pub_date": "2010-05-02 09:30:00", 20 | "slug": "entry-2", 21 | "title": "Entry 2" 22 | } 23 | }, 24 | { 25 | "pk": 1, 26 | "model": "tests.category", 27 | "fields": { 28 | "width": null, 29 | "image": "images/test_image1.jpg", 30 | "name": "Category 1", 31 | "height": null 32 | } 33 | }, 34 | { 35 | "pk": 2, 36 | "model": "tests.category", 37 | "fields": { 38 | "width": null, 39 | "image": "", 40 | "name": "Category 2", 41 | "height": null 42 | } 43 | }, 44 | { 45 | "pk": 1, 46 | "model": "tests.rich", 47 | "fields": { 48 | "name": "Rich One", 49 | "slug": "rich-one", 50 | "content": "This is rich one\nAwesome!" 51 | } 52 | }, 53 | { 54 | "pk": 1, 55 | "model": "oembed.storedoembed", 56 | "fields": { 57 | "maxwidth": null, 58 | "maxheight": null, 59 | "date_added": "2009-12-23 11:07:40", 60 | "date_expires": "2029-12-23 11:07:40", 61 | "resource_type": "video", 62 | "response_json": "{\"provider_url\": \"http:\\/\\/www.youtube.com\\/\", \"version\": \"1.0\", \"title\": \"Leprechaun in Mobile, Alabama\", \"author_name\": \"botmib\", \"height\": 313, \"width\": 384, \"html\": \"<\\/embed><\\/object>\", \"author_url\": \"http:\\/\\/www.youtube.com\\/user\\/botmib\", \"provider_name\": \"YouTube\", \"type\": \"video\"}", 63 | "match": "http://www.youtube.com/watch?v=nda_OSWeyn8" 64 | } 65 | }, 66 | { 67 | "pk": 2, 68 | "model": "oembed.storedoembed", 69 | "fields": { 70 | "maxwidth": null, 71 | "maxheight": null, 72 | "date_added": "2009-12-23 11:07:41", 73 | "date_expires": "2029-12-23 11:07:40", 74 | "resource_type": "video", 75 | "response_json": "{\"provider_url\": \"http:\\/\\/www.flickr.com\\/\", \"title\": \"INVISIBLE PYRAMID\", \"url\": \"http:\\/\\/farm4.static.flickr.com\\/3013\\/2554073003_3e16215e12.jpg\", \"author_name\": \"neil \\u25b3 krug\", \"height\": 500, \"width\": 482, \"version\": \"1.0\", \"author_url\": \"http:\\/\\/www.flickr.com\\/photos\\/neilkrug\\/\", \"provider_name\": \"Flickr\", \"cache_age\": 3600, \"type\": \"photo\"}", 76 | "match": "http://www.flickr.com/photos/neilkrug/2554073003/" 77 | } 78 | }, 79 | { 80 | "pk": 100, 81 | "model": "oembed.storedprovider", 82 | "fields": { 83 | "endpoint_url": "http://www.active.com/oembed/json/", 84 | "regex": "http://www.active.com/.+?", 85 | "wildcard_regex": "http://www.active.com/*", 86 | "resource_type": "photo", 87 | "active": "True", 88 | "provides": "False" 89 | } 90 | }, 91 | { 92 | "pk": 101, 93 | "model": "oembed.storedprovider", 94 | "fields": { 95 | "endpoint_url": "http://www.inactive.com/oembed/json/", 96 | "regex": "http://www.inactive.com/.+?", 97 | "wildcard_regex": "http://www.inactive.com/*", 98 | "resource_type": "video", 99 | "active": "False", 100 | "provides": "False" 101 | } 102 | } 103 | ] 104 | -------------------------------------------------------------------------------- /oembed/tests/models.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | from django.db import models 3 | from django.template.defaultfilters import slugify 4 | 5 | from oembed.fields import EmbeddedMediaField 6 | 7 | class Blog(models.Model): 8 | title = models.CharField(max_length=255) 9 | slug = models.SlugField() 10 | content = models.TextField() 11 | author = models.CharField(max_length=255) 12 | pub_date = models.DateTimeField() 13 | 14 | def save(self, *args, **kwargs): 15 | self.slug = slugify(self.title) 16 | super(Blog, self).save(*args, **kwargs) 17 | 18 | def get_absolute_url(self): 19 | year = self.pub_date.year 20 | month = self.pub_date.strftime('%b').lower() 21 | day = self.pub_date.day 22 | return reverse('test_blog_detail', args=[year, month, day, self.slug]) 23 | 24 | class Category(models.Model): 25 | name = models.CharField(max_length=255) 26 | image = models.ImageField(upload_to='images') 27 | width = models.IntegerField(blank=True, null=True, editable=False) 28 | height = models.IntegerField(blank=True, null=True, editable=False) 29 | 30 | def get_absolute_url(self): 31 | return reverse('test_category_detail', args=[self.pk]) 32 | 33 | class Rich(models.Model): 34 | name = models.CharField(max_length=255) 35 | slug = models.SlugField() 36 | content = models.TextField() 37 | 38 | media = EmbeddedMediaField(['photo', 'video'], related_name='rich_media') 39 | photos = EmbeddedMediaField('photo', related_name='rich_photos') 40 | videos = EmbeddedMediaField('video', related_name='rich_videos') 41 | 42 | def get_absolute_url(self): 43 | return reverse('test_rich_detail', args=[self.slug]) 44 | -------------------------------------------------------------------------------- /oembed/tests/oembed_providers.py: -------------------------------------------------------------------------------- 1 | import oembed 2 | from oembed.providers import DjangoDateBasedProvider, DjangoProvider 3 | from oembed.utils import size_to_nearest 4 | 5 | from oembed.tests.models import Blog, Category, Rich 6 | 7 | class BlogProvider(DjangoDateBasedProvider): 8 | resource_type = 'link' 9 | 10 | class Meta: 11 | model = Blog 12 | named_view = 'test_blog_detail' 13 | fields_to_match = {'entry_slug': 'slug'} 14 | date_field = 'pub_date' 15 | 16 | def author_name(self, obj): 17 | return obj.author 18 | 19 | def title(self, obj): 20 | return obj.title 21 | 22 | class CategoryProvider(DjangoProvider): 23 | resource_type = 'photo' 24 | 25 | class Meta: 26 | model = Category 27 | named_view = 'test_category_detail' 28 | fields_to_match = {'_0': 'pk'} 29 | 30 | def title(self, obj): 31 | return obj.name 32 | 33 | class RichProvider(DjangoProvider): 34 | resource_type = 'rich' 35 | 36 | class Meta: 37 | model = Rich 38 | named_view = 'test_rich_detail' 39 | fields_to_match = {'_0': 'slug'} 40 | 41 | def title(self, obj): 42 | return obj.name 43 | 44 | oembed.site.register(BlogProvider) 45 | oembed.site.register(CategoryProvider) 46 | oembed.site.register(RichProvider) 47 | -------------------------------------------------------------------------------- /oembed/tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEBUG = True 4 | 5 | DATABASE_ENGINE = 'sqlite3' 6 | 7 | SITE_ROOT = os.path.dirname(os.path.realpath(__file__)) 8 | 9 | INSTALLED_APPS = [ 10 | 'django.contrib.auth', 11 | 'django.contrib.admin', 12 | 'django.contrib.contenttypes', 13 | 'django.contrib.sessions', 14 | 'django.contrib.sites', 15 | 'oembed.tests', 16 | 'oembed', 17 | ] 18 | 19 | DEFAULT_FILE_STORAGE = 'oembed.tests.storage.DummyMemoryStorage' 20 | 21 | TEMPLATE_LOADERS = ( 22 | 'django.template.loaders.app_directories.load_template_source', 23 | ) 24 | 25 | TEMPLATE_CONTEXT_PROCESSORS = ( 26 | "django.core.context_processors.auth", 27 | "django.core.context_processors.media", 28 | "django.core.context_processors.request" 29 | ) 30 | 31 | MIDDLEWARE_CLASSES = ( 32 | 'django.middleware.common.CommonMiddleware', 33 | 'django.contrib.sessions.middleware.SessionMiddleware', 34 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 35 | ) 36 | 37 | ROOT_URLCONF = 'oembed.tests.urls' 38 | SITE_ID = 1 39 | 40 | MEDIA_ROOT = '%s/media/' % (SITE_ROOT) 41 | MEDIA_URL = '/media/' 42 | -------------------------------------------------------------------------------- /oembed/tests/storage.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib2 import urlparse 3 | try: 4 | from cStringIO import StringIO 5 | except ImportError: 6 | from StringIO import StringIO 7 | 8 | from django.conf import settings 9 | from django.core.files import storage 10 | 11 | 12 | class DummyMemoryStorage(storage.Storage): 13 | """ 14 | A simple in-memory storage backend for testing image storage - copied from 15 | djutils (http://github.com/coleifer/djutils/) 16 | """ 17 | _files = {} # bampf 18 | 19 | def delete(self, name): 20 | if name in self._files: 21 | del(self._files[name]) 22 | 23 | def exists(self, name): 24 | return name in self._files 25 | 26 | def listdir(self, path): 27 | files = [] 28 | if not path.endswith('/'): 29 | path += '/' # make sure ends in slash for string comp below 30 | for k in self._files: 31 | if k.startswith(path) and '/' not in k.replace(path, ''): 32 | files.append(k.replace(path, '')) 33 | 34 | def size(self, name): 35 | return len(self._files[name]) 36 | 37 | def url(self, name): 38 | return urlparse.urljoin(settings.MEDIA_URL, name) 39 | 40 | def _open(self, name, mode): 41 | return StringIO(self._files.get(name, '')) 42 | 43 | def _save(self, name, content): 44 | content.seek(0) 45 | self._files[name] = content.read() 46 | return name 47 | 48 | def get_valid_name(self, name): 49 | return name 50 | 51 | def get_available_name(self, name): 52 | if name not in self._files: 53 | return name 54 | 55 | base_path, ext = os.path.splitext(name) 56 | counter = 1 57 | 58 | while 1: 59 | test = '%s_%s%s' % (base_path, counter, ext) 60 | if test not in self._files: 61 | return test 62 | counter += 1 63 | -------------------------------------------------------------------------------- /oembed/tests/templates/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worldcompany/djangoembed/f3f2be283441d91d1f89db780444dc75f7b51902/oembed/tests/templates/404.html -------------------------------------------------------------------------------- /oembed/tests/templates/oembed/inline/video.html: -------------------------------------------------------------------------------- 1 | {% if response.title %}{{ response.title }}{% else %}{{ original_url }}{% endif %} 2 | -------------------------------------------------------------------------------- /oembed/tests/templates/oembed/provider/tests_rich.html: -------------------------------------------------------------------------------- 1 |

{{ object.name }}

{{ object.content|linebreaks }} 2 | -------------------------------------------------------------------------------- /oembed/tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from oembed.tests.tests.consumer import * 2 | from oembed.tests.tests.models import * 3 | from oembed.tests.tests.parsers import * 4 | from oembed.tests.tests.providers import * 5 | from oembed.tests.tests.resources import * 6 | from oembed.tests.tests.sites import * 7 | from oembed.tests.tests.templatetags import * 8 | from oembed.tests.tests.utils import * 9 | from oembed.tests.tests.views import * 10 | -------------------------------------------------------------------------------- /oembed/tests/tests/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | from urllib2 import urlparse 3 | try: 4 | import Image 5 | except ImportError: 6 | from PIL import Image 7 | try: 8 | from cStringIO import StringIO 9 | except ImportError: 10 | from StringIO import StringIO 11 | 12 | from django.conf import settings 13 | from django.core.files import storage 14 | from django.core.files.base import ContentFile 15 | from django.core.urlresolvers import reverse, NoReverseMatch 16 | from django.test import TestCase 17 | from django.utils import simplejson 18 | 19 | import oembed 20 | from oembed.providers import BaseProvider 21 | from oembed.resources import OEmbedResource 22 | 23 | from oembed.tests.settings import MEDIA_ROOT, MEDIA_URL, DEFAULT_FILE_STORAGE 24 | from oembed.tests.storage import DummyMemoryStorage 25 | 26 | 27 | class BaseOEmbedTestCase(TestCase): 28 | fixtures = ['oembed_testdata.json'] 29 | urls = 'oembed.tests.urls' 30 | 31 | # third party providers (StoredProvider) 32 | flickr_url = 'http://www.flickr.com/photos/neilkrug/2554073003/' 33 | youtube_url = 'http://www.youtube.com/watch?v=nda_OSWeyn8' 34 | 35 | # django providers (DjangoProvider and DjangoDatebasedProvider) 36 | category_url = 'http://example.com/testapp/category/1/' 37 | blog_url = 'http://example.com/testapp/blog/2010/may/01/entry-1/' 38 | rich_url = 'http://example.com/testapp/rich/rich-one/' 39 | 40 | category_embed = 'Category 1' 41 | 42 | def setUp(self): 43 | "Set up test environment" 44 | # load up all the providers and register the test-only provider 45 | oembed.autodiscover() 46 | 47 | # refresh the attribute-cached time the db providers were last updated 48 | oembed.site._db_updated = None 49 | 50 | self.storage = DummyMemoryStorage() 51 | 52 | # monkeypatch default_storage 53 | self.orig_default_storage = storage.default_storage 54 | storage.default_storage = self.storage 55 | 56 | # swap media root & media url 57 | self.media_root, self.media_url = settings.MEDIA_ROOT, settings.MEDIA_URL 58 | settings.MEDIA_ROOT = MEDIA_ROOT 59 | settings.MEDIA_URL = MEDIA_URL 60 | 61 | # swap out template dirs 62 | self.template_dirs = settings.TEMPLATE_DIRS 63 | cur_dir = os.path.dirname(__file__) 64 | settings.TEMPLATE_DIRS = [os.path.join(os.path.dirname(cur_dir), 'templates')] 65 | 66 | # swap out file storage backend 67 | self.orig_file_storage = settings.DEFAULT_FILE_STORAGE 68 | settings.DEFAULT_FILE_STORAGE = DEFAULT_FILE_STORAGE 69 | 70 | # create 2 images for testing 71 | test_image = Image.new('CMYK', (1024, 768), (255, 255, 255, 255)) 72 | self.test_img_buffer = StringIO() 73 | test_image.save(self.test_img_buffer, 'JPEG') 74 | 75 | self.test_img_file = ContentFile(self.test_img_buffer.getvalue()) 76 | self.test_img_location = 'images/test_image1.jpg' 77 | storage.default_storage.save(self.test_img_location, self.test_img_file) 78 | 79 | def tearDown(self): 80 | settings.MEDIA_ROOT = self.media_root 81 | settings.MEDIA_URL = self.media_url 82 | settings.TEMPLATE_DIRS = self.template_dirs 83 | settings.DEFAULT_FILE_STORAGE = self.orig_file_storage 84 | storage.default_storage = self.orig_default_storage 85 | 86 | def _sort_by_pk(self, list_or_qs): 87 | # decorate, sort, undecorate using the pk of the items 88 | # in the list or queryset 89 | annotated = [(item.pk, item) for item in list_or_qs] 90 | annotated.sort() 91 | return map(lambda item_tuple: item_tuple[1], annotated) 92 | 93 | def assertQuerysetEqual(self, a, b): 94 | # assert list or queryset a is the same as list or queryset b 95 | return self.assertEqual(self._sort_by_pk(a), self._sort_by_pk(b)) 96 | -------------------------------------------------------------------------------- /oembed/tests/tests/consumer.py: -------------------------------------------------------------------------------- 1 | import oembed 2 | 3 | from oembed.tests.tests.base import BaseOEmbedTestCase 4 | from oembed.consumer import OEmbedConsumer 5 | from oembed.resources import OEmbedResource 6 | 7 | 8 | class ConsumerTestCase(BaseOEmbedTestCase): 9 | def setUp(self): 10 | "Set up test environment" 11 | super(ConsumerTestCase, self).setUp() 12 | self.oembed_client = OEmbedConsumer() 13 | 14 | def test_parse_text(self): 15 | consumed = self.oembed_client.parse_text(self.category_url) 16 | self.assertEqual(consumed, self.category_embed) 17 | 18 | def test_parse_html(self): 19 | consumed = self.oembed_client.parse_html('

%s

' % self.category_url) 20 | self.assertEqual(consumed, '

%s

' % self.category_embed) 21 | 22 | def test_extract_oembeds(self): 23 | embeds = self.oembed_client.extract_oembeds(self.category_url) 24 | self.assertEqual(len(embeds), 1) 25 | self.assertEqual(embeds[0]['original_url'], self.category_url) 26 | 27 | embeds = self.oembed_client.extract_oembeds(self.category_url, resource_type='photo') 28 | self.assertEqual(len(embeds), 1) 29 | self.assertEqual(embeds[0]['original_url'], self.category_url) 30 | 31 | embeds = self.oembed_client.extract_oembeds(self.category_url, resource_type='video') 32 | self.assertEqual(len(embeds), 0) 33 | 34 | def test_extract_oembeds_html(self): 35 | embeds = self.oembed_client.extract_oembeds_html('

%s

' % self.category_url) 36 | self.assertEqual(len(embeds), 1) 37 | self.assertEqual(embeds[0]['original_url'], self.category_url) 38 | 39 | embeds = self.oembed_client.extract_oembeds_html('

%s

' % self.category_url, resource_type='photo') 40 | self.assertEqual(len(embeds), 1) 41 | self.assertEqual(embeds[0]['original_url'], self.category_url) 42 | 43 | embeds = self.oembed_client.extract_oembeds_html('

%s

' % self.category_url, resource_type='video') 44 | self.assertEqual(len(embeds), 0) 45 | 46 | embeds = self.oembed_client.extract_oembeds_html('

Some link

' % self.category_url) 47 | self.assertEqual(len(embeds), 0) 48 | 49 | embeds = self.oembed_client.extract_oembeds_html('

%s

' % self.category_url) 50 | self.assertEqual(len(embeds), 0) 51 | 52 | def test_strip(self): 53 | test_string = 'testing [%s] [http://www.google.com]' % self.category_url 54 | expected = 'testing [] [http://www.google.com]' 55 | 56 | self.assertEqual(self.oembed_client.strip(test_string), expected) 57 | 58 | # with width & height 59 | self.assertEqual(self.oembed_client.strip(test_string, 600, 400), expected) 60 | 61 | # with resource_type 62 | self.assertEqual(self.oembed_client.strip(test_string, resource_type='photo'), expected) 63 | self.assertEqual(self.oembed_client.strip(test_string, resource_type='link'), test_string) 64 | 65 | def test_strip_html(self): 66 | test_string = '%(match)s

%(no_match)s

' % \ 67 | {'match': self.category_url, 'no_match': 'http://www.google.com'} 68 | expected = test_string 69 | 70 | self.assertEqual(self.oembed_client.strip(test_string), expected) 71 | 72 | def test_strip_html_failure(self): 73 | # show how strip can fail when handling html - it picks up the match 74 | # in the p tag then replaces it everywhere, including in the a tags 75 | test_string = '%(match)s

%(match)s

%(no_match)s

' % \ 76 | {'match': self.category_url, 'no_match': 'http://www.google.com'} 77 | expected = test_string 78 | actual = '

http://www.google.com

' 79 | 80 | self.assertEqual(self.oembed_client.strip(test_string), actual) 81 | -------------------------------------------------------------------------------- /oembed/tests/tests/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import oembed 4 | from oembed.exceptions import OEmbedMissingEndpoint 5 | from oembed.models import StoredOEmbed, StoredProvider, AggregateMedia 6 | from oembed.providers import DjangoProvider 7 | from oembed.tests.tests.base import BaseOEmbedTestCase 8 | 9 | from oembed.tests.models import Blog, Category, Rich 10 | 11 | 12 | class ModelTestCase(BaseOEmbedTestCase): 13 | def test_stored_oembeds(self): 14 | video = oembed.site.embed(self.youtube_url) 15 | stored = StoredOEmbed.objects.get(match=self.youtube_url) 16 | self.assertEqual(stored.response, video.get_data()) 17 | 18 | photo = oembed.site.embed(self.flickr_url) 19 | stored = StoredOEmbed.objects.get(match=self.flickr_url) 20 | self.assertEqual(stored.response, photo.get_data()) 21 | 22 | # now create some on-the-fly 23 | link = oembed.site.embed(self.blog_url) 24 | stored = StoredOEmbed.objects.get(match=self.blog_url) 25 | self.assertEqual(stored.response, link.get_data()) 26 | 27 | photo = oembed.site.embed(self.category_url) 28 | stored = StoredOEmbed.objects.get(match=self.category_url) 29 | self.assertEqual(stored.response, photo.get_data()) 30 | 31 | rich = oembed.site.embed(self.rich_url) 32 | stored = StoredOEmbed.objects.get(match=self.rich_url) 33 | self.assertEqual(stored.response, rich.get_data()) 34 | 35 | def test_stored_providers(self): 36 | active = StoredProvider.objects.get(pk=100) 37 | inactive = StoredProvider.objects.get(pk=101) 38 | 39 | active_qs = StoredProvider.objects.active() 40 | self.assertTrue(active in active_qs) 41 | self.assertFalse(inactive in active_qs) 42 | 43 | provider_list = oembed.site.get_providers() 44 | self.assertTrue(active in provider_list) 45 | self.assertFalse(inactive in provider_list) 46 | 47 | active.active = False 48 | active.save() 49 | provider_list = oembed.site.get_providers() 50 | self.assertFalse(active in provider_list) 51 | 52 | def test_media_aggregation(self): 53 | r = Rich(name='Test', slug='test', content='Hey check this out: %s' % self.youtube_url) 54 | r.save() 55 | 56 | am_queryset = AggregateMedia.objects.all() 57 | self.assertEqual(am_queryset.count(), 1) 58 | 59 | aggregated_object = am_queryset[0] 60 | self.assertEqual(aggregated_object.url, self.youtube_url) 61 | self.assertEqual(aggregated_object.media, StoredOEmbed.objects.get(match=self.youtube_url)) 62 | 63 | self.assertQuerysetEqual(r.media.all(), am_queryset) 64 | self.assertQuerysetEqual(r.videos.all(), am_queryset) 65 | self.assertQuerysetEqual(r.photos.all(), []) 66 | 67 | r.content = 'Whoa i changed my mind, you should check this out: %s' % self.flickr_url 68 | r.save() 69 | 70 | am_queryset = AggregateMedia.objects.all() 71 | 72 | # the youtube one sticks around, but records from the rel table are killed 73 | self.assertEqual(am_queryset.count(), 2) 74 | 75 | # the flickr embed is there 76 | aggregated_object = am_queryset.get(url=self.flickr_url) 77 | 78 | # check that the flickr aggregated object GFKs to the StoredOEmbed 79 | self.assertEqual(aggregated_object.media, StoredOEmbed.objects.get(match=self.flickr_url)) 80 | 81 | # the m2m fields should all be cleared out now 82 | self.assertQuerysetEqual(r.media.all(), am_queryset.filter(url=self.flickr_url)) 83 | self.assertQuerysetEqual(r.videos.all(), []) 84 | self.assertQuerysetEqual(r.photos.all(), am_queryset.filter(url=self.flickr_url)) 85 | 86 | r.content = 'Just text please' 87 | r.save() 88 | 89 | self.assertQuerysetEqual(r.media.all(), []) 90 | self.assertQuerysetEqual(r.videos.all(), []) 91 | self.assertQuerysetEqual(r.photos.all(), []) 92 | 93 | def test_internal_media_aggregation(self): 94 | category1 = Category.objects.get(pk=1) 95 | 96 | r = Rich(name='Container', slug='container', content='Check this out: %s' % self.category_url) 97 | r.save() 98 | 99 | am_queryset = AggregateMedia.objects.all() 100 | self.assertEqual(am_queryset.count(), 1) 101 | 102 | aggregated_object = am_queryset[0] 103 | self.assertEqual(aggregated_object.url, self.category_url) 104 | self.assertEqual(aggregated_object.media, category1) # gfk to actual category 105 | 106 | self.assertQuerysetEqual(r.media.all(), am_queryset) 107 | self.assertQuerysetEqual(r.videos.all(), []) 108 | self.assertQuerysetEqual(r.photos.all(), am_queryset) 109 | -------------------------------------------------------------------------------- /oembed/tests/tests/parsers.py: -------------------------------------------------------------------------------- 1 | import oembed 2 | 3 | from oembed.tests.tests.base import BaseOEmbedTestCase 4 | from oembed.parsers.text import TextParser, TextBlockParser 5 | from oembed.parsers.html import HTMLParser 6 | 7 | 8 | class TextBlockParserTestCase(BaseOEmbedTestCase): 9 | def setUp(self): 10 | self.parser = TextBlockParser() 11 | super(TextBlockParserTestCase, self).setUp() 12 | 13 | def test_basic_handling(self): 14 | parsed = self.parser.parse(self.category_url) 15 | self.assertEqual(parsed, self.category_embed) 16 | 17 | def test_inline_link_handling(self): 18 | parsed = self.parser.parse('Testing %s' % self.category_url) 19 | self.assertEqual(parsed, 'Testing %s' % self.category_embed) 20 | 21 | def test_block_handling(self): 22 | parsed = self.parser.parse('Testing %(url)s\n%(url)s' % ({'url': self.category_url})) 23 | self.assertEqual(parsed, 'Testing %(embed)s\n%(embed)s' % ({'embed': self.category_embed})) 24 | 25 | def test_urlization(self): 26 | test_string = 'Testing http://www.google.com' 27 | parsed = self.parser.parse(test_string, urlize_all_links=False) 28 | self.assertEqual(parsed, test_string) 29 | 30 | parsed = self.parser.parse(test_string, urlize_all_links=True) 31 | self.assertEqual(parsed, 'Testing http://www.google.com') 32 | 33 | def test_extraction(self): 34 | extracted = self.parser.extract_urls('Testing %s wha?' % self.category_url) 35 | self.assertEqual(extracted, [self.category_url]) 36 | 37 | def test_extraction_ordering(self): 38 | extracted = self.parser.extract_urls(''' 39 | %s %s %s 40 | %s 41 | ''' % (self.category_url, self.blog_url, self.category_url, self.rich_url)) 42 | 43 | self.assertEqual(extracted, [ 44 | self.category_url, 45 | self.blog_url, 46 | self.rich_url, 47 | ]) 48 | 49 | 50 | class TextParserTestCase(BaseOEmbedTestCase): 51 | def setUp(self): 52 | self.parser = TextParser() 53 | super(TextParserTestCase, self).setUp() 54 | 55 | def test_basic_handling(self): 56 | parsed = self.parser.parse(self.category_url) 57 | self.assertEqual(parsed, self.category_embed) 58 | 59 | def test_inline_link_handling(self): 60 | parsed = self.parser.parse('Testing %s' % self.category_url) 61 | self.assertEqual(parsed, 'Testing Category 1') 62 | 63 | def test_block_handling(self): 64 | parsed = self.parser.parse('Testing %(url)s\n%(url)s' % ({'url': self.category_url})) 65 | self.assertEqual(parsed, 'Testing Category 1\n%s' % self.category_embed) 66 | 67 | def test_extraction(self): 68 | extracted = self.parser.extract_urls('Testing %s wha?' % self.category_url) 69 | self.assertEqual(extracted, [self.category_url]) 70 | 71 | def test_extraction_ordering(self): 72 | extracted = self.parser.extract_urls(''' 73 | %s %s %s 74 | 75 | %s 76 | ''' % (self.category_url, self.blog_url, self.category_url, self.rich_url)) 77 | 78 | self.assertEqual(extracted, [ 79 | self.category_url, 80 | self.blog_url, 81 | self.rich_url, 82 | ]) 83 | 84 | 85 | class HTMLParserTestCase(BaseOEmbedTestCase): 86 | def setUp(self): 87 | self.parser = HTMLParser() 88 | super(HTMLParserTestCase, self).setUp() 89 | 90 | def test_basic_handling(self): 91 | parsed = self.parser.parse('

%s

' % self.category_url) 92 | self.assertEqual(parsed, '

%s

' % self.category_embed) 93 | 94 | def test_inline_link_handling(self): 95 | parsed = self.parser.parse('

Testing %s

' % self.category_url) 96 | self.assertEqual(parsed, '

Testing Category 1

') 97 | 98 | def test_block_handling(self): 99 | parsed = self.parser.parse('

Testing %(url)s

%(url)s

' % ({'url': self.category_url})) 100 | self.assertEqual(parsed, '

Testing Category 1

%s

' % self.category_embed) 101 | 102 | def test_buried_link(self): 103 | parsed = self.parser.parse('

Testing %(url)s

' % ({'url': self.category_url})) 104 | self.assertEqual(parsed, '

Testing http://example.com/testapp/category/1/

') 105 | 106 | def test_outside_of_markup(self): 107 | parsed = self.parser.parse('%s

Wow this is bad

' % self.category_url) 108 | self.assertEqual(parsed, '%s

Wow this is bad

' % self.category_embed) 109 | 110 | def test_extraction(self): 111 | extracted = self.parser.extract_urls('

Testing %s wha?

' % self.category_url) 112 | self.assertEqual(extracted, [self.category_url]) 113 | 114 | extracted = self.parser.extract_urls('

Testing %(url)s wha?

' % ({'url': self.category_url})) 115 | self.assertEqual(extracted, []) 116 | 117 | def test_extraction_ordering(self): 118 | extracted = self.parser.extract_urls(''' 119 |

%s

%s

120 | %s 121 | yo 122 |

%s

123 | ''' % (self.category_url, self.blog_url, self.rich_url, self.rich_url, self.flickr_url)) 124 | 125 | self.assertEqual(extracted, [ 126 | self.category_url, 127 | self.blog_url, 128 | self.flickr_url, 129 | ]) 130 | -------------------------------------------------------------------------------- /oembed/tests/tests/resources.py: -------------------------------------------------------------------------------- 1 | import oembed 2 | 3 | from oembed.tests.tests.base import BaseOEmbedTestCase 4 | from oembed.resources import OEmbedResource 5 | 6 | 7 | class ResourceTestCase(BaseOEmbedTestCase): 8 | def test_json_handling(self): 9 | resource = oembed.site.embed(self.category_url) 10 | 11 | json = resource.json 12 | another_resource = OEmbedResource.create_json(json) 13 | 14 | self.assertEqual(resource.get_data(), another_resource.get_data()) 15 | -------------------------------------------------------------------------------- /oembed/tests/tests/sites.py: -------------------------------------------------------------------------------- 1 | from django.utils import simplejson 2 | 3 | import oembed 4 | from oembed.exceptions import AlreadyRegistered, NotRegistered, OEmbedMissingEndpoint 5 | from oembed.models import StoredProvider, StoredOEmbed 6 | from oembed.resources import OEmbedResource 7 | from oembed.tests.oembed_providers import BlogProvider 8 | from oembed.tests.tests.base import BaseOEmbedTestCase 9 | 10 | 11 | class ProviderSiteTestCase(BaseOEmbedTestCase): 12 | def test_register(self): 13 | oembed.site.unregister(BlogProvider) 14 | self.assertRaises(NotRegistered, oembed.site.unregister, BlogProvider) 15 | 16 | oembed.site.register(BlogProvider) 17 | self.assertRaises(AlreadyRegistered, oembed.site.register, BlogProvider) 18 | 19 | def test_get_provider(self): 20 | oembed.site.unregister(BlogProvider) 21 | self.assertRaises(OEmbedMissingEndpoint, oembed.site.provider_for_url, self.blog_url) 22 | 23 | oembed.site.register(BlogProvider) 24 | provider = oembed.site.provider_for_url(self.blog_url) 25 | self.assertTrue(isinstance(provider, BlogProvider)) 26 | 27 | def test_embed(self): 28 | oembed.site.unregister(BlogProvider) 29 | self.assertRaises(OEmbedMissingEndpoint, oembed.site.embed, self.blog_url) 30 | 31 | oembed.site.register(BlogProvider) 32 | resource = oembed.site.embed(self.blog_url) 33 | 34 | self.assertTrue(isinstance(resource, OEmbedResource)) 35 | 36 | def test_object_caching(self): 37 | StoredOEmbed.objects.all().delete() 38 | 39 | for i in range(3): 40 | resource = oembed.site.embed(self.blog_url) 41 | self.assertEqual(StoredOEmbed.objects.count(), 1) 42 | 43 | for i in range(3): 44 | resource = oembed.site.embed(self.blog_url, maxwidth=400, maxheight=400) 45 | self.assertEqual(StoredOEmbed.objects.count(), 2) 46 | 47 | for i in range(3): 48 | resource = oembed.site.embed(self.blog_url, maxwidth=400) 49 | self.assertEqual(StoredOEmbed.objects.count(), 3) 50 | 51 | def test_autodiscovery(self): 52 | resp = self.client.get('/oembed/') 53 | json = simplejson.loads(resp.content) 54 | 55 | providers = oembed.site.store_providers(json) 56 | self.assertEqual(len(providers), 3) 57 | 58 | blog_provider, category_provider, rich_provider = providers 59 | 60 | self.assertEqual(blog_provider.wildcard_regex, 'http://example.com/testapp/blog/*/*/*/*/') 61 | self.assertEqual(blog_provider.regex, 'http://example.com/testapp/blog/.+?/.+?/.+?/.+?/') 62 | self.assertEqual(blog_provider.resource_type, 'link') 63 | self.assertEqual(blog_provider.endpoint_url, 'http://example.com/oembed/json/') 64 | 65 | self.assertEqual(category_provider.wildcard_regex, 'http://example.com/testapp/category/*/') 66 | self.assertEqual(category_provider.regex, 'http://example.com/testapp/category/.+?/') 67 | self.assertEqual(category_provider.resource_type, 'photo') 68 | self.assertEqual(category_provider.endpoint_url, 'http://example.com/oembed/json/') 69 | 70 | self.assertEqual(rich_provider.wildcard_regex, 'http://example.com/testapp/rich/*/') 71 | self.assertEqual(rich_provider.regex, 'http://example.com/testapp/rich/.+?/') 72 | self.assertEqual(rich_provider.resource_type, 'rich') 73 | self.assertEqual(rich_provider.endpoint_url, 'http://example.com/oembed/json/') 74 | -------------------------------------------------------------------------------- /oembed/tests/tests/templatetags.py: -------------------------------------------------------------------------------- 1 | from django.template import Context, Template 2 | 3 | import oembed 4 | from oembed.models import StoredOEmbed 5 | from oembed.tests.models import Category 6 | 7 | from oembed.tests.tests.base import BaseOEmbedTestCase 8 | 9 | class OEmbedTemplateTagTestCase(BaseOEmbedTestCase): 10 | template_string = '{% load oembed_tags %}{% oembed %}XXX{% endoembed %}' 11 | 12 | def test_oembed_tag(self): 13 | t = Template(self.template_string.replace('XXX', self.category_url)) 14 | c = Context() 15 | result = t.render(c) 16 | self.assertEqual(result, self.category_embed) 17 | 18 | t = Template(self.template_string.replace('XXX', 'http://www.google.com/')) 19 | c = Context() 20 | result = t.render(c) 21 | self.assertEqual('%(LINK)s' % {'LINK': 'http://www.google.com/'}, result) 22 | 23 | def test_oembed_filter(self): 24 | t = Template('{% load oembed_tags %}{{ test_string|oembed }}') 25 | c = Context({'test_string': self.category_url}) 26 | result = t.render(c) 27 | self.assertEqual(result, self.category_embed) 28 | 29 | c = Context({'test_string': 'http://www.google.com/'}) 30 | result = t.render(c) 31 | self.assertEqual('%(LINK)s' % {'LINK': 'http://www.google.com/'}, result) 32 | 33 | def test_extract_filter(self): 34 | t = Template('{% load oembed_tags %}{% for embed in test_string|extract_oembeds %}{{ embed.original_url }}{% endfor %}') 35 | c = Context({'test_string': self.category_url}) 36 | result = t.render(c) 37 | self.assertEqual(result, self.category_url) 38 | 39 | t = Template('{% load oembed_tags %}{% for embed in test_string|extract_oembeds:"photo" %}{{ embed.original_url }}{% endfor %}') 40 | c = Context({'test_string': self.category_url + ' ' + self.blog_url}) 41 | result = t.render(c) 42 | self.assertEqual(result, self.category_url) 43 | 44 | t = Template('{% load oembed_tags %}{% for embed in test_string|extract_oembeds:"link" %}{{ embed.original_url }}{% endfor %}') 45 | c = Context({'test_string': self.category_url + ' ' + self.blog_url}) 46 | result = t.render(c) 47 | self.assertEqual(result, self.blog_url) 48 | 49 | def test_strip_filter(self): 50 | t = Template('{% load oembed_tags %}{{ test_string|strip_oembeds }}') 51 | c = Context({'test_string': 'testing [%s]' % self.category_url}) 52 | result = t.render(c) 53 | self.assertEqual(result, 'testing []') 54 | 55 | t = Template('{% load oembed_tags %}{{ test_string|strip_oembeds:"photo" }}') 56 | c = Context({'test_string': 'testing [%s]' % self.category_url}) 57 | result = t.render(c) 58 | self.assertEqual(result, 'testing []') 59 | 60 | t = Template('{% load oembed_tags %}{{ test_string|strip_oembeds:"link" }}') 61 | c = Context({'test_string': 'testing [%s]' % self.category_url}) 62 | result = t.render(c) 63 | self.assertEqual(result, c['test_string']) 64 | 65 | def test_autodiscover(self): 66 | t = Template('{% load oembed_tags %}{% oembed_autodiscover obj %}') 67 | c = Context({'obj': Category.objects.get(pk=1)}) 68 | result = t.render(c) 69 | self.assertEqual(result, '') 70 | 71 | def test_scheme(self): 72 | t = Template('{% load oembed_tags %}{% oembed_url_scheme %}') 73 | c = Context() 74 | result = t.render(c) 75 | self.assertEqual(result, '') 76 | -------------------------------------------------------------------------------- /oembed/tests/tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sites.models import Site 2 | 3 | from oembed.tests.tests.base import BaseOEmbedTestCase 4 | from oembed.utils import size_to_nearest, relative_to_full, load_class, cleaned_sites, scale 5 | 6 | class OEmbedUtilsTestCase(BaseOEmbedTestCase): 7 | def test_size_to_nearest(self): 8 | sizes = ((100, 100), (200, 200), (300, 300)) 9 | 10 | self.assertEqual((300, 200), size_to_nearest(400, 200, sizes, False)) 11 | self.assertEqual((100, 100), size_to_nearest(100, 100, sizes, False)) 12 | self.assertEqual((200, 300), size_to_nearest(250, 500, sizes, False)) 13 | 14 | # if force_fit is False then jump to the largest on None 15 | self.assertEqual((100, 300), size_to_nearest(150, None, sizes, False)) 16 | self.assertEqual((300, 100), size_to_nearest(None, 150, sizes, False)) 17 | 18 | # if force_fit is True then scale to the nearest size for the one 19 | # that is defined 20 | self.assertEqual((100, 100), size_to_nearest(150, None, sizes, True)) 21 | self.assertEqual((100, 100), size_to_nearest(None, 150, sizes, True)) 22 | self.assertEqual((200, 200), size_to_nearest(220, None, sizes, True)) 23 | self.assertEqual((200, 200), size_to_nearest(None, 220, sizes, True)) 24 | 25 | 26 | # if both dimensions are None use the largest possible 27 | self.assertEqual((300, 300), size_to_nearest(None, None, sizes, False)) 28 | self.assertEqual((300, 300), size_to_nearest(None, None, sizes, True)) 29 | 30 | # if a dimension is too small scale it up to the minimum 31 | self.assertEqual((100, 300), size_to_nearest(50, 300, sizes, False)) 32 | self.assertEqual((100, 100), size_to_nearest(50, 300, sizes, True)) 33 | self.assertEqual((100, 300), size_to_nearest(50, None, sizes, False)) 34 | self.assertEqual((100, 100), size_to_nearest(50, None, sizes, True)) 35 | 36 | # test when things are too large 37 | self.assertEqual((200, 200), size_to_nearest(400, 200, sizes, True)) 38 | self.assertEqual((100, 100), size_to_nearest(100, 100, sizes, True)) 39 | self.assertEqual((200, 200), size_to_nearest(250, 500, sizes, True)) 40 | 41 | # test Nones around the edges 42 | self.assertEqual((100, 100), size_to_nearest(100, None, sizes, True)) 43 | self.assertEqual((100, 100), size_to_nearest(None, 100, sizes, True)) 44 | 45 | def test_size_to_nearest_defaults(self): 46 | self.assertEqual((800, 600), size_to_nearest(800, 600)) 47 | self.assertEqual((800, 600), size_to_nearest(850, 650)) 48 | 49 | self.assertEqual((800, 300), size_to_nearest(None, 350)) 50 | self.assertEqual((400, 800), size_to_nearest(450, None)) 51 | 52 | self.assertEqual((800, 300), size_to_nearest(None, 350)) 53 | self.assertEqual((400, 800), size_to_nearest(450, None)) 54 | 55 | self.assertEqual((200, 200), size_to_nearest(400, 250, force_fit=True)) 56 | 57 | def test_relative_to_full(self): 58 | self.assertEqual('http://test.com/a/b/', relative_to_full('/a/b/', 'http://test.com')) 59 | self.assertEqual('http://test.com/a/b/', relative_to_full('/a/b/', 'http://test.com/c/d/?cruft')) 60 | self.assertEqual('http://test.com/a/b/', relative_to_full('http://test.com/a/b/', 'http://test.com')) 61 | self.assertEqual('http://blah.com/a/b/', relative_to_full('http://blah.com/a/b/', 'http://test.com')) 62 | self.assertEqual('/a/b/', relative_to_full('/a/b/', '')) 63 | 64 | def test_load_class(self): 65 | parser_class = load_class('oembed.parsers.html.HTMLParser') 66 | self.assertEqual(parser_class.__name__, 'HTMLParser') 67 | self.assertEqual(parser_class.__module__, 'oembed.parsers.html') 68 | 69 | def test_cleaned_sites(self): 70 | sites = Site.objects.all() 71 | cleaned = cleaned_sites() 72 | example = cleaned[1] # example site 73 | self.assertEquals(example[1], 'example.com') 74 | self.assertEquals(example[2], 'http://example.com') 75 | self.assertEquals(example[0], 'https?:\/\/(?:www[^\.]*\.)?example.com') 76 | 77 | www2_site = Site.objects.create(name='Test Site', domain='www2.testsite.com') 78 | mobile_site = Site.objects.create(name='Mobile Site', domain='m.testsite.com') 79 | 80 | cleaned = cleaned_sites() 81 | self.assertEquals(cleaned[www2_site.pk][1], 'Test Site') 82 | self.assertEquals(cleaned[www2_site.pk][2], 'http://www2.testsite.com') 83 | self.assertEquals(cleaned[www2_site.pk][0], 'https?:\/\/(?:www[^\.]*\.)?testsite.com') 84 | 85 | self.assertEquals(cleaned[mobile_site.pk][1], 'Mobile Site') 86 | self.assertEquals(cleaned[mobile_site.pk][2], 'http://m.testsite.com') 87 | self.assertEquals(cleaned[mobile_site.pk][0], 'https?:\/\/(?:www[^\.]*\.)?m.testsite.com') 88 | 89 | def test_scale(self): 90 | self.assertEqual(scale(640, 480, 320), (320, 240)) 91 | self.assertEqual(scale(640, 480, 500, 240), (320, 240)) 92 | self.assertEqual(scale(640, 480, 320, 500), (320, 240)) 93 | self.assertEqual(scale(640, 480, 320, 240), (320, 240)) 94 | 95 | self.assertEqual(scale(640, 480, 700), (640, 480)) 96 | self.assertEqual(scale(640, 480, 700, 500), (640, 480)) 97 | -------------------------------------------------------------------------------- /oembed/tests/tests/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.urlresolvers import reverse 3 | from django.utils import simplejson 4 | 5 | import oembed 6 | from oembed.models import StoredOEmbed, StoredProvider 7 | from oembed.tests.tests.base import BaseOEmbedTestCase 8 | 9 | class OEmbedViewTestCase(BaseOEmbedTestCase): 10 | def test_missing_endpoint(self): 11 | response = self.client.get('/oembed/json/?url=http://www.nothere.com/asdf/') 12 | self.assertEqual(response.status_code, 404) 13 | 14 | def test_bad_request(self): 15 | # no url provided raises bad request (400) 16 | response = self.client.get('/oembed/json/') 17 | self.assertEqual(response.status_code, 400) 18 | 19 | def test_basic_handling(self): 20 | response = self.client.get('/oembed/json/?url=%s' % self.category_url) 21 | self.assertEqual(response.status_code, 200) 22 | response_json = simplejson.loads(response.content) 23 | 24 | stored_oembed = StoredOEmbed.objects.get(match=self.category_url) 25 | self.assertEqual(response_json, stored_oembed.response) 26 | 27 | def test_stored_provider_signals(self): 28 | response = self.client.get('/oembed/json/?url=%s' % self.youtube_url) 29 | 30 | # Check some response details - this provider doesn't provide upstream 31 | self.assertEqual(response.status_code, 404) 32 | 33 | # re-register the youtube provider to provide upstream 34 | yt = StoredProvider.objects.get(endpoint_url='http://www.youtube.com/oembed') 35 | yt.provides = True 36 | yt.save() 37 | 38 | # now we should be able to get the youtube object through the endpoint 39 | response = self.client.get('/oembed/json/?url=%s' % self.youtube_url) 40 | self.assertEqual(response.status_code, 200) 41 | 42 | stored = StoredOEmbed.objects.get(match=self.youtube_url) 43 | self.assertEqual(simplejson.loads(response.content), stored.response) 44 | 45 | def test_oembed_schema(self): 46 | response = self.client.get('/oembed/') 47 | self.assertEqual(response.status_code, 200) 48 | 49 | json_data = simplejson.loads(response.content) 50 | self.assertEqual(json_data, [ 51 | { 52 | "matches": "http://example.com/testapp/blog/*/*/*/*/", 53 | "endpoint": "/oembed/json/", 54 | "type": "link" 55 | }, 56 | { 57 | "matches": "http://example.com/testapp/category/*/", 58 | "endpoint": "/oembed/json/", 59 | "type": "photo" 60 | }, 61 | { 62 | "matches": "http://example.com/testapp/rich/*/", 63 | "endpoint": "/oembed/json/", 64 | "type": "rich" 65 | } 66 | ]) 67 | 68 | stored_provider = StoredProvider.objects.get(pk=100) 69 | stored_provider.provides = True 70 | stored_provider.save() 71 | 72 | expected = { 73 | 'matches': 'http://www.active.com/*', 74 | 'endpoint': '/oembed/json/', 75 | 'type': 'photo' 76 | } 77 | 78 | response = self.client.get('/oembed/') 79 | self.assertEqual(response.status_code, 200) 80 | 81 | json_data = simplejson.loads(response.content) 82 | self.assertTrue(expected in json_data) 83 | -------------------------------------------------------------------------------- /oembed/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | def null_view(*args, **kwargs): 7 | pass 8 | 9 | urlpatterns = patterns('', 10 | url(r'^admin/', include(admin.site.urls)), 11 | url(r'^oembed/', include('oembed.urls')), 12 | url(r'^testapp/blog/(?P\d+)/(?P\w+)/(?P\d+)/(?P[\w-]+)/$', null_view, name='test_blog_detail'), 13 | url(r'^testapp/rich/([a-z-]+)/$', null_view, name='test_rich_detail'), 14 | url(r'^testapp/category/(\d+)/$', null_view, name='test_category_detail'), 15 | ) 16 | -------------------------------------------------------------------------------- /oembed/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.defaults import * 3 | 4 | urlpatterns = patterns('oembed.views', 5 | url(r'^$', 'oembed_schema', name='oembed_schema'), 6 | url(r'^json/$', 'json', name='oembed_json'), 7 | url(r'^consume/json/$', 'consume_json', name='oembed_consume_json'), 8 | ) 9 | -------------------------------------------------------------------------------- /oembed/utils.py: -------------------------------------------------------------------------------- 1 | import httplib2 2 | import re 3 | 4 | from django.conf import settings 5 | from django.contrib.sites.models import Site 6 | from django.http import HttpRequest 7 | from django.utils.importlib import import_module 8 | 9 | from oembed.constants import DOMAIN_RE, OEMBED_ALLOWED_SIZES, SOCKET_TIMEOUT 10 | from oembed.exceptions import OEmbedHTTPException 11 | 12 | 13 | def size_to_nearest(width=None, height=None, allowed_sizes=OEMBED_ALLOWED_SIZES, 14 | force_fit=False): 15 | """ 16 | Generate some dimensions for resizing an object. This function DOES NOT handle 17 | scaling, it simply calculates maximums. These values should then be passed to 18 | the resize() method which will scale it and return the scaled width & height. 19 | """ 20 | minwidth, minheight = min(allowed_sizes) 21 | maxwidth, maxheight = max(allowed_sizes) 22 | 23 | if not width and not height: 24 | return maxwidth, maxheight 25 | 26 | if width: 27 | width = int(width) > minwidth and int(width) or minwidth 28 | elif force_fit: 29 | width = maxwidth 30 | 31 | if height: 32 | height = int(height) > minheight and int(height) or minheight 33 | elif force_fit: 34 | height = maxheight 35 | 36 | for size in sorted(allowed_sizes): 37 | if width and not height: 38 | if width >= size[0]: 39 | maxwidth = size[0] 40 | if force_fit: 41 | maxheight = size[1] 42 | else: 43 | break 44 | elif height and not width: 45 | if height >= size[1]: 46 | maxheight = size[1] 47 | if force_fit: 48 | maxwidth = size[0] 49 | else: 50 | break 51 | else: 52 | if force_fit: 53 | if (width >= size[0]) and (height >= size[1]): 54 | maxwidth, maxheight = size 55 | else: 56 | break 57 | else: 58 | if width >= size[0]: 59 | maxwidth = size[0] 60 | if height >= size[1]: 61 | maxheight = size[1] 62 | return maxwidth, maxheight 63 | 64 | def scale(width, height, new_width, new_height=None): 65 | # determine if resizing needs to be done (will not scale up) 66 | if width < new_width: 67 | if not new_height or height < new_height: 68 | return (width, height) 69 | 70 | # calculate ratios 71 | width_percent = (new_width / float(width)) 72 | if new_height: 73 | height_percent = (new_height / float(height)) 74 | 75 | if not new_height or width_percent < height_percent: 76 | new_height = int((float(height) * float(width_percent))) 77 | else: 78 | new_width = int((float(width) * float(height_percent))) 79 | 80 | return (new_width, new_height) 81 | 82 | def fetch_url(url, method='GET', user_agent='django-oembed', timeout=SOCKET_TIMEOUT): 83 | """ 84 | Fetch response headers and data from a URL, raising a generic exception 85 | for any kind of failure. 86 | """ 87 | sock = httplib2.Http(timeout=timeout) 88 | request_headers = { 89 | 'User-Agent': user_agent, 90 | 'Accept-Encoding': 'gzip'} 91 | try: 92 | headers, raw = sock.request(url, headers=request_headers, method=method) 93 | except: 94 | raise OEmbedHTTPException('Error fetching %s' % url) 95 | return headers, raw 96 | 97 | def get_domain(url): 98 | match = re.search(DOMAIN_RE, url) 99 | if match: 100 | return match.group() 101 | return '' 102 | 103 | def relative_to_full(url, example_url): 104 | """ 105 | Given a url which may or may not be a relative url, convert it to a full 106 | url path given another full url as an example 107 | """ 108 | if re.match('https?:\/\/', url): 109 | return url 110 | domain = get_domain(example_url) 111 | if domain: 112 | return '%s%s' % (domain, url) 113 | return url 114 | 115 | def mock_request(): 116 | """ 117 | Generate a fake request object to allow oEmbeds to use context processors. 118 | """ 119 | current_site = Site.objects.get_current() 120 | request = HttpRequest() 121 | request.META['SERVER_NAME'] = current_site.domain 122 | return request 123 | 124 | def load_class(path): 125 | """ 126 | dynamically load a class given a string of the format 127 | 128 | package.Class 129 | """ 130 | package, klass = path.rsplit('.', 1) 131 | module = import_module(package) 132 | return getattr(module, klass) 133 | 134 | def cleaned_sites(): 135 | """ 136 | Create a list of tuples mapping domains from the sites table to their 137 | site name. The domains will be cleaned into regexes that may be 138 | more permissive than the site domain is in the db. 139 | 140 | [(domain_regex, domain_name, domain_string), ...] 141 | """ 142 | mappings = {} 143 | for site in Site.objects.all(): 144 | # match the site domain, breaking it into several pieces 145 | match = re.match(r'(https?://)?(www[^\.]*\.)?([^/]+)', site.domain) 146 | 147 | if match is not None: 148 | http, www, domain = match.groups() 149 | 150 | # if the protocol is specified, use it, otherwise accept 80/443 151 | http_re = http or r'https?:\/\/' 152 | 153 | # whether or not there's a www (or www2 :x) allow it in the match 154 | www_re = r'(?:www[^\.]*\.)?' 155 | 156 | # build a regex of the permissive http re, the www re, and the domain 157 | domain_re = http_re + www_re + domain 158 | 159 | # now build a pretty string representation of the domain 160 | http = http or r'http://' 161 | www = www or '' 162 | normalized = http + www + domain 163 | 164 | mappings[site.pk] = (domain_re, site.name, normalized) 165 | return mappings 166 | -------------------------------------------------------------------------------- /oembed/views.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.contrib.sites.models import Site 4 | from django.core.urlresolvers import reverse, get_resolver 5 | from django.http import HttpResponse, HttpResponseBadRequest, Http404 6 | from django.template import defaultfilters, RequestContext 7 | from django.utils import simplejson 8 | from django.utils.encoding import smart_str 9 | 10 | import oembed 11 | from oembed.consumer import OEmbedConsumer 12 | from oembed.exceptions import OEmbedException, OEmbedMissingEndpoint 13 | from oembed.providers import DjangoProvider, HTTPProvider 14 | 15 | 16 | resolver = get_resolver(None) 17 | 18 | 19 | def json(request, *args, **kwargs): 20 | """ 21 | The oembed endpoint, or the url to which requests for metadata are passed. 22 | Third parties will want to access this view with URLs for your site's 23 | content and be returned OEmbed metadata. 24 | """ 25 | # coerce to dictionary 26 | params = dict(request.GET.items()) 27 | 28 | callback = params.pop('callback', None) 29 | url = params.pop('url', None) 30 | 31 | if not url: 32 | return HttpResponseBadRequest('Required parameter missing: URL') 33 | 34 | try: 35 | provider = oembed.site.provider_for_url(url) 36 | if not provider.provides: 37 | raise OEmbedMissingEndpoint() 38 | except OEmbedMissingEndpoint: 39 | raise Http404('No provider found for %s' % url) 40 | 41 | query = dict([(smart_str(k), smart_str(v)) for k, v in params.items() if v]) 42 | 43 | try: 44 | resource = oembed.site.embed(url, **query) 45 | except OEmbedException, e: 46 | raise Http404('Error embedding %s: %s' % (url, str(e))) 47 | 48 | response = HttpResponse(mimetype='application/json') 49 | json = resource.json 50 | 51 | if callback: 52 | response.write('%s(%s)' % (defaultfilters.force_escape(callback), json)) 53 | else: 54 | response.write(json) 55 | 56 | return response 57 | 58 | 59 | def consume_json(request): 60 | """ 61 | Extract and return oembed content for given urls. 62 | 63 | Required GET params: 64 | urls - list of urls to consume 65 | 66 | Optional GET params: 67 | width - maxwidth attribute for oembed content 68 | height - maxheight attribute for oembed content 69 | template_dir - template_dir to use when rendering oembed 70 | 71 | Returns: 72 | list of dictionaries with oembed metadata and renderings, json encoded 73 | """ 74 | client = OEmbedConsumer() 75 | 76 | urls = request.GET.getlist('urls') 77 | width = request.GET.get('width') 78 | height = request.GET.get('height') 79 | template_dir = request.GET.get('template_dir') 80 | 81 | output = {} 82 | ctx = RequestContext(request) 83 | 84 | for url in urls: 85 | try: 86 | provider = oembed.site.provider_for_url(url) 87 | except OEmbedMissingEndpoint: 88 | oembeds = None 89 | rendered = None 90 | else: 91 | oembeds = url 92 | rendered = client.parse_text(url, width, height, context=ctx, template_dir=template_dir) 93 | 94 | output[url] = { 95 | 'oembeds': oembeds, 96 | 'rendered': rendered, 97 | } 98 | 99 | return HttpResponse(simplejson.dumps(output), mimetype='application/json') 100 | 101 | def oembed_schema(request): 102 | """ 103 | A site profile detailing valid endpoints for a given domain. Allows for 104 | better auto-discovery of embeddable content. 105 | 106 | OEmbed-able content lives at a URL that maps to a provider. 107 | """ 108 | current_domain = Site.objects.get_current().domain 109 | url_schemes = [] # a list of dictionaries for all the urls we can match 110 | endpoint = reverse('oembed_json') # the public endpoint for our oembeds 111 | providers = oembed.site.get_providers() 112 | 113 | for provider in providers: 114 | # first make sure this provider class is exposed at the public endpoint 115 | if not provider.provides: 116 | continue 117 | 118 | match = None 119 | if isinstance(provider, DjangoProvider): 120 | # django providers define their regex_list by using urlreversing 121 | url_pattern = resolver.reverse_dict.get(provider._meta.named_view) 122 | 123 | # this regex replacement is set to be non-greedy, which results 124 | # in things like /news/*/*/*/*/ -- this is more explicit 125 | if url_pattern: 126 | regex = re.sub(r'%\(.+?\)s', '*', url_pattern[0][0][0]) 127 | match = 'http://%s/%s' % (current_domain, regex) 128 | elif isinstance(provider, HTTPProvider): 129 | match = provider.url_scheme 130 | else: 131 | match = provider.regex 132 | 133 | if match: 134 | url_schemes.append({ 135 | 'type': provider.resource_type, 136 | 'matches': match, 137 | 'endpoint': endpoint 138 | }) 139 | 140 | url_schemes.sort(key=lambda item: item['matches']) 141 | 142 | response = HttpResponse(mimetype='application/json') 143 | response.write(simplejson.dumps(url_schemes)) 144 | return response 145 | -------------------------------------------------------------------------------- /pip_requirements.txt: -------------------------------------------------------------------------------- 1 | PIL 2 | BeautifulSoup 3 | httplib2 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from os.path import dirname, abspath 5 | 6 | from django.conf import settings 7 | 8 | if not settings.configured: 9 | settings.configure( 10 | DATABASE_ENGINE='sqlite3', 11 | ROOT_URLCONF='oembed.tests.urls', 12 | SITE_ID=1, 13 | INSTALLED_APPS=[ 14 | 'django.contrib.auth', 15 | 'django.contrib.admin', 16 | 'django.contrib.contenttypes', 17 | 'django.contrib.sessions', 18 | 'django.contrib.sites', 19 | 'oembed.tests', 20 | 'oembed', 21 | ] 22 | ) 23 | 24 | from django.test.simple import run_tests 25 | 26 | 27 | def runtests(*test_args): 28 | if not test_args: 29 | test_args = ['oembed'] 30 | parent = dirname(abspath(__file__)) 31 | sys.path.insert(0, parent) 32 | failures = run_tests(test_args, verbosity=1, interactive=True) 33 | sys.exit(failures) 34 | 35 | 36 | if __name__ == '__main__': 37 | runtests(*sys.argv[1:]) 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | f = open(os.path.join(os.path.dirname(__file__), 'README.rst')) 5 | readme = f.read() 6 | f.close() 7 | 8 | setup( 9 | name='djangoembed', 10 | version='0.1.1', 11 | description='rich media consuming/providing for django', 12 | long_description=readme, 13 | author='Charles Leifer', 14 | author_email='coleifer@gmail.com', 15 | url='http://github.com/worldcompany/djangoembed/tree/master', 16 | packages=find_packages(), 17 | package_data = { 18 | 'oembed': [ 19 | 'fixtures/*.json', 20 | 'templates/*.html', 21 | 'templates/*/*.html', 22 | 'templates/*/*/*.html', 23 | 'tests/fixtures/*.json', 24 | 'tests/templates/*.html', 25 | 'tests/templates/*/*.html', 26 | 'tests/templates/*/*/*.html', 27 | ], 28 | }, 29 | install_requires = [ 30 | 'PIL', 31 | 'BeautifulSoup', 32 | 'httplib2', 33 | ], 34 | classifiers=[ 35 | 'Development Status :: 4 - Beta', 36 | 'Environment :: Web Environment', 37 | 'Intended Audience :: Developers', 38 | 'License :: OSI Approved :: MIT License', 39 | 'Operating System :: OS Independent', 40 | 'Programming Language :: Python', 41 | 'Framework :: Django', 42 | ], 43 | test_suite='runtests.runtests', 44 | ) 45 | --------------------------------------------------------------------------------