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