Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/utils/feedgenerator.py: 26%

204 statements  

« prev     ^ index     » next       coverage.py v7.0.5, created at 2023-01-17 06:13 +0000

1""" 

2Syndication feed generation library -- used for generating RSS, etc. 

3 

4Sample usage: 

5 

6>>> from django.utils import feedgenerator 

7>>> feed = feedgenerator.Rss201rev2Feed( 

8... title="Poynter E-Media Tidbits", 

9... link="http://www.poynter.org/column.asp?id=31", 

10... description="A group blog by the sharpest minds in online journalism.", 

11... language="en", 

12... ) 

13>>> feed.add_item( 

14... title="Hello", 

15... link="http://www.holovaty.com/test/", 

16... description="Testing." 

17... ) 

18>>> with open('test.rss', 'w') as fp: 

19... feed.write(fp, 'utf-8') 

20 

21For definitions of the different versions of RSS, see: 

22https://web.archive.org/web/20110718035220/http://diveintomark.org/archives/2004/02/04/incompatible-rss 

23""" 

24import datetime 

25import email 

26from io import StringIO 

27from urllib.parse import urlparse 

28 

29from django.utils.encoding import iri_to_uri 

30from django.utils.xmlutils import SimplerXMLGenerator 

31 

32 

33def rfc2822_date(date): 

34 if not isinstance(date, datetime.datetime): 

35 date = datetime.datetime.combine(date, datetime.time()) 

36 return email.utils.format_datetime(date) 

37 

38 

39def rfc3339_date(date): 

40 if not isinstance(date, datetime.datetime): 

41 date = datetime.datetime.combine(date, datetime.time()) 

42 return date.isoformat() + ("Z" if date.utcoffset() is None else "") 

43 

44 

45def get_tag_uri(url, date): 

46 """ 

47 Create a TagURI. 

48 

49 See 

50 https://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id 

51 """ 

52 bits = urlparse(url) 

53 d = "" 

54 if date is not None: 

55 d = ",%s" % date.strftime("%Y-%m-%d") 

56 return "tag:%s%s:%s/%s" % (bits.hostname, d, bits.path, bits.fragment) 

57 

58 

59class SyndicationFeed: 

60 "Base class for all syndication feeds. Subclasses should provide write()" 

61 

62 def __init__( 

63 self, 

64 title, 

65 link, 

66 description, 

67 language=None, 

68 author_email=None, 

69 author_name=None, 

70 author_link=None, 

71 subtitle=None, 

72 categories=None, 

73 feed_url=None, 

74 feed_copyright=None, 

75 feed_guid=None, 

76 ttl=None, 

77 **kwargs, 

78 ): 

79 def to_str(s): 

80 return str(s) if s is not None else s 

81 

82 categories = categories and [str(c) for c in categories] 

83 self.feed = { 

84 "title": to_str(title), 

85 "link": iri_to_uri(link), 

86 "description": to_str(description), 

87 "language": to_str(language), 

88 "author_email": to_str(author_email), 

89 "author_name": to_str(author_name), 

90 "author_link": iri_to_uri(author_link), 

91 "subtitle": to_str(subtitle), 

92 "categories": categories or (), 

93 "feed_url": iri_to_uri(feed_url), 

94 "feed_copyright": to_str(feed_copyright), 

95 "id": feed_guid or link, 

96 "ttl": to_str(ttl), 

97 **kwargs, 

98 } 

99 self.items = [] 

100 

101 def add_item( 

102 self, 

103 title, 

104 link, 

105 description, 

106 author_email=None, 

107 author_name=None, 

108 author_link=None, 

109 pubdate=None, 

110 comments=None, 

111 unique_id=None, 

112 unique_id_is_permalink=None, 

113 categories=(), 

114 item_copyright=None, 

115 ttl=None, 

116 updateddate=None, 

117 enclosures=None, 

118 **kwargs, 

119 ): 

120 """ 

121 Add an item to the feed. All args are expected to be strings except 

122 pubdate and updateddate, which are datetime.datetime objects, and 

123 enclosures, which is an iterable of instances of the Enclosure class. 

124 """ 

125 

126 def to_str(s): 

127 return str(s) if s is not None else s 

128 

129 categories = categories and [to_str(c) for c in categories] 

130 self.items.append( 

131 { 

132 "title": to_str(title), 

133 "link": iri_to_uri(link), 

134 "description": to_str(description), 

135 "author_email": to_str(author_email), 

136 "author_name": to_str(author_name), 

137 "author_link": iri_to_uri(author_link), 

138 "pubdate": pubdate, 

139 "updateddate": updateddate, 

140 "comments": to_str(comments), 

141 "unique_id": to_str(unique_id), 

142 "unique_id_is_permalink": unique_id_is_permalink, 

143 "enclosures": enclosures or (), 

144 "categories": categories or (), 

145 "item_copyright": to_str(item_copyright), 

146 "ttl": to_str(ttl), 

147 **kwargs, 

148 } 

149 ) 

150 

151 def num_items(self): 

152 return len(self.items) 

153 

154 def root_attributes(self): 

155 """ 

156 Return extra attributes to place on the root (i.e. feed/channel) element. 

157 Called from write(). 

158 """ 

159 return {} 

160 

161 def add_root_elements(self, handler): 

162 """ 

163 Add elements in the root (i.e. feed/channel) element. Called 

164 from write(). 

165 """ 

166 pass 

167 

168 def item_attributes(self, item): 

169 """ 

170 Return extra attributes to place on each item (i.e. item/entry) element. 

171 """ 

172 return {} 

173 

174 def add_item_elements(self, handler, item): 

175 """ 

176 Add elements on each item (i.e. item/entry) element. 

177 """ 

178 pass 

179 

180 def write(self, outfile, encoding): 

181 """ 

182 Output the feed in the given encoding to outfile, which is a file-like 

183 object. Subclasses should override this. 

184 """ 

185 raise NotImplementedError( 

186 "subclasses of SyndicationFeed must provide a write() method" 

187 ) 

188 

189 def writeString(self, encoding): 

190 """ 

191 Return the feed in the given encoding as a string. 

192 """ 

193 s = StringIO() 

194 self.write(s, encoding) 

195 return s.getvalue() 

196 

197 def latest_post_date(self): 

198 """ 

199 Return the latest item's pubdate or updateddate. If no items 

200 have either of these attributes this return the current UTC date/time. 

201 """ 

202 latest_date = None 

203 date_keys = ("updateddate", "pubdate") 

204 

205 for item in self.items: 

206 for date_key in date_keys: 

207 item_date = item.get(date_key) 

208 if item_date: 

209 if latest_date is None or item_date > latest_date: 

210 latest_date = item_date 

211 

212 return latest_date or datetime.datetime.now(tz=datetime.timezone.utc) 

213 

214 

215class Enclosure: 

216 """An RSS enclosure""" 

217 

218 def __init__(self, url, length, mime_type): 

219 "All args are expected to be strings" 

220 self.length, self.mime_type = length, mime_type 

221 self.url = iri_to_uri(url) 

222 

223 

224class RssFeed(SyndicationFeed): 

225 content_type = "application/rss+xml; charset=utf-8" 

226 

227 def write(self, outfile, encoding): 

228 handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True) 

229 handler.startDocument() 

230 handler.startElement("rss", self.rss_attributes()) 

231 handler.startElement("channel", self.root_attributes()) 

232 self.add_root_elements(handler) 

233 self.write_items(handler) 

234 self.endChannelElement(handler) 

235 handler.endElement("rss") 

236 

237 def rss_attributes(self): 

238 return { 

239 "version": self._version, 

240 "xmlns:atom": "http://www.w3.org/2005/Atom", 

241 } 

242 

243 def write_items(self, handler): 

244 for item in self.items: 

245 handler.startElement("item", self.item_attributes(item)) 

246 self.add_item_elements(handler, item) 

247 handler.endElement("item") 

248 

249 def add_root_elements(self, handler): 

250 handler.addQuickElement("title", self.feed["title"]) 

251 handler.addQuickElement("link", self.feed["link"]) 

252 handler.addQuickElement("description", self.feed["description"]) 

253 if self.feed["feed_url"] is not None: 

254 handler.addQuickElement( 

255 "atom:link", None, {"rel": "self", "href": self.feed["feed_url"]} 

256 ) 

257 if self.feed["language"] is not None: 

258 handler.addQuickElement("language", self.feed["language"]) 

259 for cat in self.feed["categories"]: 

260 handler.addQuickElement("category", cat) 

261 if self.feed["feed_copyright"] is not None: 

262 handler.addQuickElement("copyright", self.feed["feed_copyright"]) 

263 handler.addQuickElement("lastBuildDate", rfc2822_date(self.latest_post_date())) 

264 if self.feed["ttl"] is not None: 

265 handler.addQuickElement("ttl", self.feed["ttl"]) 

266 

267 def endChannelElement(self, handler): 

268 handler.endElement("channel") 

269 

270 

271class RssUserland091Feed(RssFeed): 

272 _version = "0.91" 

273 

274 def add_item_elements(self, handler, item): 

275 handler.addQuickElement("title", item["title"]) 

276 handler.addQuickElement("link", item["link"]) 

277 if item["description"] is not None: 

278 handler.addQuickElement("description", item["description"]) 

279 

280 

281class Rss201rev2Feed(RssFeed): 

282 # Spec: https://cyber.harvard.edu/rss/rss.html 

283 _version = "2.0" 

284 

285 def add_item_elements(self, handler, item): 

286 handler.addQuickElement("title", item["title"]) 

287 handler.addQuickElement("link", item["link"]) 

288 if item["description"] is not None: 

289 handler.addQuickElement("description", item["description"]) 

290 

291 # Author information. 

292 if item["author_name"] and item["author_email"]: 

293 handler.addQuickElement( 

294 "author", "%s (%s)" % (item["author_email"], item["author_name"]) 

295 ) 

296 elif item["author_email"]: 

297 handler.addQuickElement("author", item["author_email"]) 

298 elif item["author_name"]: 

299 handler.addQuickElement( 

300 "dc:creator", 

301 item["author_name"], 

302 {"xmlns:dc": "http://purl.org/dc/elements/1.1/"}, 

303 ) 

304 

305 if item["pubdate"] is not None: 

306 handler.addQuickElement("pubDate", rfc2822_date(item["pubdate"])) 

307 if item["comments"] is not None: 

308 handler.addQuickElement("comments", item["comments"]) 

309 if item["unique_id"] is not None: 

310 guid_attrs = {} 

311 if isinstance(item.get("unique_id_is_permalink"), bool): 

312 guid_attrs["isPermaLink"] = str(item["unique_id_is_permalink"]).lower() 

313 handler.addQuickElement("guid", item["unique_id"], guid_attrs) 

314 if item["ttl"] is not None: 

315 handler.addQuickElement("ttl", item["ttl"]) 

316 

317 # Enclosure. 

318 if item["enclosures"]: 

319 enclosures = list(item["enclosures"]) 

320 if len(enclosures) > 1: 

321 raise ValueError( 

322 "RSS feed items may only have one enclosure, see " 

323 "http://www.rssboard.org/rss-profile#element-channel-item-enclosure" 

324 ) 

325 enclosure = enclosures[0] 

326 handler.addQuickElement( 

327 "enclosure", 

328 "", 

329 { 

330 "url": enclosure.url, 

331 "length": enclosure.length, 

332 "type": enclosure.mime_type, 

333 }, 

334 ) 

335 

336 # Categories. 

337 for cat in item["categories"]: 

338 handler.addQuickElement("category", cat) 

339 

340 

341class Atom1Feed(SyndicationFeed): 

342 # Spec: https://tools.ietf.org/html/rfc4287 

343 content_type = "application/atom+xml; charset=utf-8" 

344 ns = "http://www.w3.org/2005/Atom" 

345 

346 def write(self, outfile, encoding): 

347 handler = SimplerXMLGenerator(outfile, encoding, short_empty_elements=True) 

348 handler.startDocument() 

349 handler.startElement("feed", self.root_attributes()) 

350 self.add_root_elements(handler) 

351 self.write_items(handler) 

352 handler.endElement("feed") 

353 

354 def root_attributes(self): 

355 if self.feed["language"] is not None: 

356 return {"xmlns": self.ns, "xml:lang": self.feed["language"]} 

357 else: 

358 return {"xmlns": self.ns} 

359 

360 def add_root_elements(self, handler): 

361 handler.addQuickElement("title", self.feed["title"]) 

362 handler.addQuickElement( 

363 "link", "", {"rel": "alternate", "href": self.feed["link"]} 

364 ) 

365 if self.feed["feed_url"] is not None: 

366 handler.addQuickElement( 

367 "link", "", {"rel": "self", "href": self.feed["feed_url"]} 

368 ) 

369 handler.addQuickElement("id", self.feed["id"]) 

370 handler.addQuickElement("updated", rfc3339_date(self.latest_post_date())) 

371 if self.feed["author_name"] is not None: 

372 handler.startElement("author", {}) 

373 handler.addQuickElement("name", self.feed["author_name"]) 

374 if self.feed["author_email"] is not None: 

375 handler.addQuickElement("email", self.feed["author_email"]) 

376 if self.feed["author_link"] is not None: 

377 handler.addQuickElement("uri", self.feed["author_link"]) 

378 handler.endElement("author") 

379 if self.feed["subtitle"] is not None: 

380 handler.addQuickElement("subtitle", self.feed["subtitle"]) 

381 for cat in self.feed["categories"]: 

382 handler.addQuickElement("category", "", {"term": cat}) 

383 if self.feed["feed_copyright"] is not None: 

384 handler.addQuickElement("rights", self.feed["feed_copyright"]) 

385 

386 def write_items(self, handler): 

387 for item in self.items: 

388 handler.startElement("entry", self.item_attributes(item)) 

389 self.add_item_elements(handler, item) 

390 handler.endElement("entry") 

391 

392 def add_item_elements(self, handler, item): 

393 handler.addQuickElement("title", item["title"]) 

394 handler.addQuickElement("link", "", {"href": item["link"], "rel": "alternate"}) 

395 

396 if item["pubdate"] is not None: 

397 handler.addQuickElement("published", rfc3339_date(item["pubdate"])) 

398 

399 if item["updateddate"] is not None: 

400 handler.addQuickElement("updated", rfc3339_date(item["updateddate"])) 

401 

402 # Author information. 

403 if item["author_name"] is not None: 

404 handler.startElement("author", {}) 

405 handler.addQuickElement("name", item["author_name"]) 

406 if item["author_email"] is not None: 

407 handler.addQuickElement("email", item["author_email"]) 

408 if item["author_link"] is not None: 

409 handler.addQuickElement("uri", item["author_link"]) 

410 handler.endElement("author") 

411 

412 # Unique ID. 

413 if item["unique_id"] is not None: 

414 unique_id = item["unique_id"] 

415 else: 

416 unique_id = get_tag_uri(item["link"], item["pubdate"]) 

417 handler.addQuickElement("id", unique_id) 

418 

419 # Summary. 

420 if item["description"] is not None: 

421 handler.addQuickElement("summary", item["description"], {"type": "html"}) 

422 

423 # Enclosures. 

424 for enclosure in item["enclosures"]: 

425 handler.addQuickElement( 

426 "link", 

427 "", 

428 { 

429 "rel": "enclosure", 

430 "href": enclosure.url, 

431 "length": enclosure.length, 

432 "type": enclosure.mime_type, 

433 }, 

434 ) 

435 

436 # Categories. 

437 for cat in item["categories"]: 

438 handler.addQuickElement("category", "", {"term": cat}) 

439 

440 # Rights. 

441 if item["item_copyright"] is not None: 

442 handler.addQuickElement("rights", item["item_copyright"]) 

443 

444 

445# This isolates the decision of what the system default is, so calling code can 

446# do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed". 

447DefaultFeed = Rss201rev2Feed