Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed #6547, added support for GeoRSS feeds in `django.contrib.gis.fe…
…eds`; added the `feed_extra_kwargs` and `item_extra_kwargs` to the `Feed` baseclass so that it's possible for subclasses to add dynamic attributes. git-svn-id: http://code.djangoproject.com/svn/django/trunk@8414 bcc190cf-cafb-0310-a4f2-bffc1f526a37
- Loading branch information
Showing
9 changed files
with
333 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
from django.contrib.syndication.feeds import Feed as BaseFeed, FeedDoesNotExist | ||
from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed | ||
|
||
class GeoFeedMixin(object): | ||
""" | ||
This mixin provides the necessary routines for SyndicationFeed subclasses | ||
to produce simple GeoRSS or W3C Geo elements. | ||
""" | ||
|
||
def georss_coords(self, coords): | ||
""" | ||
In GeoRSS coordinate pairs are ordered by lat/lon and separated by | ||
a single white space. Given a tuple of coordinates, this will return | ||
a unicode GeoRSS representation. | ||
""" | ||
return u' '.join([u'%f %f' % (coord[1], coord[0]) for coord in coords]) | ||
|
||
def add_georss_point(self, handler, coords, w3c_geo=False): | ||
""" | ||
Adds a GeoRSS point with the given coords using the given handler. | ||
Handles the differences between simple GeoRSS and the more pouplar | ||
W3C Geo specification. | ||
""" | ||
if w3c_geo: | ||
lon, lat = coords[:2] | ||
handler.addQuickElement(u'geo:lat', u'%f' % lat) | ||
handler.addQuickElement(u'geo:lon', u'%f' % lon) | ||
else: | ||
handler.addQuickElement(u'georss:point', self.georss_coords((coords,))) | ||
|
||
def add_georss_element(self, handler, item, w3c_geo=False): | ||
""" | ||
This routine adds a GeoRSS XML element using the given item and handler. | ||
""" | ||
# Getting the Geometry object. | ||
geom = item.get('geometry', None) | ||
if not geom is None: | ||
if isinstance(geom, (list, tuple)): | ||
# Special case if a tuple/list was passed in. The tuple may be | ||
# a point or a box | ||
box_coords = None | ||
if isinstance(geom[0], (list, tuple)): | ||
# Box: ( (X0, Y0), (X1, Y1) ) | ||
if len(geom) == 2: | ||
box_coords = geom | ||
else: | ||
raise ValueError('Only should be two sets of coordinates.') | ||
else: | ||
if len(geom) == 2: | ||
# Point: (X, Y) | ||
self.add_georss_point(handler, geom, w3c_geo=w3c_geo) | ||
elif len(geom) == 4: | ||
# Box: (X0, Y0, X1, Y1) | ||
box_coords = (geom[:2], geom[2:]) | ||
else: | ||
raise ValueError('Only should be 2 or 4 numeric elements.') | ||
# If a GeoRSS box was given via tuple. | ||
if not box_coords is None: | ||
if w3c_geo: raise ValueError('Cannot use simple GeoRSS box in W3C Geo feeds.') | ||
handler.addQuickElement(u'georss:box', self.georss_coords(box_coords)) | ||
else: | ||
# Getting the lower-case geometry type. | ||
gtype = str(geom.geom_type).lower() | ||
if gtype == 'point': | ||
self.add_georss_point(handler, geom.coords, w3c_geo=w3c_geo) | ||
else: | ||
if w3c_geo: raise ValueError('W3C Geo only supports Point geometries.') | ||
# For formatting consistent w/the GeoRSS simple standard: | ||
# http://georss.org/1.0#simple | ||
if gtype in ('linestring', 'linearring'): | ||
handler.addQuickElement(u'georss:line', self.georss_coords(geom.coords)) | ||
elif gtype in ('polygon',): | ||
# Only support the exterior ring. | ||
handler.addQuickElement(u'georss:polygon', self.georss_coords(geom[0].coords)) | ||
else: | ||
raise ValueError('Geometry type "%s" not supported.' % geom.geom_type) | ||
|
||
### SyndicationFeed subclasses ### | ||
class GeoRSSFeed(Rss201rev2Feed, GeoFeedMixin): | ||
def rss_attributes(self): | ||
attrs = super(GeoRSSFeed, self).rss_attributes() | ||
attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' | ||
return attrs | ||
|
||
def add_item_elements(self, handler, item): | ||
super(GeoRSSFeed, self).add_item_elements(handler, item) | ||
self.add_georss_element(handler, item) | ||
|
||
def add_root_elements(self, handler): | ||
super(GeoRSSFeed, self).add_root_elements(handler) | ||
self.add_georss_element(handler, self.feed) | ||
|
||
class GeoAtom1Feed(Atom1Feed, GeoFeedMixin): | ||
def root_attributes(self): | ||
attrs = super(GeoAtom1Feed, self).root_attributes() | ||
attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' | ||
return attrs | ||
|
||
def add_item_elements(self, handler, item): | ||
super(GeoAtom1Feed, self).add_item_elements(handler, item) | ||
self.add_georss_element(handler, item) | ||
|
||
def add_root_elements(self, handler): | ||
super(GeoAtom1Feed, self).add_root_elements(handler) | ||
self.add_georss_element(handler, self.feed) | ||
|
||
class W3CGeoFeed(Rss201rev2Feed, GeoFeedMixin): | ||
def rss_attributes(self): | ||
attrs = super(W3CGeoFeed, self).rss_attributes() | ||
attrs[u'xmlns:geo'] = u'http://www.w3.org/2003/01/geo/wgs84_pos#' | ||
return attrs | ||
|
||
def add_item_elements(self, handler, item): | ||
super(W3CGeoFeed, self).add_item_elements(handler, item) | ||
self.add_georss_element(handler, item, w3c_geo=True) | ||
|
||
def add_root_elements(self, handler): | ||
super(W3CGeoFeed, self).add_root_elements(handler) | ||
self.add_georss_element(handler, self.feed, w3c_geo=True) | ||
|
||
### Feed subclass ### | ||
class Feed(BaseFeed): | ||
""" | ||
This is a subclass of the `Feed` from `django.contrib.syndication`. | ||
This allows users to define a `geometry(obj)` and/or `item_geometry(item)` | ||
methods on their own subclasses so that geo-referenced information may | ||
placed in the feed. | ||
""" | ||
feed_type = GeoRSSFeed | ||
|
||
def feed_extra_kwargs(self, obj): | ||
return {'geometry' : self.__get_dynamic_attr('geometry', obj)} | ||
|
||
def item_extra_kwargs(self, item): | ||
return {'geometry' : self.__get_dynamic_attr('item_geometry', item)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
from django.contrib.gis import feeds | ||
from django.contrib.gis.tests.utils import mysql | ||
from models import City, Country | ||
|
||
class TestGeoRSS1(feeds.Feed): | ||
link = '/city/' | ||
title = 'Test GeoDjango Cities' | ||
|
||
def items(self): | ||
return City.objects.all() | ||
|
||
def item_link(self, item): | ||
return '/city/%s/' % item.pk | ||
|
||
def item_geometry(self, item): | ||
return item.point | ||
|
||
class TestGeoRSS2(TestGeoRSS1): | ||
def geometry(self, obj): | ||
# This should attach a <georss:box> element for the extent of | ||
# of the cities in the database. This tuple came from | ||
# calling `City.objects.extent()` -- we can't do that call here | ||
# because `extent` is not implemented for MySQL/Oracle. | ||
return (-123.30, -41.32, 174.78, 48.46) | ||
|
||
def item_geometry(self, item): | ||
# Returning a simple tuple for the geometry. | ||
return item.point.x, item.point.y | ||
|
||
class TestGeoAtom1(TestGeoRSS1): | ||
feed_type = feeds.GeoAtom1Feed | ||
|
||
class TestGeoAtom2(TestGeoRSS2): | ||
feed_type = feeds.GeoAtom1Feed | ||
|
||
def geometry(self, obj): | ||
# This time we'll use a 2-tuple of coordinates for the box. | ||
return ((-123.30, -41.32), (174.78, 48.46)) | ||
|
||
class TestW3CGeo1(TestGeoRSS1): | ||
feed_type = feeds.W3CGeoFeed | ||
|
||
# The following feeds are invalid, and will raise exceptions. | ||
class TestW3CGeo2(TestGeoRSS2): | ||
feed_type = feeds.W3CGeoFeed | ||
|
||
class TestW3CGeo3(TestGeoRSS1): | ||
feed_type = feeds.W3CGeoFeed | ||
|
||
def item_geometry(self, item): | ||
from django.contrib.gis.geos import Polygon | ||
return Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import unittest | ||
from xml.dom import minidom | ||
|
||
from django.test import Client | ||
from models import City | ||
|
||
class GeoFeedTest(unittest.TestCase): | ||
client = Client() | ||
|
||
def assertChildNodes(self, elem, expected): | ||
"Taken from regressiontests/syndication/tests.py." | ||
actual = set([n.nodeName for n in elem.childNodes]) | ||
expected = set(expected) | ||
self.assertEqual(actual, expected) | ||
|
||
def test_geofeed_rss(self): | ||
"Tests geographic feeds using GeoRSS over RSSv2." | ||
# Uses `GEOSGeometry` in `item_geometry` | ||
doc1 = minidom.parseString(self.client.get('/geoapp/feeds/rss1/').content) | ||
# Uses a 2-tuple in `item_geometry` | ||
doc2 = minidom.parseString(self.client.get('/geoapp/feeds/rss2/').content) | ||
feed1, feed2 = doc1.firstChild, doc2.firstChild | ||
|
||
# Making sure the box got added to the second GeoRSS feed. | ||
self.assertChildNodes(feed2.getElementsByTagName('channel')[0], | ||
['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'georss:box'] | ||
) | ||
|
||
# Incrementing through the feeds. | ||
for feed in [feed1, feed2]: | ||
# Ensuring the georss namespace was added to the <rss> element. | ||
self.assertEqual(feed.getAttribute(u'xmlns:georss'), u'http://www.georss.org/georss') | ||
chan = feed.getElementsByTagName('channel')[0] | ||
items = chan.getElementsByTagName('item') | ||
self.assertEqual(len(items), City.objects.count()) | ||
|
||
# Ensuring the georss element was added to each item in the feed. | ||
for item in items: | ||
self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'georss:point']) | ||
|
||
def test_geofeed_atom(self): | ||
"Testing geographic feeds using GeoRSS over Atom." | ||
doc1 = minidom.parseString(self.client.get('/geoapp/feeds/atom1/').content) | ||
doc2 = minidom.parseString(self.client.get('/geoapp/feeds/atom2/').content) | ||
feed1, feed2 = doc1.firstChild, doc2.firstChild | ||
|
||
# Making sure the box got added to the second GeoRSS feed. | ||
self.assertChildNodes(feed2, ['title', 'link', 'id', 'updated', 'entry', 'georss:box']) | ||
|
||
for feed in [feed1, feed2]: | ||
# Ensuring the georsss namespace was added to the <feed> element. | ||
self.assertEqual(feed.getAttribute(u'xmlns:georss'), u'http://www.georss.org/georss') | ||
entries = feed.getElementsByTagName('entry') | ||
self.assertEqual(len(entries), City.objects.count()) | ||
|
||
# Ensuring the georss element was added to each entry in the feed. | ||
for entry in entries: | ||
self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'georss:point']) | ||
|
||
def test_geofeed_w3c(self): | ||
"Testing geographic feeds using W3C Geo." | ||
doc = minidom.parseString(self.client.get('/geoapp/feeds/w3cgeo1/').content) | ||
feed = doc.firstChild | ||
# Ensuring the geo namespace was added to the <feed> element. | ||
self.assertEqual(feed.getAttribute(u'xmlns:geo'), u'http://www.w3.org/2003/01/geo/wgs84_pos#') | ||
chan = feed.getElementsByTagName('channel')[0] | ||
items = chan.getElementsByTagName('item') | ||
self.assertEqual(len(items), City.objects.count()) | ||
|
||
# Ensuring the geo:lat and geo:lon element was added to each item in the feed. | ||
for item in items: | ||
self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'geo:lat', 'geo:lon']) | ||
|
||
# Boxes and Polygons aren't allowed in W3C Geo feeds. | ||
self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo2/') # Box in <channel> | ||
self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo3/') # Polygons in <entry> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from django.conf.urls.defaults import * | ||
from feeds import TestGeoRSS1, TestGeoRSS2, TestGeoAtom1, TestGeoAtom2, TestW3CGeo1, TestW3CGeo2, TestW3CGeo3 | ||
|
||
feed_dict = { | ||
'rss1' : TestGeoRSS1, | ||
'rss2' : TestGeoRSS2, | ||
'atom1' : TestGeoAtom1, | ||
'atom2' : TestGeoAtom2, | ||
'w3cgeo1' : TestW3CGeo1, | ||
'w3cgeo2' : TestW3CGeo2, | ||
'w3cgeo3' : TestW3CGeo3, | ||
} | ||
|
||
urlpatterns = patterns('', | ||
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict}) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
from django.conf.urls.defaults import * | ||
|
||
urlpatterns = patterns('', | ||
(r'^geoapp/', include('django.contrib.gis.tests.geoapp.urls')), | ||
) | ||
|
Oops, something went wrong.