Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

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...
commit a2be52fd2a27b5a39744939c906cd57507aa8f10 1 parent c127f01
Justin Bronn authored August 16, 2008
135  django/contrib/gis/feeds.py
... ...
@@ -0,0 +1,135 @@
  1
+from django.contrib.syndication.feeds import Feed as BaseFeed, FeedDoesNotExist
  2
+from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
  3
+
  4
+class GeoFeedMixin(object):
  5
+    """
  6
+    This mixin provides the necessary routines for SyndicationFeed subclasses
  7
+    to produce simple GeoRSS or W3C Geo elements.
  8
+    """
  9
+
  10
+    def georss_coords(self, coords):
  11
+        """
  12
+        In GeoRSS coordinate pairs are ordered by lat/lon and separated by
  13
+        a single white space.  Given a tuple of coordinates, this will return
  14
+        a unicode GeoRSS representation.
  15
+        """
  16
+        return u' '.join([u'%f %f' % (coord[1], coord[0]) for coord in coords])
  17
+
  18
+    def add_georss_point(self, handler, coords, w3c_geo=False):
  19
+        """
  20
+        Adds a GeoRSS point with the given coords using the given handler.
  21
+        Handles the differences between simple GeoRSS and the more pouplar
  22
+        W3C Geo specification.
  23
+        """
  24
+        if w3c_geo:
  25
+            lon, lat = coords[:2]
  26
+            handler.addQuickElement(u'geo:lat', u'%f' % lat)
  27
+            handler.addQuickElement(u'geo:lon', u'%f' % lon)
  28
+        else:
  29
+            handler.addQuickElement(u'georss:point', self.georss_coords((coords,)))
  30
+
  31
+    def add_georss_element(self, handler, item, w3c_geo=False):
  32
+        """
  33
+        This routine adds a GeoRSS XML element using the given item and handler.
  34
+        """
  35
+        # Getting the Geometry object.
  36
+        geom = item.get('geometry', None)
  37
+        if not geom is None:
  38
+            if isinstance(geom, (list, tuple)):
  39
+                # Special case if a tuple/list was passed in.  The tuple may be
  40
+                # a point or a box
  41
+                box_coords = None
  42
+                if isinstance(geom[0], (list, tuple)):
  43
+                    # Box: ( (X0, Y0), (X1, Y1) )
  44
+                    if len(geom) == 2:
  45
+                        box_coords = geom
  46
+                    else:
  47
+                        raise ValueError('Only should be two sets of coordinates.')
  48
+                else:
  49
+                    if len(geom) == 2:
  50
+                        # Point: (X, Y)
  51
+                        self.add_georss_point(handler, geom, w3c_geo=w3c_geo)
  52
+                    elif len(geom) == 4:
  53
+                        # Box: (X0, Y0, X1, Y1)
  54
+                        box_coords = (geom[:2], geom[2:])
  55
+                    else:
  56
+                        raise ValueError('Only should be 2 or 4 numeric elements.')
  57
+                # If a GeoRSS box was given via tuple.
  58
+                if not box_coords is None:
  59
+                    if w3c_geo: raise ValueError('Cannot use simple GeoRSS box in W3C Geo feeds.')
  60
+                    handler.addQuickElement(u'georss:box', self.georss_coords(box_coords))
  61
+            else:
  62
+                # Getting the lower-case geometry type.
  63
+                gtype = str(geom.geom_type).lower()
  64
+                if gtype == 'point':
  65
+                    self.add_georss_point(handler, geom.coords, w3c_geo=w3c_geo) 
  66
+                else:
  67
+                    if w3c_geo: raise ValueError('W3C Geo only supports Point geometries.')
  68
+                    # For formatting consistent w/the GeoRSS simple standard:
  69
+                    # http://georss.org/1.0#simple
  70
+                    if gtype in ('linestring', 'linearring'):
  71
+                        handler.addQuickElement(u'georss:line', self.georss_coords(geom.coords))
  72
+                    elif gtype in ('polygon',):
  73
+                        # Only support the exterior ring.
  74
+                        handler.addQuickElement(u'georss:polygon', self.georss_coords(geom[0].coords))
  75
+                    else:
  76
+                        raise ValueError('Geometry type "%s" not supported.' % geom.geom_type)
  77
+
  78
+### SyndicationFeed subclasses ###
  79
+class GeoRSSFeed(Rss201rev2Feed, GeoFeedMixin):
  80
+    def rss_attributes(self):
  81
+        attrs = super(GeoRSSFeed, self).rss_attributes()
  82
+        attrs[u'xmlns:georss'] = u'http://www.georss.org/georss'
  83
+        return attrs
  84
+
  85
+    def add_item_elements(self, handler, item):
  86
+        super(GeoRSSFeed, self).add_item_elements(handler, item)
  87
+        self.add_georss_element(handler, item)
  88
+
  89
+    def add_root_elements(self, handler):
  90
+        super(GeoRSSFeed, self).add_root_elements(handler)
  91
+        self.add_georss_element(handler, self.feed)
  92
+
  93
+class GeoAtom1Feed(Atom1Feed, GeoFeedMixin):
  94
+    def root_attributes(self):
  95
+        attrs = super(GeoAtom1Feed, self).root_attributes()
  96
+        attrs[u'xmlns:georss'] = u'http://www.georss.org/georss'
  97
+        return attrs
  98
+
  99
+    def add_item_elements(self, handler, item):
  100
+        super(GeoAtom1Feed, self).add_item_elements(handler, item)
  101
+        self.add_georss_element(handler, item)
  102
+
  103
+    def add_root_elements(self, handler):
  104
+        super(GeoAtom1Feed, self).add_root_elements(handler)
  105
+        self.add_georss_element(handler, self.feed)
  106
+
  107
+class W3CGeoFeed(Rss201rev2Feed, GeoFeedMixin):
  108
+    def rss_attributes(self):
  109
+        attrs = super(W3CGeoFeed, self).rss_attributes()
  110
+        attrs[u'xmlns:geo'] = u'http://www.w3.org/2003/01/geo/wgs84_pos#'
  111
+        return attrs
  112
+
  113
+    def add_item_elements(self, handler, item):
  114
+        super(W3CGeoFeed, self).add_item_elements(handler, item)
  115
+        self.add_georss_element(handler, item, w3c_geo=True)
  116
+
  117
+    def add_root_elements(self, handler):
  118
+        super(W3CGeoFeed, self).add_root_elements(handler)
  119
+        self.add_georss_element(handler, self.feed, w3c_geo=True)
  120
+
  121
+### Feed subclass ###
  122
+class Feed(BaseFeed):
  123
+    """
  124
+    This is a subclass of the `Feed` from `django.contrib.syndication`.
  125
+    This allows users to define a `geometry(obj)` and/or `item_geometry(item)`
  126
+    methods on their own subclasses so that geo-referenced information may
  127
+    placed in the feed.
  128
+    """
  129
+    feed_type = GeoRSSFeed
  130
+
  131
+    def feed_extra_kwargs(self, obj):
  132
+        return {'geometry' : self.__get_dynamic_attr('geometry', obj)}
  133
+
  134
+    def item_extra_kwargs(self, item):
  135
+        return {'geometry' : self.__get_dynamic_attr('item_geometry', item)}
37  django/contrib/gis/tests/__init__.py
... ...
@@ -1,5 +1,4 @@
1 1
 import sys
2  
-from copy import copy
3 2
 from unittest import TestSuite, TextTestRunner
4 3
 
5 4
 from django.conf import settings
@@ -94,20 +93,36 @@ def run_tests(module_list, verbosity=1, interactive=True):
94 93
     from django.contrib.gis.db.backend import create_spatial_db
95 94
     from django.contrib.gis.tests.utils import mysql
96 95
     from django.db import connection
  96
+    from django.db.models import loading
97 97
 
98 98
     # Getting initial values.
99 99
     old_debug = settings.DEBUG
100  
-    old_name = copy(settings.DATABASE_NAME)
101  
-    old_installed = copy(settings.INSTALLED_APPS)
102  
-    new_installed = copy(settings.INSTALLED_APPS)
  100
+    old_name = settings.DATABASE_NAME
  101
+    old_installed = settings.INSTALLED_APPS
  102
+    old_root_urlconf = settings.ROOT_URLCONF
  103
+
  104
+    # Based on ALWAYS_INSTALLED_APPS from django test suite --
  105
+    # this prevents us from creating tables in our test database
  106
+    # from locally installed apps.
  107
+    new_installed =  ['django.contrib.contenttypes',
  108
+                      'django.contrib.auth',
  109
+                      'django.contrib.sites',
  110
+                      'django.contrib.flatpages',
  111
+                      'django.contrib.gis',
  112
+                      'django.contrib.redirects',
  113
+                      'django.contrib.sessions',
  114
+                      'django.contrib.comments',
  115
+                      'django.contrib.admin',
  116
+                      ]
  117
+
  118
+    # Setting the URLs.
  119
+    settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls'
103 120
 
104 121
     # Want DEBUG to be set to False.
105 122
     settings.DEBUG = False
106 123
 
107  
-    from django.db.models import loading
108  
-
109 124
     # Creating the test suite, adding the test models to INSTALLED_APPS, and
110  
-    #  adding the model test suites to our suite package.
  125
+    # adding the model test suites to our suite package.
111 126
     test_suite, test_models = geo_suite()
112 127
     for test_model in test_models:
113 128
         module_name = 'django.contrib.gis.tests.%s' % test_model
@@ -117,8 +132,9 @@ def run_tests(module_list, verbosity=1, interactive=True):
117 132
             test_module_name = 'tests'
118 133
         new_installed.append(module_name)
119 134
 
120  
-        # Getting the test suite
121  
-        tsuite = getattr(__import__('django.contrib.gis.tests.%s' % test_model, globals(), locals(), [test_module_name]), test_module_name)
  135
+        # Getting the model test suite
  136
+        tsuite = getattr(__import__('django.contrib.gis.tests.%s' % test_model, globals(), locals(), [test_module_name]), 
  137
+                         test_module_name)
122 138
         test_suite.addTest(tsuite.suite())
123 139
     
124 140
     # Resetting the loaded flag to take into account what we appended to 
@@ -138,7 +154,8 @@ def run_tests(module_list, verbosity=1, interactive=True):
138 154
     connection.creation.destroy_test_db(old_name, verbosity)
139 155
     settings.DEBUG = old_debug
140 156
     settings.INSTALLED_APPS = old_installed
141  
-    
  157
+    settings.ROOT_URLCONF = old_root_urlconf
  158
+
142 159
     # Returning the total failures and errors
143 160
     return len(result.failures) + len(result.errors)
144 161
 
52  django/contrib/gis/tests/geoapp/feeds.py
... ...
@@ -0,0 +1,52 @@
  1
+from django.contrib.gis import feeds
  2
+from django.contrib.gis.tests.utils import mysql
  3
+from models import City, Country
  4
+
  5
+class TestGeoRSS1(feeds.Feed):
  6
+    link = '/city/'
  7
+    title = 'Test GeoDjango Cities'
  8
+
  9
+    def items(self):
  10
+        return City.objects.all()
  11
+
  12
+    def item_link(self, item):
  13
+        return '/city/%s/' % item.pk
  14
+
  15
+    def item_geometry(self, item):
  16
+        return item.point
  17
+
  18
+class TestGeoRSS2(TestGeoRSS1):
  19
+    def geometry(self, obj):
  20
+        # This should attach a <georss:box> element for the extent of
  21
+        # of the cities in the database.  This tuple came from
  22
+        # calling `City.objects.extent()` -- we can't do that call here
  23
+        # because `extent` is not implemented for MySQL/Oracle.
  24
+        return (-123.30, -41.32, 174.78, 48.46)
  25
+
  26
+    def item_geometry(self, item):
  27
+        # Returning a simple tuple for the geometry.
  28
+        return item.point.x, item.point.y
  29
+
  30
+class TestGeoAtom1(TestGeoRSS1):
  31
+    feed_type = feeds.GeoAtom1Feed
  32
+
  33
+class TestGeoAtom2(TestGeoRSS2):
  34
+    feed_type = feeds.GeoAtom1Feed
  35
+
  36
+    def geometry(self, obj):
  37
+        # This time we'll use a 2-tuple of coordinates for the box.
  38
+        return ((-123.30, -41.32), (174.78, 48.46))
  39
+
  40
+class TestW3CGeo1(TestGeoRSS1):
  41
+    feed_type = feeds.W3CGeoFeed
  42
+
  43
+# The following feeds are invalid, and will raise exceptions.
  44
+class TestW3CGeo2(TestGeoRSS2):
  45
+    feed_type = feeds.W3CGeoFeed
  46
+
  47
+class TestW3CGeo3(TestGeoRSS1):
  48
+    feed_type = feeds.W3CGeoFeed
  49
+
  50
+    def item_geometry(self, item):
  51
+        from django.contrib.gis.geos import Polygon
  52
+        return Polygon(((0, 0), (0, 1), (1, 1), (1, 0), (0, 0)))
76  django/contrib/gis/tests/geoapp/test_feeds.py
... ...
@@ -0,0 +1,76 @@
  1
+import unittest
  2
+from xml.dom import minidom
  3
+
  4
+from django.test import Client
  5
+from models import City
  6
+
  7
+class GeoFeedTest(unittest.TestCase):
  8
+    client = Client()
  9
+
  10
+    def assertChildNodes(self, elem, expected):
  11
+        "Taken from regressiontests/syndication/tests.py."
  12
+        actual = set([n.nodeName for n in elem.childNodes])
  13
+        expected = set(expected)
  14
+        self.assertEqual(actual, expected)
  15
+
  16
+    def test_geofeed_rss(self):
  17
+        "Tests geographic feeds using GeoRSS over RSSv2."
  18
+        # Uses `GEOSGeometry` in `item_geometry`
  19
+        doc1 = minidom.parseString(self.client.get('/geoapp/feeds/rss1/').content)
  20
+        # Uses a 2-tuple in `item_geometry`
  21
+        doc2 = minidom.parseString(self.client.get('/geoapp/feeds/rss2/').content) 
  22
+        feed1, feed2 = doc1.firstChild, doc2.firstChild
  23
+
  24
+        # Making sure the box got added to the second GeoRSS feed.
  25
+        self.assertChildNodes(feed2.getElementsByTagName('channel')[0], 
  26
+                              ['title', 'link', 'description', 'language', 'lastBuildDate', 'item', 'georss:box']
  27
+                              )
  28
+        
  29
+        # Incrementing through the feeds.
  30
+        for feed in [feed1, feed2]:
  31
+            # Ensuring the georss namespace was added to the <rss> element.
  32
+            self.assertEqual(feed.getAttribute(u'xmlns:georss'),  u'http://www.georss.org/georss')
  33
+            chan = feed.getElementsByTagName('channel')[0]
  34
+            items = chan.getElementsByTagName('item')
  35
+            self.assertEqual(len(items), City.objects.count())
  36
+                
  37
+            # Ensuring the georss element was added to each item in the feed.
  38
+            for item in items:
  39
+                self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'georss:point'])
  40
+
  41
+    def test_geofeed_atom(self):
  42
+        "Testing geographic feeds using GeoRSS over Atom."
  43
+        doc1 = minidom.parseString(self.client.get('/geoapp/feeds/atom1/').content)
  44
+        doc2 = minidom.parseString(self.client.get('/geoapp/feeds/atom2/').content)
  45
+        feed1, feed2 = doc1.firstChild, doc2.firstChild
  46
+
  47
+        # Making sure the box got added to the second GeoRSS feed.
  48
+        self.assertChildNodes(feed2, ['title', 'link', 'id', 'updated', 'entry', 'georss:box'])        
  49
+
  50
+        for feed in [feed1, feed2]:
  51
+            # Ensuring the georsss namespace was added to the <feed> element.
  52
+            self.assertEqual(feed.getAttribute(u'xmlns:georss'),  u'http://www.georss.org/georss')
  53
+            entries = feed.getElementsByTagName('entry')
  54
+            self.assertEqual(len(entries), City.objects.count())
  55
+            
  56
+            # Ensuring the georss element was added to each entry in the feed.
  57
+            for entry in entries:
  58
+                self.assertChildNodes(entry, ['title', 'link', 'id', 'summary', 'georss:point'])
  59
+
  60
+    def test_geofeed_w3c(self):
  61
+        "Testing geographic feeds using W3C Geo."
  62
+        doc = minidom.parseString(self.client.get('/geoapp/feeds/w3cgeo1/').content)
  63
+        feed = doc.firstChild
  64
+        # Ensuring the geo namespace was added to the <feed> element.
  65
+        self.assertEqual(feed.getAttribute(u'xmlns:geo'), u'http://www.w3.org/2003/01/geo/wgs84_pos#')
  66
+        chan = feed.getElementsByTagName('channel')[0]
  67
+        items = chan.getElementsByTagName('item')
  68
+        self.assertEqual(len(items), City.objects.count())
  69
+
  70
+        # Ensuring the geo:lat and geo:lon element was added to each item in the feed.
  71
+        for item in items:
  72
+            self.assertChildNodes(item, ['title', 'link', 'description', 'guid', 'geo:lat', 'geo:lon'])
  73
+
  74
+        # Boxes and Polygons aren't allowed in W3C Geo feeds.
  75
+        self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo2/') # Box in <channel>
  76
+        self.assertRaises(ValueError, self.client.get, '/geoapp/feeds/w3cgeo3/') # Polygons in <entry>
4  django/contrib/gis/tests/geoapp/tests.py
@@ -372,8 +372,8 @@ def test13_left_right(self):
372 372
         for c in qs: self.assertEqual(True, c.name in cities)
373 373
 
374 374
     def test14_equals(self):
375  
-        if DISABLE: return
376 375
         "Testing the 'same_as' and 'equals' lookup types."
  376
+        if DISABLE: return
377 377
         pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
378 378
         c1 = City.objects.get(point=pnt)
379 379
         c2 = City.objects.get(point__same_as=pnt)
@@ -558,7 +558,9 @@ def test25_geoset(self):
558 558
             self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
559 559
             self.assertEqual(c.mpoly.union(geom), c.union)
560 560
 
  561
+from test_feeds import GeoFeedTest
561 562
 def suite():
562 563
     s = unittest.TestSuite()
563 564
     s.addTest(unittest.makeSuite(GeoModelTest))
  565
+    s.addTest(unittest.makeSuite(GeoFeedTest))
564 566
     return s
2  django/contrib/gis/tests/geoapp/tests_mysql.py
@@ -173,7 +173,9 @@ def test07_mysql_limitations(self):
173 173
         self.assertRaises(ImproperlyConfigured, State.objects.all().kml, field_name='poly')
174 174
         self.assertRaises(ImproperlyConfigured, Country.objects.all().gml, field_name='mpoly')
175 175
 
  176
+from test_feeds import GeoFeedTest
176 177
 def suite():
177 178
     s = unittest.TestSuite()
178 179
     s.addTest(unittest.makeSuite(GeoModelTest))
  180
+    s.addTest(unittest.makeSuite(GeoFeedTest))
179 181
     return s
16  django/contrib/gis/tests/geoapp/urls.py
... ...
@@ -0,0 +1,16 @@
  1
+from django.conf.urls.defaults import *
  2
+from feeds import TestGeoRSS1, TestGeoRSS2, TestGeoAtom1, TestGeoAtom2, TestW3CGeo1, TestW3CGeo2, TestW3CGeo3
  3
+
  4
+feed_dict = {
  5
+    'rss1' : TestGeoRSS1,
  6
+    'rss2' : TestGeoRSS2,
  7
+    'atom1' : TestGeoAtom1,
  8
+    'atom2' : TestGeoAtom2,
  9
+    'w3cgeo1' : TestW3CGeo1,
  10
+    'w3cgeo2' : TestW3CGeo2,
  11
+    'w3cgeo3' : TestW3CGeo3,
  12
+}
  13
+
  14
+urlpatterns = patterns('',
  15
+    (r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feed_dict})
  16
+)
6  django/contrib/gis/tests/urls.py
... ...
@@ -0,0 +1,6 @@
  1
+from django.conf.urls.defaults import *
  2
+
  3
+urlpatterns = patterns('',
  4
+                       (r'^geoapp/', include('django.contrib.gis.tests.geoapp.urls')),
  5
+                       )
  6
+                        
16  django/contrib/syndication/feeds.py
@@ -59,6 +59,20 @@ def __get_dynamic_attr(self, attname, obj, default=None):
59 59
                 return attr()
60 60
         return attr
61 61
 
  62
+    def feed_extra_kwargs(self, obj):
  63
+        """
  64
+        Returns an extra keyword arguments dictionary that is used when
  65
+        initializing the feed generator.
  66
+        """
  67
+        return {}
  68
+
  69
+    def item_extra_kwargs(self, item):
  70
+        """
  71
+        Returns an extra keyword arguments dictionary that is used with
  72
+        the `add_item` call of the feed generator.
  73
+        """
  74
+        return {}
  75
+
62 76
     def get_object(self, bits):
63 77
         return None
64 78
 
@@ -100,6 +114,7 @@ def get_feed(self, url=None):
100 114
             feed_copyright = self.__get_dynamic_attr('feed_copyright', obj),
101 115
             feed_guid = self.__get_dynamic_attr('feed_guid', obj),
102 116
             ttl = self.__get_dynamic_attr('ttl', obj),
  117
+            **self.feed_extra_kwargs(obj)
103 118
         )
104 119
 
105 120
         try:
@@ -158,5 +173,6 @@ def get_feed(self, url=None):
158 173
                 author_link = author_link,
159 174
                 categories = self.__get_dynamic_attr('item_categories', item),
160 175
                 item_copyright = self.__get_dynamic_attr('item_copyright', item),
  176
+                **self.item_extra_kwargs(item)
161 177
             )
162 178
         return feed

0 notes on commit a2be52f

Please sign in to comment.
Something went wrong with that request. Please try again.