Skip to content

Commit 2045c3a

Browse files
committed
issue2551085 - add rss feed
https://issues.roundup-tracker.org/issue2551085 add rss generating detector, add link for rss feed to page.html so browser rss feed autodetect will work.
1 parent 05936a0 commit 2045c3a

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed

website/issues/detectors/rsswriter.py

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#!/usr/bin/python
2+
3+
#
4+
# RSS writer Roundup reactor
5+
# Mark Paschal <[email protected]>
6+
#
7+
8+
import os
9+
10+
import logging
11+
logger = logging.getLogger('detector')
12+
13+
import sys
14+
15+
# How many <item>s to have in the feed, at most.
16+
MAX_ITEMS = 30
17+
18+
#
19+
# Module metadata
20+
#
21+
22+
__author__ = "Mark Paschal <[email protected]>"
23+
__copyright__ = "Copyright 2003 Mark Paschal"
24+
__version__ = "1.2"
25+
26+
__changes__ = """
27+
1.1 29 Aug 2003 Produces valid pubDates. Produces pubDates and authors for
28+
change notes. Consolidates a message and change note into one
29+
item. Uses TRACKER_NAME in filename to produce one feed per
30+
tracker. Keeps to MAX_ITEMS limit more efficiently.
31+
1.2 5 Sep 2003 Fixes bug with programmatically submitted issues having
32+
messages without summaries (?!).
33+
x.x 26 Feb 2017 John Rouillard try to deal with truncation of rss
34+
file cause by error in parsing 8'bit characcters in
35+
input message. Further attempts to fix issue by
36+
modifying message bail on 0 length rss file. Delete
37+
it and retry.
38+
"""
39+
40+
__license__ = 'MIT'
41+
42+
#
43+
# Copyright 2003 Mark Paschal
44+
#
45+
# Permission is hereby granted, free of charge, to any person obtaining a copy
46+
# of this software and associated documentation files (the "Software"), to deal
47+
# in the Software without restriction, including without limitation the rights
48+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
49+
# copies of the Software, and to permit persons to whom the Software is
50+
# furnished to do so, subject to the following conditions:
51+
#
52+
# The above copyright notice and this permission notice shall be included in all
53+
# copies or substantial portions of the Software.
54+
#
55+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
56+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
57+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
58+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
59+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
60+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
61+
# SOFTWARE.
62+
#
63+
64+
65+
# The strftime format to use for <pubDate>s.
66+
RSS20_DATE_FORMAT = '%a, %d %b %Y %H:%M:%S %z'
67+
68+
69+
def newRss(title, link, description):
70+
"""Returns an XML Document containing an RSS 2.0 feed with no items."""
71+
import xml.dom.minidom
72+
rss = xml.dom.minidom.Document()
73+
74+
root = rss.appendChild(rss.createElement("rss"))
75+
root.setAttribute("version", "2.0")
76+
root.setAttribute("xmlns:atom","http://www.w3.org/2005/Atom")
77+
78+
channel = root.appendChild(rss.createElement("channel"))
79+
addEl = lambda tag,value: channel.appendChild(rss.createElement(tag)).appendChild(rss.createTextNode(value))
80+
def addElA(tag,attr):
81+
node=rss.createElement(tag)
82+
for attr, val in attr.items():
83+
node.setAttribute(attr, val)
84+
channel.appendChild(node)
85+
86+
addEl("title", title)
87+
addElA('atom:link', attr={"rel": "self",
88+
"type": "application/rss+xml", "href": link + "@@file/rss.xml"})
89+
addEl("link", link)
90+
addEl("description", description)
91+
92+
return rss # has no items
93+
94+
95+
def writeRss(db, cl, nodeid, olddata):
96+
"""
97+
Reacts to a created or changed issue. Puts new messages and the change note
98+
in items in the RSS feed, as determined by the rsswriter.py FILENAME setting.
99+
If no RSS feed exists where FILENAME specifies, a new feed is created with
100+
rsswriter.newRss.
101+
"""
102+
103+
# The filename of a tracker's RSS feed. Tracker config variables
104+
# are placed with the standard '%' operator syntax.
105+
106+
FILENAME = "%s/rss.xml"%db.config['TEMPLATES']
107+
108+
# i.e., roundup.cgi/projects/_file/rss.xml
109+
# FILENAME = "/home/markpasc/public_html/%(TRACKER_NAME)s.xml"
110+
111+
filename = FILENAME % db.config.__dict__
112+
113+
# return if issue is private
114+
if ( db.issue.get(nodeid, 'private') ):
115+
if __debug__:
116+
logger.debug("rss: Private issue. not generating rss")
117+
return
118+
119+
if __debug__:
120+
logger.debug("rss: generating rss for issue %s", nodeid)
121+
122+
# open the RSS
123+
import xml.dom.minidom
124+
from xml.parsers.expat import ExpatError
125+
126+
try:
127+
rss = xml.dom.minidom.parse(filename)
128+
except IOError as e:
129+
if 2 != e.errno: raise
130+
# File not found
131+
rss = newRss(
132+
"%s tracker" % (db.config.TRACKER_NAME,),
133+
db.config.TRACKER_WEB,
134+
"Recent changes to the %s Roundup issue tracker" % (db.config.TRACKER_NAME,)
135+
)
136+
except ExpatError as e:
137+
if os.path.getsize(filename) == 0:
138+
# delete the file, it's broke
139+
os.remove(filename)
140+
# create new rss file
141+
rss = newRss(
142+
"%s tracker" % (db.config.TRACKER_NAME,),
143+
db.config.TRACKER_WEB,
144+
"Recent changes to the %s Roundup issue tracker" % (db.config.TRACKER_NAME,)
145+
)
146+
else:
147+
raise
148+
149+
channel = rss.documentElement.getElementsByTagName('channel')[0]
150+
addEl = lambda parent,tag,value: parent.appendChild(rss.createElement(tag)).appendChild(rss.createTextNode(value))
151+
issuelink = '%sissue%s' % (db.config.TRACKER_WEB, nodeid)
152+
153+
154+
if olddata:
155+
chg = cl.generateChangeNote(nodeid, olddata)
156+
else:
157+
chg = cl.generateCreateNote(nodeid)
158+
159+
def addItem(desc, date, userid):
160+
"""
161+
Adds an RSS item to the RSS document. The title, link, and comments
162+
link are those of the current issue.
163+
164+
desc: the description text to use
165+
date: an appropriately formatted string for pubDate
166+
userid: a Roundup user ID to use as author
167+
"""
168+
169+
item = rss.createElement('item')
170+
171+
addEl(item, 'title', db.issue.get(nodeid, 'title'))
172+
addEl(item, 'link', issuelink)
173+
addEl(item, 'guid', issuelink + '#' + date.replace(' ','+'))
174+
addEl(item, 'comments', issuelink)
175+
addEl(item, 'description', desc.replace('&','&amp;').replace('<','&lt;').replace('\n', '<br>\n'))
176+
addEl(item, 'pubDate', date)
177+
addEl(item, 'author',
178+
'%s (%s)' % (
179+
db.user.get(userid, 'address'),
180+
db.user.get(userid, 'username')
181+
)
182+
)
183+
184+
channel.appendChild(item)
185+
186+
# add detectors directory to path if it's not there.
187+
# FIXME - see if this pollutes the sys.path for other
188+
# trackers.
189+
detector_path="%s/detectors"%(db.config.TRACKER_HOME)
190+
if ( sys.path.count(detector_path) == 0 ):
191+
sys.path.insert(0,detector_path)
192+
193+
from nosyreaction import determineNewMessages
194+
for msgid in determineNewMessages(cl, nodeid, olddata):
195+
logger.debug("Processing new message msg%s for issue%s", msgid, nodeid)
196+
desc = db.msg.get(msgid, 'content')
197+
198+
if desc and chg:
199+
desc += chg
200+
elif chg:
201+
desc = chg
202+
chg = None
203+
204+
addItem(desc or '', db.msg.get(msgid, 'date').pretty(RSS20_DATE_FORMAT), db.msg.get(msgid, 'author'))
205+
206+
if chg:
207+
from time import strftime
208+
addItem(chg.replace('\n----------\n', ''), strftime(RSS20_DATE_FORMAT), db.getuid())
209+
210+
211+
for c in channel.getElementsByTagName('item')[0:-MAX_ITEMS]: # leaves at most MAX_ITEMS at the end
212+
channel.removeChild(c)
213+
214+
# write the RSS
215+
out = open(filename, 'w')
216+
217+
try:
218+
out.write(rss.toxml())
219+
except Exception as e:
220+
# record the falure This should not happen.
221+
logger.error(e)
222+
out.close() # create 0 length file maybe?? But we handle above.
223+
raise # let the user know something went wrong.
224+
225+
out.close()
226+
227+
228+
def init(db):
229+
db.issue.react('create', writeRss)
230+
db.issue.react('set', writeRss)
231+
#SHA: f4c0ccb5d0d9a6ef7829696333b33bc0619b0167

website/issues/html/page.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
</script>
1313
<metal:x define-slot="more-javascript" />
1414

15+
<link rel="alternate" type="application/rss+xml"
16+
tal:attributes="href string:${request/base}@@file/rss.xml"
17+
title="Publicly available tracker feed.">
1518
</head>
1619
<body
1720
tal:define="

0 commit comments

Comments
 (0)