|
6229
|
1 #!/usr/bin/python
|
|
|
2
|
|
|
3 #
|
|
|
4 # RSS writer Roundup reactor
|
|
|
5 # Mark Paschal <markpasc@markpasc.org>
|
|
|
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 <markpasc@markpasc.org>"
|
|
|
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('&','&').replace('<','<').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
|