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