Commits

Ratnadeep Debnath committed d1fb021

Bugfix in notices/feed/.

Comments (0)

Files changed (4)

transifex/txcommon/feeds.py

+from datetime import datetime
+
 from django.core.exceptions import ObjectDoesNotExist
 from django.conf import settings
 from django.contrib.syndication.feeds import FeedDoesNotExist
 from django.contrib.auth.models import User
 from django.core.urlresolvers import reverse
 from django.utils.timesince import timesince
+from django.template.defaultfilters import linebreaks, escape, striptags
 
 from transifex.actionlog.models import LogEntry
 import re
 
+from notification.models import Notice
+from txcommon.notification_atomformat import Feed as notification_Feed
+
+
 current_site = Site.objects.get_current()
 
+ITEMS_PER_FEED = getattr(settings, 'ITEMS_PER_FEED', 20)
+DEFAULT_HTTP_PROTOCOL = getattr(settings, "DEFAULT_HTTP_PROTOCOL", "http")
+
+
+class BaseNoticeFeed(notification_Feed):
+    def item_id(self, notification):
+        return "%s://%s%s" % (
+            DEFAULT_HTTP_PROTOCOL,
+            Site.objects.get_current().domain,
+            notification.get_absolute_url(),
+        )
+    
+    def item_title(self, notification):
+        return striptags(notification.message)
+    
+    def item_updated(self, notification):
+        return notification.added
+    
+    def item_published(self, notification):
+        return notification.added
+    
+    def item_content(self, notification):
+        return {"type" : "html", }, linebreaks(escape(notification.message))
+    
+    def item_links(self, notification):
+        return [{"href" : self.item_id(notification)}]
+    
+    def item_authors(self, notification):
+        return [{"name" : notification.user.username}]
+
+
+class NoticeUserFeed(BaseNoticeFeed):
+    def get_object(self, params):
+        return get_object_or_404(User, username=params[0].lower())
+
+    def feed_id(self, user):
+        return "%s://%s%s" % (
+            DEFAULT_HTTP_PROTOCOL,
+            Site.objects.get_current().domain,
+            reverse('notification_feed_for_user'),
+        )
+
+    def feed_title(self, user):
+        return _('Notices Feed')
+
+    def feed_updated(self, user):
+        qs = Notice.objects.filter(user=user)
+        # We return an arbitrary date if there are no results, because there
+        # must be a feed_updated field as per the Atom specifications, however
+        # there is no real data to go by, and an arbitrary date can be static.
+        if qs.count() == 0:
+            return datetime(year=2008, month=7, day=1)
+        return qs.latest('added').added
+
+    def feed_links(self, user):
+        complete_url = "%s://%s%s" % (
+            DEFAULT_HTTP_PROTOCOL,
+            Site.objects.get_current().domain,
+            reverse('notification_notices'),
+        )
+        return ({'href': complete_url},)
+
+    def items(self, user):
+        return Notice.objects.notices_for(user).order_by("-added")[:ITEMS_PER_FEED]
+
 class UserFeed(Feed):
     def get_object(self, request, username, url='feed/admin'):
         if not username:

transifex/txcommon/notification_atomformat.py

+# 
+# django-atompub by James Tauber <http://jtauber.com/>
+# http://code.google.com/p/django-atompub/
+# An implementation of the Atom format and protocol for Django
+# 
+# For instructions on how to use this module to generate Atom feeds,
+# see http://code.google.com/p/django-atompub/wiki/UserGuide
+# 
+# 
+# Copyright (c) 2007, James Tauber
+# 
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# 
+
+from xml.sax.saxutils import XMLGenerator
+from datetime import datetime
+from django.contrib.syndication.feeds import Feed
+
+GENERATOR_TEXT = 'django-atompub'
+GENERATOR_ATTR = {
+    'uri': 'http://code.google.com/p/django-atompub/',
+    'version': 'r33'
+}
+
+
+
+## based on django.utils.xmlutils.SimplerXMLGenerator
+class SimplerXMLGenerator(XMLGenerator):
+    def addQuickElement(self, name, contents=None, attrs=None):
+        "Convenience method for adding an element with no children"
+        if attrs is None: attrs = {}
+        self.startElement(name, attrs)
+        if contents is not None:
+            self.characters(contents)
+        self.endElement(name)
+
+
+
+## based on django.utils.feedgenerator.rfc3339_date
+def rfc3339_date(date):
+    return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+
+
+## based on django.utils.feedgenerator.get_tag_uri
+def get_tag_uri(url, date):
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id"
+    parts = urlparse.urlparse(url)
+    date_part = ""
+    if date is not None:
+        date_part = ",%s:" % date.strftime("%Y-%m-%d")
+    return "tag:%s%s%s/%s" % (
+        parts.hostname,
+        date_part,
+        parts.path,
+        parts.fragment,
+    )
+
+
+
+## based on django.contrib.syndication.feeds.Feed
+class Feed(Feed):
+    
+    
+    VALIDATE = True
+    
+    
+    def __init__(self, slug, feed_url):
+        # @@@ slug and feed_url are not used yet
+        pass
+    
+    
+    def __get_dynamic_attr(self, attname, obj, default=None):
+        try:
+            attr = getattr(self, attname)
+        except AttributeError:
+            return default
+        if callable(attr):
+            # Check func_code.co_argcount rather than try/excepting the
+            # function and catching the TypeError, because something inside
+            # the function may raise the TypeError. This technique is more
+            # accurate.
+            if hasattr(attr, 'func_code'):
+                argcount = attr.func_code.co_argcount
+            else:
+                argcount = attr.__call__.func_code.co_argcount
+            if argcount == 2: # one argument is 'self'
+                return attr(obj)
+            else:
+                return attr()
+        return attr
+    
+    
+    def get_feed(self, extra_params=None):
+        
+        if extra_params:
+            try:
+                obj = self.get_object(extra_params.split('/'))
+            except (AttributeError, LookupError):
+                raise LookupError('Feed does not exist')
+        else:
+            obj = None
+        
+        feed = AtomFeed(
+            atom_id = self.__get_dynamic_attr('feed_id', obj),
+            title = self.__get_dynamic_attr('feed_title', obj),
+            updated = self.__get_dynamic_attr('feed_updated', obj),
+            icon = self.__get_dynamic_attr('feed_icon', obj),
+            logo = self.__get_dynamic_attr('feed_logo', obj),
+            rights = self.__get_dynamic_attr('feed_rights', obj),
+            subtitle = self.__get_dynamic_attr('feed_subtitle', obj),
+            authors = self.__get_dynamic_attr('feed_authors', obj, default=[]),
+            categories = self.__get_dynamic_attr('feed_categories', obj, default=[]),
+            contributors = self.__get_dynamic_attr('feed_contributors', obj, default=[]),
+            links = self.__get_dynamic_attr('feed_links', obj, default=[]),
+            extra_attrs = self.__get_dynamic_attr('feed_extra_attrs', obj),
+            hide_generator = self.__get_dynamic_attr('hide_generator', obj, default=False)
+        )
+        
+        items = self.__get_dynamic_attr('items', obj)
+        if items is None:
+            raise LookupError('Feed has no items field')
+        
+        for item in items:
+            feed.add_item(
+                atom_id = self.__get_dynamic_attr('item_id', item), 
+                title = self.__get_dynamic_attr('item_title', item),
+                updated = self.__get_dynamic_attr('item_updated', item),
+                content = self.__get_dynamic_attr('item_content', item),
+                published = self.__get_dynamic_attr('item_published', item),
+                rights = self.__get_dynamic_attr('item_rights', item),
+                source = self.__get_dynamic_attr('item_source', item),
+                summary = self.__get_dynamic_attr('item_summary', item),
+                authors = self.__get_dynamic_attr('item_authors', item, default=[]),
+                categories = self.__get_dynamic_attr('item_categories', item, default=[]),
+                contributors = self.__get_dynamic_attr('item_contributors', item, default=[]),
+                links = self.__get_dynamic_attr('item_links', item, default=[]),
+                extra_attrs = self.__get_dynamic_attr('item_extra_attrs', None, default={}),
+            )
+        
+        if self.VALIDATE:
+            feed.validate()
+        return feed
+
+
+
+class ValidationError(Exception):
+    pass
+
+
+
+## based on django.utils.feedgenerator.SyndicationFeed and django.utils.feedgenerator.Atom1Feed
+class AtomFeed(object):
+    
+    
+    mime_type = 'application/atom+xml'
+    ns = u'http://www.w3.org/2005/Atom'
+    
+    
+    def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
+        authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
+        if atom_id is None:
+            raise LookupError('Feed has no feed_id field')
+        if title is None:
+            raise LookupError('Feed has no feed_title field')
+        # if updated == None, we'll calculate it
+        self.feed = {
+            'id': atom_id,
+            'title': title,
+            'updated': updated,
+            'icon': icon,
+            'logo': logo,
+            'rights': rights,
+            'subtitle': subtitle,
+            'authors': authors,
+            'categories': categories,
+            'contributors': contributors,
+            'links': links,
+            'extra_attrs': extra_attrs,
+            'hide_generator': hide_generator,
+        }
+        self.items = []
+    
+    
+    def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
+        authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
+        if atom_id is None:
+            raise LookupError('Feed has no item_id method')
+        if title is None:
+            raise LookupError('Feed has no item_title method')
+        if updated is None:
+            raise LookupError('Feed has no item_updated method')
+        self.items.append({
+            'id': atom_id,
+            'title': title,
+            'updated': updated,
+            'content': content,
+            'published': published,
+            'rights': rights,
+            'source': source,
+            'summary': summary,
+            'authors': authors,
+            'categories': categories,
+            'contributors': contributors,
+            'links': links,
+            'extra_attrs': extra_attrs,
+        })
+    
+    
+    def latest_updated(self):
+        """
+        Returns the latest item's updated or the current time if there are no items.
+        """
+        updates = [item['updated'] for item in self.items]
+        if len(updates) > 0:
+            updates.sort()
+            return updates[-1]
+        else:
+            return datetime.now() # @@@ really we should allow a feed to define its "start" for this case
+    
+    
+    def write_text_construct(self, handler, element_name, data):
+        if isinstance(data, tuple):
+            text_type, text = data
+            if text_type == 'xhtml':
+                handler.startElement(element_name, {'type': text_type})
+                handler._write(text) # write unescaped -- it had better be well-formed XML
+                handler.endElement(element_name)
+            else:
+                handler.addQuickElement(element_name, text, {'type': text_type})
+        else:
+            handler.addQuickElement(element_name, data)
+    
+    
+    def write_person_construct(self, handler, element_name, person):
+        handler.startElement(element_name, {})
+        handler.addQuickElement(u'name', person['name'])
+        if 'uri' in person:
+            handler.addQuickElement(u'uri', person['uri'])
+        if 'email' in person:
+            handler.addQuickElement(u'email', person['email'])
+        handler.endElement(element_name)
+    
+    
+    def write_link_construct(self, handler, link):
+        if 'length' in link:
+            link['length'] = str(link['length'])
+        handler.addQuickElement(u'link', None, link)
+    
+    
+    def write_category_construct(self, handler, category):
+        handler.addQuickElement(u'category', None, category)
+    
+    
+    def write_source(self, handler, data):
+        handler.startElement(u'source', {})
+        if data.get('id'):
+            handler.addQuickElement(u'id', data['id'])
+        if data.get('title'):
+            self.write_text_construct(handler, u'title', data['title'])
+        if data.get('subtitle'):
+            self.write_text_construct(handler, u'subtitle', data['subtitle'])
+        if data.get('icon'):
+            handler.addQuickElement(u'icon', data['icon'])
+        if data.get('logo'):
+            handler.addQuickElement(u'logo', data['logo'])
+        if data.get('updated'):
+            handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
+        for category in data.get('categories', []):
+            self.write_category_construct(handler, category)
+        for link in data.get('links', []):
+            self.write_link_construct(handler, link)
+        for author in data.get('authors', []):
+            self.write_person_construct(handler, u'author', author)
+        for contributor in data.get('contributors', []):
+            self.write_person_construct(handler, u'contributor', contributor)
+        if data.get('rights'):
+            self.write_text_construct(handler, u'rights', data['rights'])
+        handler.endElement(u'source')
+    
+    
+    def write_content(self, handler, data):
+        if isinstance(data, tuple):
+            content_dict, text = data
+            if content_dict.get('type') == 'xhtml':
+                handler.startElement(u'content', content_dict)
+                handler._write(text) # write unescaped -- it had better be well-formed XML
+                handler.endElement(u'content')
+            else:
+                handler.addQuickElement(u'content', text, content_dict)
+        else:
+            handler.addQuickElement(u'content', data)
+    
+    
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        feed_attrs = {u'xmlns': self.ns}
+        if self.feed.get('extra_attrs'):
+            feed_attrs.update(self.feed['extra_attrs'])
+        handler.startElement(u'feed', feed_attrs)
+        handler.addQuickElement(u'id', self.feed['id'])
+        self.write_text_construct(handler, u'title', self.feed['title'])
+        if self.feed.get('subtitle'):
+            self.write_text_construct(handler, u'subtitle', self.feed['subtitle'])
+        if self.feed.get('icon'):
+            handler.addQuickElement(u'icon', self.feed['icon'])
+        if self.feed.get('logo'):
+            handler.addQuickElement(u'logo', self.feed['logo'])
+        if self.feed['updated']:
+            handler.addQuickElement(u'updated', rfc3339_date(self.feed['updated']))
+        else:
+            handler.addQuickElement(u'updated', rfc3339_date(self.latest_updated()))
+        for category in self.feed['categories']:
+            self.write_category_construct(handler, category)
+        for link in self.feed['links']:
+            self.write_link_construct(handler, link)
+        for author in self.feed['authors']:
+            self.write_person_construct(handler, u'author', author)
+        for contributor in self.feed['contributors']:
+            self.write_person_construct(handler, u'contributor', contributor)
+        if self.feed.get('rights'):
+            self.write_text_construct(handler, u'rights', self.feed['rights'])
+        if not self.feed.get('hide_generator'):
+            handler.addQuickElement(u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
+        
+        self.write_items(handler)
+        
+        handler.endElement(u'feed')
+    
+    
+    def write_items(self, handler):
+        for item in self.items:
+            entry_attrs = item.get('extra_attrs', {})
+            handler.startElement(u'entry', entry_attrs)
+            
+            handler.addQuickElement(u'id', item['id'])
+            self.write_text_construct(handler, u'title', item['title'])
+            handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
+            if item.get('published'):
+                handler.addQuickElement(u'published', rfc3339_date(item['published']))
+            if item.get('rights'):
+                self.write_text_construct(handler, u'rights', item['rights'])
+            if item.get('source'):
+                self.write_source(handler, item['source'])
+            
+            for author in item['authors']:
+                self.write_person_construct(handler, u'author', author)
+            for contributor in item['contributors']:
+                self.write_person_construct(handler, u'contributor', contributor)
+            for category in item['categories']:
+                self.write_category_construct(handler, category)
+            for link in item['links']:
+                self.write_link_construct(handler, link)
+            if item.get('summary'):
+                self.write_text_construct(handler, u'summary', item['summary'])
+            if item.get('content'):
+                self.write_content(handler, item['content'])
+            
+            handler.endElement(u'entry')
+    
+    
+    def validate(self):
+        
+        def validate_text_construct(obj):
+            if isinstance(obj, tuple):
+                if obj[0] not in ['text', 'html', 'xhtml']:
+                    return False
+            # @@@ no validation is done that 'html' text constructs are valid HTML
+            # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
+            
+            return True
+        
+        if not validate_text_construct(self.feed['title']):
+            raise ValidationError('feed title has invalid type')
+        if self.feed.get('subtitle'):
+            if not validate_text_construct(self.feed['subtitle']):
+                raise ValidationError('feed subtitle has invalid type')
+        if self.feed.get('rights'):
+            if not validate_text_construct(self.feed['rights']):
+                raise ValidationError('feed rights has invalid type')
+        
+        alternate_links = {}
+        for link in self.feed.get('links'):
+            if link.get('rel') == 'alternate' or link.get('rel') == None:
+                key = (link.get('type'), link.get('hreflang'))
+                if key in alternate_links:
+                    raise ValidationError('alternate links must have unique type/hreflang')
+                alternate_links[key] = link
+        
+        if self.feed.get('authors'):
+            feed_author = True
+        else:
+            feed_author = False
+        
+        for item in self.items:
+            if not feed_author and not item.get('authors'):
+                if item.get('source') and item['source'].get('authors'):
+                    pass
+                else:
+                    raise ValidationError('if no feed author, all entries must have author (possibly in source)')
+            
+            if not validate_text_construct(item['title']):
+                raise ValidationError('entry title has invalid type')
+            if item.get('rights'):
+                if not validate_text_construct(item['rights']):
+                    raise ValidationError('entry rights has invalid type')
+            if item.get('summary'):
+                if not validate_text_construct(item['summary']):
+                    raise ValidationError('entry summary has invalid type')
+            source = item.get('source')
+            if source:
+                if source.get('title'):
+                    if not validate_text_construct(source['title']):
+                        raise ValidationError('source title has invalid type')
+                if source.get('subtitle'):
+                    if not validate_text_construct(source['subtitle']):
+                        raise ValidationError('source subtitle has invalid type')
+                if source.get('rights'):
+                    if not validate_text_construct(source['rights']):
+                        raise ValidationError('source rights has invalid type')
+            
+            alternate_links = {}
+            for link in item.get('links'):
+                if link.get('rel') == 'alternate' or link.get('rel') == None:
+                    key = (link.get('type'), link.get('hreflang'))
+                    if key in alternate_links:
+                        raise ValidationError('alternate links must have unique type/hreflang')
+                    alternate_links[key] = link
+            
+            if not item.get('content'):
+                if not alternate_links:
+                    raise ValidationError('if no content, entry must have alternate link')
+            
+            if item.get('content') and isinstance(item.get('content'), tuple):
+                content_type = item.get('content')[0].get('type')
+                if item.get('content')[0].get('src'):
+                    if item.get('content')[1]:
+                        raise ValidationError('content with src should be empty')
+                    if not item.get('summary'):
+                        raise ValidationError('content with src requires a summary too')
+                    if content_type in ['text', 'html', 'xhtml']:
+                        raise ValidationError('content with src cannot have type of text, html or xhtml')
+                if content_type:
+                    if '/' in content_type and \
+                        not content_type.startswith('text/') and \
+                        not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
+                        not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
+                        # @@@ check content is Base64
+                        if not item.get('summary'):
+                            raise ValidationError('content in Base64 requires a summary too')
+                    if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
+                        raise ValidationError('content type does not appear to be valid')
+                    
+                    # @@@ no validation is done that 'html' text constructs are valid HTML
+                    # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
+                    
+                    return
+        
+        return
+
+
+
+class LegacySyndicationFeed(AtomFeed):
+    """
+    Provides an SyndicationFeed-compatible interface in its __init__ and
+    add_item but is really a new AtomFeed object.
+    """
+    
+    def __init__(self, title, link, description, language=None, author_email=None,
+            author_name=None, author_link=None, subtitle=None, categories=[],
+            feed_url=None, feed_copyright=None):
+        
+        atom_id = link
+        title = title
+        updated = None # will be calculated
+        rights = feed_copyright
+        subtitle = subtitle
+        author_dict = {'name': author_name}
+        if author_link:
+            author_dict['uri'] = author_uri
+        if author_email:
+            author_dict['email'] = author_email
+        authors = [author_dict]
+        if categories:
+            categories = [{'term': term} for term in categories]
+        links = [{'rel': 'alternate', 'href': link}]
+        if feed_url:
+            links.append({'rel': 'self', 'href': feed_url})
+        if language:
+            extra_attrs = {'xml:lang': language}
+        else:
+            extra_attrs = {}
+        
+        # description ignored (as with Atom1Feed)
+        
+        AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
+                authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
+    
+    
+    def add_item(self, title, link, description, author_email=None,
+            author_name=None, author_link=None, pubdate=None, comments=None,
+            unique_id=None, enclosure=None, categories=[], item_copyright=None):
+        
+        if unique_id:
+            atom_id = unique_id
+        else:
+            atom_id = get_tag_uri(link, pubdate)
+        title = title
+        updated = pubdate
+        if item_copyright:
+            rights = item_copyright
+        else:
+            rights = None
+        if description:
+            summary = 'html', description
+        else:
+            summary = None
+        author_dict = {'name': author_name}
+        if author_link:
+            author_dict['uri'] = author_uri
+        if author_email:
+            author_dict['email'] = author_email
+        authors = [author_dict]
+        categories = [{'term': term} for term in categories]
+        links = [{'rel': 'alternate', 'href': link}]
+        if enclosure:
+            links.append({'rel': 'enclosure', 'href': enclosure.url, 'length': enclosure.length, 'type': enclosure.mime_type})
+        
+        AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
+                authors=authors, categories=categories, links=links)

transifex/txcommon/views.py

 from django.views.decorators.csrf import csrf_protect
 from django.views.generic import list_detail
 from django.core.urlresolvers import reverse
+from django.contrib.syndication.views import feed
 
 from haystack.query import SearchQuerySet
 from notification import models as notification
 from transifex.txcommon.haystack_utils import prepare_solr_query_string, \
     fulltext_fuzzy_match_filter
 
+from notification.decorators import basic_auth_required, simple_basic_auth_callback
+from transifex.txcommon.feeds import NoticeUserFeed
+
+@basic_auth_required(realm='Notices Feed', callback_func=simple_basic_auth_callback)
+def feed_for_user(request):
+    url = "feed/%s" % request.user.username
+    return feed(request, url, {
+        "feed": NoticeUserFeed,
+    })
+
+
 def permission_denied(request, template_name=None, extra_context={}, *args,
     **kwargs):
     """Wrapper to allow undeclared key arguments."""

transifex/urls.py

 
 if settings.ENABLE_NOTICES:
     urlpatterns += patterns('',
+        url(r'^notices/feed/$', 'txcommon.views.feed_for_user', name="notification_feed_for_user"),
         (r'^notices/', include('notification.urls')),
         url(r'^accounts/nudge/(?P<username>.+)/$', 'txcommon.views.user_nudge', name='user_nudge'),
     )