We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Improve IATI export
## Test plan

Before starting with testing, make sure to perform two one-time actions:
- The `iati_export` cron job is running (done on Test and UAT)
  - See running cron jobs by running: `manage.py crontab show`;
- The `perform_iati_checks` management command has been run (done on Test and UAT).

---

GIVEN the 'My IATI' section in MyRSR
WHEN connected to multiple organisations (or as superuser)
THEN an organisation selection screen should be shown

WHEN connected to one organisation
THEN this organisation should be automatically selected

GIVEN the 'My IATI' section in MyRSR
WHEN an organisation has been selected
THEN the overview of all IATI exports should be shown
AND for each export the status, number of projects, created by, created at and IATI version should be shown
AND the latest IATI export should be shown in a green row
AND a pending or in progress IATI export should be shown in a yellow row
AND a cancelled or export without an IATI file should be shown in a red row

GIVEN the 'My IATI' section in MyRSR
WHEN an organisation has been selected
THEN it should be possible to select whether the latest IATI file is shown on the organisation page

GIVEN that is has been set that the latest IATI file is shown on the organisation page
THEN it should be shown on the organisation page as well
ELSE the IATI file should not be shown

GIVEN that the 'Add new IATI export' button is clicked
THEN the user should be redirected to the project selection overview

GIVEN the project selection overview
WHEN looking at the projects overview
THEN all projects where the selected organisation is reporting organisation should be shown

GIVEN the project selection overview
WHEN applying a filter
THEN the project selection should change
AND the indication of the number of projects selected should indicate the number of selected projects

GIVEN the project selection overview
WHEN projects are selected AND the 'create new IATI export' button is clicked
THEN the user should be redirected to the IATI exports overview
AND the top IATI export should be the new IATI export (with 'Pending' status)

GIVEN the IATI export overview
WHEN an export is pending or in progress
THEN the overview should be refreshed every 10 seconds
AND when an export is in progress, the number of processed projects should be shown
## Issue description

Currently, an IATI export with more than 70 projects will give a DB timeout. However, we need to be able to export an IATI file with any amount of projects. Similar to the IATI import, we can use a cron job for this.
- [x] Move IATI export to a cron job implementation
- [x] Update the 'My IATI' tab in MyRSR


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `akvo/rsr/feeds.py`
Content:
```
1 # -*- coding: utf-8 -*-
2 
3 # Akvo RSR is covered by the GNU Affero General Public License.
4 # See more details in the license.txt file located at the root folder of the Akvo RSR module. 
5 # For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.
6 
7 import re
8 
9 from xml.sax.saxutils import XMLGenerator
10 
11 from django.contrib.syndication.views import FeedDoesNotExist, Feed
12 from django.core.urlresolvers import reverse
13 from django.shortcuts import get_object_or_404
14 from django.utils.feedgenerator import Rss201rev2Feed
15 from django.utils.translation import ugettext_lazy as _
16 
17 from akvo.rsr.models import Project, ProjectUpdate, Organisation
18 
19 
20 def __dict_replace(s, d):
21     """Replace substrings of a string using a dictionary."""
22     for key, value in d.items():
23         s = s.replace(key, value)
24     return s
25 
26 def __escape(data, entities):
27     # must do ampersand first
28     data = data.replace("&", "&amp;")
29     data  = data.replace(">", "&gt;")
30     data  = data.replace("<", "&lt;")
31     if entities:
32         data = __dict_replace(data, entities)
33     return data
34 
35 def escape(data, entities={}):
36     """Modification to xml.sax.saxutils.escape to that detects CDATA blocks that are not escaped
37 
38     Escape &, <, and > in a string of data.
39 
40     You can escape other strings of data by passing a dictionary as
41     the optional entities parameter.  The keys and values must all be
42     strings; each key will be replaced with its corresponding value.
43 
44     """
45     # find character data, re.DOTALL includes linefeed in .
46     pattern = re.compile('<!\[CDATA\[.*\]\]>', re.DOTALL)
47     iterator = pattern.finditer(data)
48     start = 0
49     bits = []
50     for match in iterator:
51         #grab chunk before first match
52         bit = data[start:match.span()[0]]
53         bit = __escape(bit, entities)
54         bits.append(bit)
55         #grab match
56         bit = data[match.span()[0]:match.span()[1]]
57         bits.extend(bit)
58         start = match.span()[1]
59     # escape tail bit after last match
60     bit = data[start:]
61     bit = __escape(bit, entities)
62     bits.extend(bit)
63     data = ''.join(bits)
64     return data
65 
66 
67 class RSRSimplerXMLGenerator(XMLGenerator):
68     """subclassed to be able to call custom escape() function, see above
69     """
70     def characters(self, content):
71         self._write(escape(content))
72 
73     def addQuickElement(self, name, contents=None, attrs=None):
74         "Convenience method for adding an element with no children"
75         if attrs is None: attrs = {}
76         self.startElement(name, attrs)
77         if contents is not None:
78             self.characters(contents)
79         self.endElement(name)
80 
81 
82 class RSRMediaRssFeed(Rss201rev2Feed):
83     def rss_attributes(self):
84         attrs = super(RSRMediaRssFeed, self).rss_attributes()
85         attrs['xmlns:media']    = 'http://search.yahoo.com/mrss/'
86         attrs['xmlns:atom']     = 'http://www.w3.org/2005/Atom'
87         return attrs
88 
89     def add_item_elements(self, handler, item):
90         """Callback to add elements to each item (item/entry) element."""
91         super(RSRMediaRssFeed, self).add_item_elements(handler, item)
92 
93         if 'media:title' in item:
94             handler.addQuickElement(u"media:title", item['title'])
95         if 'media:description' in item:
96             handler.addQuickElement(u"media:description", item['media:description'])
97         if 'media:credit' in item:
98             handler.addQuickElement(u"media:credit", item['media:credit'])
99 
100         if 'content_url' in item:
101             content = dict(url=item['content_url'])
102             if 'content_width' in item:
103                 content['width'] = str(item['content_width'])
104             if 'content_height' in item:
105                 content['height'] = str(item['content_height'])
106             handler.addQuickElement(u"media:content", '', content)
107 
108         if 'thumbnail_url' in item:
109             thumbnail = dict(url=item['thumbnail_url'])
110             if 'thumbnail_width' in item:
111                 thumbnail['width'] = str(item['thumbnail_width'])
112             if 'thumbnail_height' in item:
113                 thumbnail['height'] = str(item['thumbnail_height'])
114             handler.addQuickElement(u"media:thumbnail", '', thumbnail)
115 
116         if 'keywords' in item:
117             handler.addQuickElement(u"media:keywords", item['keywords'])
118 
119     def write(self, outfile, encoding):
120         handler = RSRSimplerXMLGenerator(outfile, encoding)
121         handler.startDocument()
122         handler.startElement(u"rss", self.rss_attributes())
123         handler.startElement(u"channel", self.root_attributes())
124         self.add_root_elements(handler)
125         self.write_items(handler)
126         self.endChannelElement(handler)
127         handler.endElement(u"rss")
128 
129 class UpdateFeed(Feed):
130     """base class generating Update feeds
131     """
132     feed_type = RSRMediaRssFeed
133 
134     def link(self, obj):
135         if not obj:
136             raise FeedDoesNotExist
137         return obj.get_absolute_url()
138 
139     def item_link(self, item):
140         return item.get_absolute_url()
141 
142     def item_title(self, item):
143         return item.title
144 
145     def item_description(self, item):
146         try:
147             size = item.photo.size
148             return '<![CDATA[<p><a href="%s"><img src="%s" alt="" /></a></p><p>%s</p>]]>' % (
149                 item.get_absolute_url(),
150                 item.photo.thumbnail.absolute_url,
151                 item.text,
152             )
153         except:
154             return item.text
155 
156     def item_pubdate(self, item):
157         return item.created_at
158 
159     def item_author_name(self, item):
160         return item.user.get_full_name()
161 
162     def item_credit(self, item):
163         return item.photo_credit
164 
165     def item_extra_kwargs(self, item):
166         """return a dictionary to the feedgenerator for each item to be added to the feed.
167         """
168         try:
169             size = item.photo.size
170             photo = item.photo
171             kwargs = {
172                 'media:title': item.title,
173                 'media:description': item.photo_caption,
174                 'media:credit': item.photo_credit,
175                 'content_url': photo.url,
176                 'content_width': photo.width,
177                 'content_height': photo.height,
178                 'thumbnail_url': photo.thumbnail.absolute_url,
179                 'thumbnail_width': photo.thumbnail.width(),
180                 'thumbnail_height': photo.thumbnail.height(),
181             }
182             return kwargs
183         except:
184             return {}
185 
186 
187 class ProjectUpdates(UpdateFeed):
188     """RSS feed for last 25 RSR updates of a project."""
189     def get_object(self, request, project_id):
190         return Project.objects.get(pk__exact=project_id)
191 
192     def title(self, obj):
193         return _(u'Akvo RSR project %(id)d: %(project_title)s') % {
194             'id': obj.id,
195             'project_title': obj.title
196         }
197 
198     def description(self, obj):
199         return _(u'Project updates for project %(project_title)s') % {
200             'project_title': obj.title
201         }
202 
203     def items(self, obj):
204         # Limited to 25 items to prevent gateway timeouts.
205         return ProjectUpdate.objects.filter(project__id=obj.id).order_by('-id')[:25]
206 
207 
208 class OrganisationUpdates(UpdateFeed):
209     """RSS feed for last 25 RSR updates of an organisation."""
210     feed_type = RSRMediaRssFeed
211 
212     def get_object(self, request, org_id):
213         return get_object_or_404(Organisation, id=int(org_id))
214 
215     def title(self, obj):
216         return _(u'Projects of %(org_name)s') % {'org_name':obj.name,}
217 
218     def description(self, obj):
219         if obj.name == obj.long_name:
220             return _(u"Project updates for projects partnered by %(org_name)s") % {
221                 'org_name': obj.name
222             }
223         else:
224             return _(
225                 u"Project updates for projects partnered by %(org_name)s - %(long_name)s"
226             ) % {'org_name': obj.name, 'long_name': obj.long_name}
227 
228     def items(self, obj):
229         # Limited to 25 items to prevent gateway timeouts.
230         return obj.published_projects().all_updates()[:25]
231 
232     def item_title(self, item):
233         return _(
234             u'Project %(project_id)d - %(project_title)s: %(update_title)s'
235         ) % {
236             'project_id': item.project.id,
237             'project_title': item.project.title,
238             'update_title': item.title
239         }
240 
241 
242 class AllProjectUpdates(UpdateFeed):
243     """RSS feed for last 25 RSR updates."""
244     title = _(u'Last 25 RSR project updates')
245     
246     def link(self):
247         return reverse('update-directory')
248 
249     description = _(u'Project updates for all Akvo RSR projects')
250 
251     def items(self):
252         # Limited to 25 items to prevent gateway timeouts.
253         return ProjectUpdate.objects.select_related().order_by('-id')[:25]
254 
255     def item_title(self, item):
256         return _(
257             u'Project %(project_id)d - %(project_title)s: %(update_title)s'
258         ) % {
259             'project_id': item.project.id,
260             'project_title': item.project.title,
261             'update_title': item.title
262         }
263
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/akvo/rsr/feeds.py b/akvo/rsr/feeds.py
--- a/akvo/rsr/feeds.py
+++ b/akvo/rsr/feeds.py
@@ -227,7 +227,7 @@
 
     def items(self, obj):
         # Limited to 25 items to prevent gateway timeouts.
-        return obj.published_projects().all_updates()[:25]
+        return obj.all_updates()[:25]
 
     def item_title(self, item):
         return _(

{"golden_diff": "diff --git a/akvo/rsr/feeds.py b/akvo/rsr/feeds.py\n--- a/akvo/rsr/feeds.py\n+++ b/akvo/rsr/feeds.py\n@@ -227,7 +227,7 @@\n \n     def items(self, obj):\n         # Limited to 25 items to prevent gateway timeouts.\n-        return obj.published_projects().all_updates()[:25]\n+        return obj.all_updates()[:25]\n \n     def item_title(self, item):\n         return _(\n", "issue": "Improve IATI export\n## Test plan\n\nBefore starting with testing, make sure to perform two one-time actions:\n- The `iati_export` cron job is running (done on Test and UAT)\n  - See running cron jobs by running: `manage.py crontab show`;\n- The `perform_iati_checks` management command has been run (done on Test and UAT).\n\n---\n\nGIVEN the 'My IATI' section in MyRSR\nWHEN connected to multiple organisations (or as superuser)\nTHEN an organisation selection screen should be shown\n\nWHEN connected to one organisation\nTHEN this organisation should be automatically selected\n\nGIVEN the 'My IATI' section in MyRSR\nWHEN an organisation has been selected\nTHEN the overview of all IATI exports should be shown\nAND for each export the status, number of projects, created by, created at and IATI version should be shown\nAND the latest IATI export should be shown in a green row\nAND a pending or in progress IATI export should be shown in a yellow row\nAND a cancelled or export without an IATI file should be shown in a red row\n\nGIVEN the 'My IATI' section in MyRSR\nWHEN an organisation has been selected\nTHEN it should be possible to select whether the latest IATI file is shown on the organisation page\n\nGIVEN that is has been set that the latest IATI file is shown on the organisation page\nTHEN it should be shown on the organisation page as well\nELSE the IATI file should not be shown\n\nGIVEN that the 'Add new IATI export' button is clicked\nTHEN the user should be redirected to the project selection overview\n\nGIVEN the project selection overview\nWHEN looking at the projects overview\nTHEN all projects where the selected organisation is reporting organisation should be shown\n\nGIVEN the project selection overview\nWHEN applying a filter\nTHEN the project selection should change\nAND the indication of the number of projects selected should indicate the number of selected projects\n\nGIVEN the project selection overview\nWHEN projects are selected AND the 'create new IATI export' button is clicked\nTHEN the user should be redirected to the IATI exports overview\nAND the top IATI export should be the new IATI export (with 'Pending' status)\n\nGIVEN the IATI export overview\nWHEN an export is pending or in progress\nTHEN the overview should be refreshed every 10 seconds\nAND when an export is in progress, the number of processed projects should be shown\n## Issue description\n\nCurrently, an IATI export with more than 70 projects will give a DB timeout. However, we need to be able to export an IATI file with any amount of projects. Similar to the IATI import, we can use a cron job for this.\n- [x] Move IATI export to a cron job implementation\n- [x] Update the 'My IATI' tab in MyRSR\n\n", "before_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Akvo RSR is covered by the GNU Affero General Public License.\n# See more details in the license.txt file located at the root folder of the Akvo RSR module. \n# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\nimport re\n\nfrom xml.sax.saxutils import XMLGenerator\n\nfrom django.contrib.syndication.views import FeedDoesNotExist, Feed\nfrom django.core.urlresolvers import reverse\nfrom django.shortcuts import get_object_or_404\nfrom django.utils.feedgenerator import Rss201rev2Feed\nfrom django.utils.translation import ugettext_lazy as _\n\nfrom akvo.rsr.models import Project, ProjectUpdate, Organisation\n\n\ndef __dict_replace(s, d):\n    \"\"\"Replace substrings of a string using a dictionary.\"\"\"\n    for key, value in d.items():\n        s = s.replace(key, value)\n    return s\n\ndef __escape(data, entities):\n    # must do ampersand first\n    data = data.replace(\"&\", \"&amp;\")\n    data  = data.replace(\">\", \"&gt;\")\n    data  = data.replace(\"<\", \"&lt;\")\n    if entities:\n        data = __dict_replace(data, entities)\n    return data\n\ndef escape(data, entities={}):\n    \"\"\"Modification to xml.sax.saxutils.escape to that detects CDATA blocks that are not escaped\n\n    Escape &, <, and > in a string of data.\n\n    You can escape other strings of data by passing a dictionary as\n    the optional entities parameter.  The keys and values must all be\n    strings; each key will be replaced with its corresponding value.\n\n    \"\"\"\n    # find character data, re.DOTALL includes linefeed in .\n    pattern = re.compile('<!\\[CDATA\\[.*\\]\\]>', re.DOTALL)\n    iterator = pattern.finditer(data)\n    start = 0\n    bits = []\n    for match in iterator:\n        #grab chunk before first match\n        bit = data[start:match.span()[0]]\n        bit = __escape(bit, entities)\n        bits.append(bit)\n        #grab match\n        bit = data[match.span()[0]:match.span()[1]]\n        bits.extend(bit)\n        start = match.span()[1]\n    # escape tail bit after last match\n    bit = data[start:]\n    bit = __escape(bit, entities)\n    bits.extend(bit)\n    data = ''.join(bits)\n    return data\n\n\nclass RSRSimplerXMLGenerator(XMLGenerator):\n    \"\"\"subclassed to be able to call custom escape() function, see above\n    \"\"\"\n    def characters(self, content):\n        self._write(escape(content))\n\n    def addQuickElement(self, name, contents=None, attrs=None):\n        \"Convenience method for adding an element with no children\"\n        if attrs is None: attrs = {}\n        self.startElement(name, attrs)\n        if contents is not None:\n            self.characters(contents)\n        self.endElement(name)\n\n\nclass RSRMediaRssFeed(Rss201rev2Feed):\n    def rss_attributes(self):\n        attrs = super(RSRMediaRssFeed, self).rss_attributes()\n        attrs['xmlns:media']    = 'http://search.yahoo.com/mrss/'\n        attrs['xmlns:atom']     = 'http://www.w3.org/2005/Atom'\n        return attrs\n\n    def add_item_elements(self, handler, item):\n        \"\"\"Callback to add elements to each item (item/entry) element.\"\"\"\n        super(RSRMediaRssFeed, self).add_item_elements(handler, item)\n\n        if 'media:title' in item:\n            handler.addQuickElement(u\"media:title\", item['title'])\n        if 'media:description' in item:\n            handler.addQuickElement(u\"media:description\", item['media:description'])\n        if 'media:credit' in item:\n            handler.addQuickElement(u\"media:credit\", item['media:credit'])\n\n        if 'content_url' in item:\n            content = dict(url=item['content_url'])\n            if 'content_width' in item:\n                content['width'] = str(item['content_width'])\n            if 'content_height' in item:\n                content['height'] = str(item['content_height'])\n            handler.addQuickElement(u\"media:content\", '', content)\n\n        if 'thumbnail_url' in item:\n            thumbnail = dict(url=item['thumbnail_url'])\n            if 'thumbnail_width' in item:\n                thumbnail['width'] = str(item['thumbnail_width'])\n            if 'thumbnail_height' in item:\n                thumbnail['height'] = str(item['thumbnail_height'])\n            handler.addQuickElement(u\"media:thumbnail\", '', thumbnail)\n\n        if 'keywords' in item:\n            handler.addQuickElement(u\"media:keywords\", item['keywords'])\n\n    def write(self, outfile, encoding):\n        handler = RSRSimplerXMLGenerator(outfile, encoding)\n        handler.startDocument()\n        handler.startElement(u\"rss\", self.rss_attributes())\n        handler.startElement(u\"channel\", self.root_attributes())\n        self.add_root_elements(handler)\n        self.write_items(handler)\n        self.endChannelElement(handler)\n        handler.endElement(u\"rss\")\n\nclass UpdateFeed(Feed):\n    \"\"\"base class generating Update feeds\n    \"\"\"\n    feed_type = RSRMediaRssFeed\n\n    def link(self, obj):\n        if not obj:\n            raise FeedDoesNotExist\n        return obj.get_absolute_url()\n\n    def item_link(self, item):\n        return item.get_absolute_url()\n\n    def item_title(self, item):\n        return item.title\n\n    def item_description(self, item):\n        try:\n            size = item.photo.size\n            return '<![CDATA[<p><a href=\"%s\"><img src=\"%s\" alt=\"\" /></a></p><p>%s</p>]]>' % (\n                item.get_absolute_url(),\n                item.photo.thumbnail.absolute_url,\n                item.text,\n            )\n        except:\n            return item.text\n\n    def item_pubdate(self, item):\n        return item.created_at\n\n    def item_author_name(self, item):\n        return item.user.get_full_name()\n\n    def item_credit(self, item):\n        return item.photo_credit\n\n    def item_extra_kwargs(self, item):\n        \"\"\"return a dictionary to the feedgenerator for each item to be added to the feed.\n        \"\"\"\n        try:\n            size = item.photo.size\n            photo = item.photo\n            kwargs = {\n                'media:title': item.title,\n                'media:description': item.photo_caption,\n                'media:credit': item.photo_credit,\n                'content_url': photo.url,\n                'content_width': photo.width,\n                'content_height': photo.height,\n                'thumbnail_url': photo.thumbnail.absolute_url,\n                'thumbnail_width': photo.thumbnail.width(),\n                'thumbnail_height': photo.thumbnail.height(),\n            }\n            return kwargs\n        except:\n            return {}\n\n\nclass ProjectUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates of a project.\"\"\"\n    def get_object(self, request, project_id):\n        return Project.objects.get(pk__exact=project_id)\n\n    def title(self, obj):\n        return _(u'Akvo RSR project %(id)d: %(project_title)s') % {\n            'id': obj.id,\n            'project_title': obj.title\n        }\n\n    def description(self, obj):\n        return _(u'Project updates for project %(project_title)s') % {\n            'project_title': obj.title\n        }\n\n    def items(self, obj):\n        # Limited to 25 items to prevent gateway timeouts.\n        return ProjectUpdate.objects.filter(project__id=obj.id).order_by('-id')[:25]\n\n\nclass OrganisationUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates of an organisation.\"\"\"\n    feed_type = RSRMediaRssFeed\n\n    def get_object(self, request, org_id):\n        return get_object_or_404(Organisation, id=int(org_id))\n\n    def title(self, obj):\n        return _(u'Projects of %(org_name)s') % {'org_name':obj.name,}\n\n    def description(self, obj):\n        if obj.name == obj.long_name:\n            return _(u\"Project updates for projects partnered by %(org_name)s\") % {\n                'org_name': obj.name\n            }\n        else:\n            return _(\n                u\"Project updates for projects partnered by %(org_name)s - %(long_name)s\"\n            ) % {'org_name': obj.name, 'long_name': obj.long_name}\n\n    def items(self, obj):\n        # Limited to 25 items to prevent gateway timeouts.\n        return obj.published_projects().all_updates()[:25]\n\n    def item_title(self, item):\n        return _(\n            u'Project %(project_id)d - %(project_title)s: %(update_title)s'\n        ) % {\n            'project_id': item.project.id,\n            'project_title': item.project.title,\n            'update_title': item.title\n        }\n\n\nclass AllProjectUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates.\"\"\"\n    title = _(u'Last 25 RSR project updates')\n    \n    def link(self):\n        return reverse('update-directory')\n\n    description = _(u'Project updates for all Akvo RSR projects')\n\n    def items(self):\n        # Limited to 25 items to prevent gateway timeouts.\n        return ProjectUpdate.objects.select_related().order_by('-id')[:25]\n\n    def item_title(self, item):\n        return _(\n            u'Project %(project_id)d - %(project_title)s: %(update_title)s'\n        ) % {\n            'project_id': item.project.id,\n            'project_title': item.project.title,\n            'update_title': item.title\n        }\n", "path": "akvo/rsr/feeds.py"}], "after_files": [{"content": "# -*- coding: utf-8 -*-\n\n# Akvo RSR is covered by the GNU Affero General Public License.\n# See more details in the license.txt file located at the root folder of the Akvo RSR module. \n# For additional details on the GNU license please see < http://www.gnu.org/licenses/agpl.html >.\n\nimport re\n\nfrom xml.sax.saxutils import XMLGenerator\n\nfrom django.contrib.syndication.views import FeedDoesNotExist, Feed\nfrom django.core.urlresolvers import reverse\nfrom django.shortcuts import get_object_or_404\nfrom django.utils.feedgenerator import Rss201rev2Feed\nfrom django.utils.translation import ugettext_lazy as _\n\nfrom akvo.rsr.models import Project, ProjectUpdate, Organisation\n\n\ndef __dict_replace(s, d):\n    \"\"\"Replace substrings of a string using a dictionary.\"\"\"\n    for key, value in d.items():\n        s = s.replace(key, value)\n    return s\n\ndef __escape(data, entities):\n    # must do ampersand first\n    data = data.replace(\"&\", \"&amp;\")\n    data  = data.replace(\">\", \"&gt;\")\n    data  = data.replace(\"<\", \"&lt;\")\n    if entities:\n        data = __dict_replace(data, entities)\n    return data\n\ndef escape(data, entities={}):\n    \"\"\"Modification to xml.sax.saxutils.escape to that detects CDATA blocks that are not escaped\n\n    Escape &, <, and > in a string of data.\n\n    You can escape other strings of data by passing a dictionary as\n    the optional entities parameter.  The keys and values must all be\n    strings; each key will be replaced with its corresponding value.\n\n    \"\"\"\n    # find character data, re.DOTALL includes linefeed in .\n    pattern = re.compile('<!\\[CDATA\\[.*\\]\\]>', re.DOTALL)\n    iterator = pattern.finditer(data)\n    start = 0\n    bits = []\n    for match in iterator:\n        #grab chunk before first match\n        bit = data[start:match.span()[0]]\n        bit = __escape(bit, entities)\n        bits.append(bit)\n        #grab match\n        bit = data[match.span()[0]:match.span()[1]]\n        bits.extend(bit)\n        start = match.span()[1]\n    # escape tail bit after last match\n    bit = data[start:]\n    bit = __escape(bit, entities)\n    bits.extend(bit)\n    data = ''.join(bits)\n    return data\n\n\nclass RSRSimplerXMLGenerator(XMLGenerator):\n    \"\"\"subclassed to be able to call custom escape() function, see above\n    \"\"\"\n    def characters(self, content):\n        self._write(escape(content))\n\n    def addQuickElement(self, name, contents=None, attrs=None):\n        \"Convenience method for adding an element with no children\"\n        if attrs is None: attrs = {}\n        self.startElement(name, attrs)\n        if contents is not None:\n            self.characters(contents)\n        self.endElement(name)\n\n\nclass RSRMediaRssFeed(Rss201rev2Feed):\n    def rss_attributes(self):\n        attrs = super(RSRMediaRssFeed, self).rss_attributes()\n        attrs['xmlns:media']    = 'http://search.yahoo.com/mrss/'\n        attrs['xmlns:atom']     = 'http://www.w3.org/2005/Atom'\n        return attrs\n\n    def add_item_elements(self, handler, item):\n        \"\"\"Callback to add elements to each item (item/entry) element.\"\"\"\n        super(RSRMediaRssFeed, self).add_item_elements(handler, item)\n\n        if 'media:title' in item:\n            handler.addQuickElement(u\"media:title\", item['title'])\n        if 'media:description' in item:\n            handler.addQuickElement(u\"media:description\", item['media:description'])\n        if 'media:credit' in item:\n            handler.addQuickElement(u\"media:credit\", item['media:credit'])\n\n        if 'content_url' in item:\n            content = dict(url=item['content_url'])\n            if 'content_width' in item:\n                content['width'] = str(item['content_width'])\n            if 'content_height' in item:\n                content['height'] = str(item['content_height'])\n            handler.addQuickElement(u\"media:content\", '', content)\n\n        if 'thumbnail_url' in item:\n            thumbnail = dict(url=item['thumbnail_url'])\n            if 'thumbnail_width' in item:\n                thumbnail['width'] = str(item['thumbnail_width'])\n            if 'thumbnail_height' in item:\n                thumbnail['height'] = str(item['thumbnail_height'])\n            handler.addQuickElement(u\"media:thumbnail\", '', thumbnail)\n\n        if 'keywords' in item:\n            handler.addQuickElement(u\"media:keywords\", item['keywords'])\n\n    def write(self, outfile, encoding):\n        handler = RSRSimplerXMLGenerator(outfile, encoding)\n        handler.startDocument()\n        handler.startElement(u\"rss\", self.rss_attributes())\n        handler.startElement(u\"channel\", self.root_attributes())\n        self.add_root_elements(handler)\n        self.write_items(handler)\n        self.endChannelElement(handler)\n        handler.endElement(u\"rss\")\n\nclass UpdateFeed(Feed):\n    \"\"\"base class generating Update feeds\n    \"\"\"\n    feed_type = RSRMediaRssFeed\n\n    def link(self, obj):\n        if not obj:\n            raise FeedDoesNotExist\n        return obj.get_absolute_url()\n\n    def item_link(self, item):\n        return item.get_absolute_url()\n\n    def item_title(self, item):\n        return item.title\n\n    def item_description(self, item):\n        try:\n            size = item.photo.size\n            return '<![CDATA[<p><a href=\"%s\"><img src=\"%s\" alt=\"\" /></a></p><p>%s</p>]]>' % (\n                item.get_absolute_url(),\n                item.photo.thumbnail.absolute_url,\n                item.text,\n            )\n        except:\n            return item.text\n\n    def item_pubdate(self, item):\n        return item.created_at\n\n    def item_author_name(self, item):\n        return item.user.get_full_name()\n\n    def item_credit(self, item):\n        return item.photo_credit\n\n    def item_extra_kwargs(self, item):\n        \"\"\"return a dictionary to the feedgenerator for each item to be added to the feed.\n        \"\"\"\n        try:\n            size = item.photo.size\n            photo = item.photo\n            kwargs = {\n                'media:title': item.title,\n                'media:description': item.photo_caption,\n                'media:credit': item.photo_credit,\n                'content_url': photo.url,\n                'content_width': photo.width,\n                'content_height': photo.height,\n                'thumbnail_url': photo.thumbnail.absolute_url,\n                'thumbnail_width': photo.thumbnail.width(),\n                'thumbnail_height': photo.thumbnail.height(),\n            }\n            return kwargs\n        except:\n            return {}\n\n\nclass ProjectUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates of a project.\"\"\"\n    def get_object(self, request, project_id):\n        return Project.objects.get(pk__exact=project_id)\n\n    def title(self, obj):\n        return _(u'Akvo RSR project %(id)d: %(project_title)s') % {\n            'id': obj.id,\n            'project_title': obj.title\n        }\n\n    def description(self, obj):\n        return _(u'Project updates for project %(project_title)s') % {\n            'project_title': obj.title\n        }\n\n    def items(self, obj):\n        # Limited to 25 items to prevent gateway timeouts.\n        return ProjectUpdate.objects.filter(project__id=obj.id).order_by('-id')[:25]\n\n\nclass OrganisationUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates of an organisation.\"\"\"\n    feed_type = RSRMediaRssFeed\n\n    def get_object(self, request, org_id):\n        return get_object_or_404(Organisation, id=int(org_id))\n\n    def title(self, obj):\n        return _(u'Projects of %(org_name)s') % {'org_name':obj.name,}\n\n    def description(self, obj):\n        if obj.name == obj.long_name:\n            return _(u\"Project updates for projects partnered by %(org_name)s\") % {\n                'org_name': obj.name\n            }\n        else:\n            return _(\n                u\"Project updates for projects partnered by %(org_name)s - %(long_name)s\"\n            ) % {'org_name': obj.name, 'long_name': obj.long_name}\n\n    def items(self, obj):\n        # Limited to 25 items to prevent gateway timeouts.\n        return obj.all_updates()[:25]\n\n    def item_title(self, item):\n        return _(\n            u'Project %(project_id)d - %(project_title)s: %(update_title)s'\n        ) % {\n            'project_id': item.project.id,\n            'project_title': item.project.title,\n            'update_title': item.title\n        }\n\n\nclass AllProjectUpdates(UpdateFeed):\n    \"\"\"RSS feed for last 25 RSR updates.\"\"\"\n    title = _(u'Last 25 RSR project updates')\n    \n    def link(self):\n        return reverse('update-directory')\n\n    description = _(u'Project updates for all Akvo RSR projects')\n\n    def items(self):\n        # Limited to 25 items to prevent gateway timeouts.\n        return ProjectUpdate.objects.select_related().order_by('-id')[:25]\n\n    def item_title(self, item):\n        return _(\n            u'Project %(project_id)d - %(project_title)s: %(update_title)s'\n        ) % {\n            'project_id': item.project.id,\n            'project_title': item.project.title,\n            'update_title': item.title\n        }\n", "path": "akvo/rsr/feeds.py"}]}