Commit a2be52fd authored by Justin Bronn's avatar Justin Bronn
Browse files

Fixed #6547, added support for GeoRSS feeds in `django.contrib.gis.feeds`;...

Fixed #6547, added support for GeoRSS feeds in `django.contrib.gis.feeds`; 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
parent c127f011
Loading
Loading
Loading
Loading
+135 −0
Original line number Diff line number Diff line
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)}
+27 −10
Original line number Diff line number Diff line
import sys
from copy import copy
from unittest import TestSuite, TextTestRunner

from django.conf import settings
@@ -94,18 +93,34 @@ def run_tests(module_list, verbosity=1, interactive=True):
    from django.contrib.gis.db.backend import create_spatial_db
    from django.contrib.gis.tests.utils import mysql
    from django.db import connection
    from django.db.models import loading

    # Getting initial values.
    old_debug = settings.DEBUG
    old_name = copy(settings.DATABASE_NAME)
    old_installed = copy(settings.INSTALLED_APPS)
    new_installed = copy(settings.INSTALLED_APPS)
    old_name = settings.DATABASE_NAME
    old_installed = settings.INSTALLED_APPS
    old_root_urlconf = settings.ROOT_URLCONF

    # Based on ALWAYS_INSTALLED_APPS from django test suite --
    # this prevents us from creating tables in our test database
    # from locally installed apps.
    new_installed =  ['django.contrib.contenttypes',
                      'django.contrib.auth',
                      'django.contrib.sites',
                      'django.contrib.flatpages',
                      'django.contrib.gis',
                      'django.contrib.redirects',
                      'django.contrib.sessions',
                      'django.contrib.comments',
                      'django.contrib.admin',
                      ]

    # Setting the URLs.
    settings.ROOT_URLCONF = 'django.contrib.gis.tests.urls'

    # Want DEBUG to be set to False.
    settings.DEBUG = False

    from django.db.models import loading

    # Creating the test suite, adding the test models to INSTALLED_APPS, and
    # adding the model test suites to our suite package.
    test_suite, test_models = geo_suite()
@@ -117,8 +132,9 @@ def run_tests(module_list, verbosity=1, interactive=True):
            test_module_name = 'tests'
        new_installed.append(module_name)

        # Getting the test suite
        tsuite = getattr(__import__('django.contrib.gis.tests.%s' % test_model, globals(), locals(), [test_module_name]), test_module_name)
        # Getting the model test suite
        tsuite = getattr(__import__('django.contrib.gis.tests.%s' % test_model, globals(), locals(), [test_module_name]), 
                         test_module_name)
        test_suite.addTest(tsuite.suite())
    
    # Resetting the loaded flag to take into account what we appended to 
@@ -138,6 +154,7 @@ def run_tests(module_list, verbosity=1, interactive=True):
    connection.creation.destroy_test_db(old_name, verbosity)
    settings.DEBUG = old_debug
    settings.INSTALLED_APPS = old_installed
    settings.ROOT_URLCONF = old_root_urlconf

    # Returning the total failures and errors
    return len(result.failures) + len(result.errors)
+52 −0
Original line number Diff line number Diff line
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)))
+76 −0
Original line number Diff line number Diff line
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>
+3 −1
Original line number Diff line number Diff line
@@ -372,8 +372,8 @@ class GeoModelTest(unittest.TestCase):
        for c in qs: self.assertEqual(True, c.name in cities)

    def test14_equals(self):
        if DISABLE: return
        "Testing the 'same_as' and 'equals' lookup types."
        if DISABLE: return
        pnt = fromstr('POINT (-95.363151 29.763374)', srid=4326)
        c1 = City.objects.get(point=pnt)
        c2 = City.objects.get(point__same_as=pnt)
@@ -558,7 +558,9 @@ class GeoModelTest(unittest.TestCase):
            self.assertEqual(c.mpoly.sym_difference(geom), c.sym_difference)
            self.assertEqual(c.mpoly.union(geom), c.union)

from test_feeds import GeoFeedTest
def suite():
    s = unittest.TestSuite()
    s.addTest(unittest.makeSuite(GeoModelTest))
    s.addTest(unittest.makeSuite(GeoFeedTest))
    return s
Loading