~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# Copyright 2009 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Announcement feed (syndication) views."""

# This module has been chosen to be the example for how to implement a new
# feed class.  While the two interfaces `IFeed` and `IFeedEntry` are heavily
# documented, additional documentation has been added to this module to
# clearly demonstrate the concepts required to implement a feed rather than
# simply referencing the interfaces.

__metaclass__ = type

__all__ = [
    'LaunchpadAnnouncementsFeed',
    'TargetAnnouncementsFeed',
    ]


from zope.component import getUtility

from canonical.launchpad.interfaces.launchpad import IFeedsApplication
from canonical.launchpad.webapp import (
    canonical_url,
    urlappend,
    )
from canonical.lazr.feed import (
    FeedBase,
    FeedEntry,
    FeedPerson,
    FeedTypedData,
    )
from lp.app.browser.stringformatter import FormattersAPI
from lp.registry.interfaces.announcement import (
    IAnnouncementSet,
    IHasAnnouncements,
    )
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.product import IProduct
from lp.registry.interfaces.projectgroup import IProjectGroup


class AnnouncementsFeedBase(FeedBase):
    """Abstract class for announcement feeds."""

    # Every feed must have a feed name.  This name will be used to construct
    # the final element in the URL for the feed with the extension for one of
    # the supported feed types appended.  So announcement feeds will end with
    # 'announcements.atom' or 'announcements.html'.
    feedname = "announcements"

    @property
    def link_alternate(self):
        """See `IFeed`."""
        # Return the human-readable alternate URL for this feed.  For example:
        # https://launchpad.net/ubuntu/+announcements
        return urlappend(canonical_url(self.context, rootsite="mainsite"),
                         "+announcements")

    def itemToFeedEntry(self, announcement):
        """See `IFeed`."""
        # Given an instance of an announcement, create a FeedEntry out of it
        # and return.

        # The title for the FeedEntry is an IFeedTypedData instance and may be
        # plain text or html.
        title = self._entryTitle(announcement)
        # The link_alternate for the entry is the human-readable alternate URL
        # for the entry.  For example:
        # http://launchpad.net/ubuntu/+announcment/12
        entry_link_alternate = "%s%s" % (
            canonical_url(announcement.target, rootsite=self.rootsite),
            "/+announcement/%d" % announcement.id)
        # The content of the entry is the text displayed as the body in the
        # feed reader.  For announcements it is plain text but it must be
        # escaped to account for any special characters the user may have
        # entered, such as '&' and '<' because it will be embedded in the XML
        # document.
        formatted_summary = FormattersAPI(announcement.summary).text_to_html()
        content = FeedTypedData(formatted_summary,
                                content_type="html",
                                root_url=self.root_url)
        # The entry for an announcement has distinct dates for created,
        # updated, and published.  For some data, the created and published
        # dates will be the same.  The announcements also only have a singe
        # author.

        entry_id = 'tag:launchpad.net,%s:/+announcement/%d' % (
            announcement.date_created.date().isoformat(),
            announcement.id)
        entry = FeedEntry(
            title=title,
            link_alternate=entry_link_alternate,
            date_created=announcement.date_created,
            date_updated=announcement.date_updated,
            date_published=announcement.date_announced,
            authors=[FeedPerson(announcement.registrant,
                                rootsite="mainsite")],
            content=content,
            id_=entry_id)
        return entry

    def _entryTitle(self, announcement):
        """Return the title for the announcement.

        Override in each base class.
        """
        raise NotImplementedError


class LaunchpadAnnouncementsFeed(AnnouncementsFeedBase):
    """Publish an Atom feed of all public announcements in Launchpad."""

    # The `usedfor` property identifies the class associated with this feed
    # class.  It is used by the `IFeedsDirective` in
    # launchpad/webapp/metazcml.py to provide a mapping from the supported
    # feed types to this class.  It is a more maintainable method than simply
    # listing each mapping in the zcml.  The only zcml change is to add this
    # class to the list of classes in the `browser:feeds` stanza of
    # launchpad/zcml/feeds.zcml.
    usedfor = IFeedsApplication

    def _getItemsWorker(self):
        """Create the list of items.

        Called by getItems which may cache the results.
        """
        # Return a list of items that will be the entries in the feed.  Each
        # item shall be an instance of `IFeedEntry`.

        # The quantity is defined in FeedBase or config file.
        items = getUtility(IAnnouncementSet).getAnnouncements(
            limit=self.quantity)
        # Convert the items into their feed entry representation.
        items = [self.itemToFeedEntry(item) for item in items]
        return items

    def _entryTitle(self, announcement):
        """Return an `IFeedTypedData` instance for the feed title."""
        return FeedTypedData('[%s] %s' % (
                announcement.target.name, announcement.title))

    @property
    def title(self):
        """See `IFeed`."""
        # The textual representation of the title for the feed.
        return "Announcements published via Launchpad"

    @property
    def logo(self):
        """See `IFeed`."""
        # The logo is an image representing the feed.  Since this feed is for
        # all announcements in Launchpad, return the Launchpad logo.
        url = '/@@/launchpad-logo'
        return self.site_url + url

    @property
    def icon(self):
        """See `IFeed`."""
        # The icon is an icon representing the feed.  Since this feed is for
        # all announcements in Launchpad, return the Launchpad icon.
        url = '/@@/launchpad'
        return self.site_url + url


class TargetAnnouncementsFeed(AnnouncementsFeedBase):
    """Publish an Atom feed of all announcements.

    Used for any class that implements IHasAnnouncements such as project,
    product, or distribution.
    """
    # This view is used for any class implementing `IHasAnnouncments`.
    usedfor = IHasAnnouncements

    def _getItemsWorker(self):
        """Create the list of items.

        Called by getItems which may cache the results.
        """
        # The quantity is defined in FeedBase or config file.
        items = self.context.getAnnouncements(limit=self.quantity)
        # Convert the items into their feed entry representation.
        items = [self.itemToFeedEntry(item) for item in items]
        return items

    def _entryTitle(self, announcement):
        return FeedTypedData(announcement.title)

    @property
    def title(self):
        """See `IFeed`."""
        return "%s Announcements" % self.context.displayname

    @property
    def logo(self):
        """See `IFeed`."""
        # The logo is different depending upon the context we are displaying.
        if self.context.logo is not None:
            return self.context.logo.getURL()
        elif IProjectGroup.providedBy(self.context):
            url = '/@@/project-logo'
        elif IProduct.providedBy(self.context):
            url = '/@@/product-logo'
        elif IDistribution.providedBy(self.context):
            url = '/@@/distribution-logo'
        else:
            raise AssertionError(
                "Context for TargetsAnnouncementsFeed does not provide an "
                "expected interface.")
        return self.site_url + url

    @property
    def icon(self):
        """See `IFeed`."""
        # The icon is customized based upon the context.
        if self.context.icon is not None:
            return self.context.icon.getURL()
        elif IProjectGroup.providedBy(self.context):
            url = '/@@/project'
        elif IProduct.providedBy(self.context):
            url = '/@@/product'
        elif IDistribution.providedBy(self.context):
            url = '/@@/distribution'
        else:
            raise AssertionError(
                "Context for TargetsAnnouncementsFeed does not provide an "
                "expected interface.")
        return self.site_url + url