]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/blog.py
wiki: Postprocess links
[ipfire.org.git] / src / backend / blog.py
CommitLineData
0a6875dc
MT
1#!/usr/bin/python
2
541c952b 3import datetime
c70a7c29 4import feedparser
2de5ad8a 5import markdown2
c70a7c29 6import re
7e64f6a3 7import textile
d1a77356 8import tornado.gen
c70a7c29 9import unicodedata
7e64f6a3 10
0a6875dc 11from . import misc
9523790a 12from . import util
a3a850a4 13from .decorators import *
0a6875dc 14
2de5ad8a
MT
15# Used to automatically link some things
16link_patterns = (
17 # Find bug reports
18 (re.compile(r"(?:#(\d+))", re.I), r"https://bugzilla.ipfire.org/show_bug.cgi?id=\1"),
19
2de5ad8a 20 # CVE Numbers
5c6aa47c 21 (re.compile(r"(?:CVE)[\s\-](\d{4}\-\d+)"), r"https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1"),
2de5ad8a
MT
22)
23
0a6875dc
MT
24class Blog(misc.Object):
25 def _get_post(self, query, *args):
26 res = self.db.get(query, *args)
27
28 if res:
29 return Post(self.backend, res.id, data=res)
30
31 def _get_posts(self, query, *args):
32 res = self.db.query(query, *args)
33
34 for row in res:
35 yield Post(self.backend, row.id, data=row)
36
487417ad
MT
37 def get_by_id(self, id):
38 return self._get_post("SELECT * FROM blog \
39 WHERE id = %s", id)
40
df157ede
MT
41 def get_by_slug(self, slug, published=True):
42 if published:
43 return self._get_post("SELECT * FROM blog \
44 WHERE slug = %s AND published_at <= NOW()", slug)
45
0a6875dc 46 return self._get_post("SELECT * FROM blog \
df157ede
MT
47 WHERE slug = %s", slug)
48
0a6875dc
MT
49 def get_newest(self, limit=None):
50 return self._get_posts("SELECT * FROM blog \
51 WHERE published_at IS NOT NULL \
52 AND published_at <= NOW() \
53 ORDER BY published_at DESC LIMIT %s", limit)
54
55 def get_by_tag(self, tag, limit=None):
56 return self._get_posts("SELECT * FROM blog \
57 WHERE published_at IS NOT NULL \
58 AND published_at <= NOW() \
59 AND %s = ANY(tags) \
4bde7f18 60 ORDER BY published_at DESC LIMIT %s", tag, limit)
0a6875dc 61
cdf85ee7 62 def get_by_author(self, author, limit=None):
0a6875dc 63 return self._get_posts("SELECT * FROM blog \
cdf85ee7 64 WHERE (author = %s OR author_uid = %s) \
0a6875dc
MT
65 AND published_at IS NOT NULL \
66 AND published_at <= NOW() \
cdf85ee7
MT
67 ORDER BY published_at DESC LIMIT %s",
68 author.name, author.uid, limit)
0a6875dc 69
7e64f6a3
MT
70 def get_by_year(self, year):
71 return self._get_posts("SELECT * FROM blog \
72 WHERE EXTRACT(year FROM published_at) = %s \
73 AND published_at IS NOT NULL \
74 AND published_at <= NOW() \
75 ORDER BY published_at DESC", year)
76
0b342a05
MT
77 def get_drafts(self, author=None, limit=None):
78 if author:
79 return self._get_posts("SELECT * FROM blog \
80 WHERE author_uid = %s \
81 AND (published_at IS NULL OR published_at > NOW()) \
82 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s",
83 author.uid, limit)
84
85 return self._get_posts("SELECT * FROM blog \
86 WHERE (published_at IS NULL OR published_at > NOW()) \
87 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s", limit)
88
0a6875dc 89 def search(self, query, limit=None):
9523790a 90 query = util.parse_search_query(query)
a47ddc3d 91
0a6875dc
MT
92 return self._get_posts("SELECT blog.* FROM blog \
93 LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
94 WHERE search_index.document @@ to_tsquery('english', %s) \
95 ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
96 LIMIT %s", query, query, limit)
97
611adbfb
MT
98 def has_had_recent_activity(self, t=None):
99 if t is None:
100 t = datetime.timedelta(hours=24)
101
102 res = self.db.get("SELECT COUNT(*) AS count FROM blog \
103 WHERE published_at IS NOT NULL AND published_at >= NOW() - %s", t)
104
105 if res and res.count > 0:
106 return True
107
108 return False
109
694c4f08 110 def create_post(self, title, text, author, tags=[], lang="markdown"):
541c952b
MT
111 """
112 Creates a new post and returns the resulting Post object
113 """
694c4f08
MT
114 # Pre-render HTML
115 html = self._render_text(text, lang=lang)
116
117 return self._get_post("INSERT INTO blog(title, slug, text, html, lang, author_uid, tags) \
118 VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", title, self._make_slug(title), text,
119 html, lang, author.uid, list(tags))
541c952b 120
c70a7c29
MT
121 def _make_slug(self, s):
122 # Remove any non-ASCII characters
123 try:
124 s = unicodedata.normalize("NFKD", s)
125 except TypeError:
126 pass
127
128 # Remove excessive whitespace
129 s = re.sub(r"[^\w]+", " ", s)
130
131 slug = "-".join(s.split()).lower()
132
133 while True:
134 e = self.db.get("SELECT 1 FROM blog WHERE slug = %s", slug)
135 if not e:
136 break
137
138 slug += "-"
139
140 return slug
141
2de5ad8a
MT
142 def _render_text(self, text, lang="markdown"):
143 if lang == "markdown":
144 return markdown2.markdown(text, link_patterns=link_patterns,
e0ef6d39
MT
145 extras=[
146 "code-friendly",
147 "cuddled-lists",
148 "fenced-code-blocks",
149 "footnotes",
150 "header-ids",
151 "link-patterns",
152 "tables",
153 ])
2de5ad8a
MT
154
155 elif lang == "textile":
156 return textile.textile(text)
157
158 return text
159
0a6875dc
MT
160 def refresh(self):
161 """
162 Needs to be called after a post has been changed
163 and updates the search index.
164 """
165 self.db.execute("REFRESH MATERIALIZED VIEW blog_search_index")
166
7e64f6a3
MT
167 @property
168 def years(self):
169 res = self.db.query("SELECT DISTINCT EXTRACT(year FROM published_at)::integer AS year \
170 FROM blog WHERE published_at IS NOT NULL AND published_at <= NOW() \
171 ORDER BY year DESC")
172
173 for row in res:
174 yield row.year
175
d1a77356 176 @tornado.gen.coroutine
c70a7c29
MT
177 def update_feeds(self):
178 """
179 Updates all enabled feeds
180 """
181 for feed in self.db.query("SELECT * FROM blog_feeds WHERE enabled IS TRUE"):
182 try:
183 f = feedparser.parse(feed.url)
184 except Exception as e:
185 raise e
186
187 with self.db.transaction():
188 # Update name
189 self.db.execute("UPDATE blog_feeds SET name = %s \
190 WHERE id = %s", f.feed.title, feed.id)
191
192 # Walk through all entries
193 for entry in f.entries:
194 # Skip everything without the "blog.ipfire.org" tag
195 try:
196 tags = list((t.term for t in entry.tags))
197
198 if not "blog.ipfire.org" in tags:
199 continue
200 except AttributeError:
201 continue
202
203 # Get link to the posting site
204 link = entry.links[0].href
205
206 # Check if the entry has already been imported
207 res = self.db.get("SELECT id, (updated_at < %s) AS needs_update \
208 FROM blog WHERE feed_id = %s AND foreign_id = %s",
209 entry.updated, feed.id, entry.id)
210 if res:
211 # If the post needs to be updated, we do so
212 if res.needs_update:
213 self.db.execute("UPDATE blog SET title = %s, author = %s, \
214 published_at = %s, updated_at = %s, html = %s, link = %s, \
215 tags = %s WHERE id = %s", entry.title, entry.author,
216 entry.published, entry.updated, entry.summary, link,
217 feed.tags + tags, res.id)
218
219 # Done here
220 continue
221
222 # Insert the new post
223 self.db.execute("INSERT INTO blog(title, slug, author, \
224 published_at, html, link, tags, updated_at, feed_id, foreign_id) \
225 VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
226 entry.title, self._make_slug(entry.title), entry.author,
227 entry.published, entry.summary, link, feed.tags + tags,
228 entry.updated, feed.id, entry.id)
229
20277bf5
MT
230 # Mark feed as updated
231 self.db.execute("UPDATE blog_feeds SET last_updated_at = CURRENT_TIMESTAMP \
232 WHERE id = %s" % feed.id)
233
c70a7c29
MT
234 # Refresh the search index
235 with self.db.transaction():
236 self.refresh()
237
0a6875dc
MT
238
239class Post(misc.Object):
240 def init(self, id, data=None):
241 self.id = id
242 self.data = data
243
541c952b
MT
244 # Title
245
93725180
MT
246 @property
247 def title(self):
0a6875dc
MT
248 return self.data.title
249
250 @property
251 def slug(self):
252 return self.data.slug
253
a3a850a4 254 @lazy_property
0a6875dc
MT
255 def author(self):
256 if self.data.author_uid:
257 return self.backend.accounts.get_by_uid(self.data.author_uid)
258
cdf85ee7
MT
259 return self.data.author
260
0a6875dc
MT
261 @property
262 def created_at(self):
263 return self.data.created_at
264
2de5ad8a
MT
265 @property
266 def lang(self):
267 return self.data.lang
268
541c952b
MT
269 # Published?
270
0a6875dc
MT
271 @property
272 def published_at(self):
273 return self.data.published_at
274
541c952b
MT
275 def is_published(self):
276 """
277 Returns True if the post is already published
278 """
279 return self.published_at and self.published_at <= datetime.datetime.now()
280
9ea64cef 281 def publish(self, when=None):
541c952b
MT
282 if self.is_published():
283 return
284
9ea64cef
MT
285 self.db.execute("UPDATE blog SET published_at = COALESCE(%s, CURRENT_TIMESTAMP) \
286 WHERE id = %s", when, self.id)
541c952b
MT
287
288 # Update search indices
289 self.backend.blog.refresh()
290
291 # Updated?
292
7e64f6a3 293 @property
541c952b
MT
294 def updated_at(self):
295 return self.data.updated_at
296
541c952b
MT
297 # Text
298
93725180
MT
299 @property
300 def text(self):
541c952b
MT
301 return self.data.text
302
541c952b 303 # HTML
7e64f6a3 304
a3a850a4 305 @lazy_property
0a6875dc
MT
306 def html(self):
307 """
308 Returns this post as rendered HTML
309 """
2de5ad8a 310 return self.data.html or self.backend.blog._render_text(self.text, lang=self.lang)
8ebc98d4 311
541c952b
MT
312 # Tags
313
93725180
MT
314 @property
315 def tags(self):
8ebc98d4 316 return self.data.tags
1e76fec4 317
93725180 318 # Link
541c952b 319
1e76fec4
MT
320 @property
321 def link(self):
322 return self.data.link
984e4e7b 323
a3a850a4 324 @lazy_property
984e4e7b
MT
325 def release(self):
326 return self.backend.releases._get_release("SELECT * FROM releases \
327 WHERE published IS NOT NULL AND published <= NOW() AND blog_id = %s", self.id)
e8a81a70
MT
328
329 def is_editable(self, editor):
330 # Authors can edit their own posts
331 return self.author == editor
93725180
MT
332
333 def update(self, title, text, tags=[]):
334 """
335 Called to update the content of this post
336 """
337 # Update slug when post isn't published yet
baa294fb
MT
338 slug = self.backend.blog._make_slug(title) \
339 if not self.is_published() and not self.title == title else self.slug
93725180 340
694c4f08
MT
341 # Render and cache HTML
342 html = self.backend.blog._render_text(text, lang=self.lang)
93725180 343
694c4f08 344 self.db.execute("UPDATE blog SET title = %s, slug = %s, text = %s, html = %s, \
93725180 345 tags = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
694c4f08 346 title, slug, text, html, list(tags), self.id)
93725180
MT
347
348 # Update cache
349 self.data.update({
350 "title" : title,
351 "slug" : slug,
352 "text" : text,
d73edae7 353 "html" : html,
93725180
MT
354 "tags" : tags,
355 })
356
357 # Update search index if post is published
358 if self.is_published():
359 self.backend.blog.refresh()
914238a5
MT
360
361 def delete(self):
362 self.db.execute("DELETE FROM blog WHERE id = %s", self.id)
363
364 # Update search indices
365 self.backend.blog.refresh()