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