]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/blog.py
7 import markdown
.extensions
8 import markdown
.preprocessors
14 from .decorators
import *
16 class Blog(misc
.Object
):
17 def _get_post(self
, query
, *args
):
18 res
= self
.db
.get(query
, *args
)
21 return Post(self
.backend
, res
.id, data
=res
)
23 def _get_posts(self
, query
, *args
):
24 res
= self
.db
.query(query
, *args
)
27 yield Post(self
.backend
, row
.id, data
=row
)
29 def get_by_id(self
, id):
30 return self
._get
_post
("SELECT * FROM blog \
33 def get_by_slug(self
, slug
, published
=True):
35 return self
._get
_post
("SELECT * FROM blog \
36 WHERE slug = %s AND published_at <= NOW()", slug
)
38 return self
._get
_post
("SELECT * FROM blog \
39 WHERE slug = %s", slug
)
41 def get_newest(self
, limit
=None):
42 return self
._get
_posts
("SELECT * FROM blog \
43 WHERE published_at IS NOT NULL \
44 AND published_at <= NOW() \
45 ORDER BY published_at DESC LIMIT %s", limit
)
47 def get_by_tag(self
, tag
, limit
=None):
48 return self
._get
_posts
("SELECT * FROM blog \
49 WHERE published_at IS NOT NULL \
50 AND published_at <= NOW() \
52 ORDER BY published_at DESC LIMIT %s", tag
, limit
)
54 def get_by_author(self
, author
, limit
=None):
55 return self
._get
_posts
("SELECT * FROM blog \
56 WHERE (author = %s OR author_uid = %s) \
57 AND published_at IS NOT NULL \
58 AND published_at <= NOW() \
59 ORDER BY published_at DESC LIMIT %s",
60 author
.name
, author
.uid
, limit
)
62 def get_by_year(self
, year
):
63 return self
._get
_posts
("SELECT * FROM blog \
64 WHERE EXTRACT(year FROM published_at) = %s \
65 AND published_at IS NOT NULL \
66 AND published_at <= NOW() \
67 ORDER BY published_at DESC", year
)
69 def get_drafts(self
, author
=None, limit
=None):
71 return self
._get
_posts
("SELECT * FROM blog \
72 WHERE author_uid = %s \
73 AND (published_at IS NULL OR published_at > NOW()) \
74 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s",
77 return self
._get
_posts
("SELECT * FROM blog \
78 WHERE (published_at IS NULL OR published_at > NOW()) \
79 ORDER BY COALESCE(updated_at, created_at) DESC LIMIT %s", limit
)
81 def search(self
, query
, limit
=None):
82 return self
._get
_posts
("SELECT blog.* FROM blog \
83 LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
84 WHERE search_index.document @@ websearch_to_tsquery('english', %s) \
85 ORDER BY ts_rank(search_index.document, websearch_to_tsquery('english', %s)) DESC \
86 LIMIT %s", query
, query
, limit
)
88 def has_had_recent_activity(self
, **kwargs
):
89 t
= datetime
.timedelta(**kwargs
)
91 res
= self
.db
.get("SELECT COUNT(*) AS count FROM blog \
92 WHERE published_at IS NOT NULL AND published_at BETWEEN NOW() - %s AND NOW()", t
)
94 if res
and res
.count
> 0:
99 def create_post(self
, title
, text
, author
, tags
=[], lang
="markdown"):
101 Creates a new post and returns the resulting Post object
104 html
= self
._render
_text
(text
, lang
=lang
)
106 return self
._get
_post
("INSERT INTO blog(title, slug, text, html, lang, author_uid, tags) \
107 VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", title
, self
._make
_slug
(title
), text
,
108 html
, lang
, author
.uid
, list(tags
))
110 def _make_slug(self
, s
):
111 # Remove any non-ASCII characters
113 s
= unicodedata
.normalize("NFKD", s
)
117 # Remove excessive whitespace
118 s
= re
.sub(r
"[^\w]+", " ", s
)
120 slug
= "-".join(s
.split()).lower()
123 e
= self
.db
.get("SELECT 1 FROM blog WHERE slug = %s", slug
)
131 def _render_text(self
, text
, lang
="markdown"):
132 if lang
== "markdown":
133 return markdown
.markdown(text
,
135 PrettyLinksExtension(),
145 elif lang
== "textile":
146 return textile
.textile(text
)
152 Needs to be called after a post has been changed
153 and updates the search index.
155 self
.db
.execute("REFRESH MATERIALIZED VIEW blog_search_index")
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() \
166 async def announce(self
):
167 posts
= self
._get
_posts
("SELECT * FROM blog \
168 WHERE (published_at IS NOT NULL AND published_at <= NOW()) \
169 AND announced_at IS NULL")
172 await post
.announce()
174 async def update_feeds(self
):
176 Updates all enabled feeds
178 for feed
in self
.db
.query("SELECT * FROM blog_feeds WHERE enabled IS TRUE"):
180 f
= feedparser
.parse(feed
.url
)
181 except Exception as e
:
184 with self
.db
.transaction():
186 self
.db
.execute("UPDATE blog_feeds SET name = %s \
187 WHERE id = %s", f
.feed
.title
, feed
.id)
189 # Walk through all entries
190 for entry
in f
.entries
:
191 # Skip everything without the "blog.ipfire.org" tag
193 tags
= list((t
.term
for t
in entry
.tags
))
195 if not "blog.ipfire.org" in tags
:
197 except AttributeError:
200 # Get link to the posting site
201 link
= entry
.links
[0].href
203 # Check if the entry has already been imported
204 res
= self
.db
.get("SELECT id, (updated_at < %s) AS needs_update \
205 FROM blog WHERE feed_id = %s AND foreign_id = %s",
206 entry
.updated
, feed
.id, entry
.id)
208 # If the post needs to be updated, we do so
210 self
.db
.execute("UPDATE blog SET title = %s, author = %s, \
211 published_at = %s, updated_at = %s, html = %s, link = %s, \
212 tags = %s WHERE id = %s", entry
.title
, entry
.author
,
213 entry
.published
, entry
.updated
, entry
.summary
, link
,
214 feed
.tags
+ tags
, res
.id)
219 # Insert the new post
220 self
.db
.execute("INSERT INTO blog(title, slug, author, \
221 published_at, html, link, tags, updated_at, feed_id, foreign_id) \
222 VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
223 entry
.title
, self
._make
_slug
(entry
.title
), entry
.author
,
224 entry
.published
, entry
.summary
, link
, feed
.tags
+ tags
,
225 entry
.updated
, feed
.id, entry
.id)
227 # Mark feed as updated
228 self
.db
.execute("UPDATE blog_feeds SET last_updated_at = CURRENT_TIMESTAMP \
229 WHERE id = %s" % feed
.id)
231 # Refresh the search index
232 with self
.db
.transaction():
236 class Post(misc
.Object
):
237 def init(self
, id, data
=None):
245 return self
.data
.title
249 return self
.data
.slug
253 if self
.data
.author_uid
:
254 return self
.backend
.accounts
.get_by_uid(self
.data
.author_uid
)
256 return self
.data
.author
259 def created_at(self
):
260 return self
.data
.created_at
264 return self
.data
.lang
269 def published_at(self
):
270 return self
.data
.published_at
272 def is_published(self
):
274 Returns True if the post is already published
276 return self
.published_at
and self
.published_at
<= datetime
.datetime
.now()
278 def publish(self
, when
=None):
279 if self
.is_published():
282 self
.db
.execute("UPDATE blog SET published_at = COALESCE(%s, CURRENT_TIMESTAMP) \
283 WHERE id = %s", when
, self
.id)
285 # Update search indices
286 self
.backend
.blog
.refresh()
291 def updated_at(self
):
292 return self
.data
.updated_at
298 return self
.data
.text
305 Returns this post as rendered HTML
307 return self
.data
.html
or self
.backend
.blog
._render
_text
(self
.text
, lang
=self
.lang
)
311 h
= html2text
.HTML2Text()
312 h
.ignore_links
= True
314 return h
.handle(self
.html
)
320 paragraphs
= self
.plaintext
.split("\n\n")
324 for paragraph
in paragraphs
:
325 excerpt
.append(paragraph
)
327 # Add another paragraph if we encountered a headline
328 if paragraph
.startswith("#"):
331 # End if this paragraph was long enough
332 if len(paragraph
) >= 40:
335 return "\n\n".join(excerpt
)
341 return self
.data
.tags
347 return self
.data
.link
351 return self
.backend
.releases
._get
_release
("SELECT * FROM releases \
352 WHERE published IS NOT NULL AND published <= NOW() AND blog_id = %s", self
.id)
354 def is_editable(self
, editor
):
355 # Authors can edit their own posts
356 return self
.author
== editor
358 def update(self
, title
, text
, tags
=[]):
360 Called to update the content of this post
362 # Update slug when post isn't published yet
363 slug
= self
.backend
.blog
._make
_slug
(title
) \
364 if not self
.is_published() and not self
.title
== title
else self
.slug
366 # Render and cache HTML
367 html
= self
.backend
.blog
._render
_text
(text
, lang
=self
.lang
)
369 self
.db
.execute("UPDATE blog SET title = %s, slug = %s, text = %s, html = %s, \
370 tags = %s, updated_at = CURRENT_TIMESTAMP WHERE id = %s",
371 title
, slug
, text
, html
, list(tags
), self
.id)
382 # Update search index if post is published
383 if self
.is_published():
384 self
.backend
.blog
.refresh()
387 self
.db
.execute("DELETE FROM blog WHERE id = %s", self
.id)
389 # Update search indices
390 self
.backend
.blog
.refresh()
392 async def announce(self
):
393 # Get people who should receive this message
394 group
= self
.backend
.groups
.get_by_gid("promotional-consent")
398 with self
.db
.transaction():
399 # Generate an email for everybody in this group
400 for account
in group
:
401 self
.backend
.messages
.send_template("blog/messages/announcement",
402 account
=account
, post
=self
)
404 # Mark this post as announced
405 self
.db
.execute("UPDATE blog SET announced_at = CURRENT_TIMESTAMP \
406 WHERE id = %s", self
.id)
409 class PrettyLinksExtension(markdown
.extensions
.Extension
):
410 def extendMarkdown(self
, md
):
411 md
.preprocessors
.register(BugzillaLinksPreprocessor(md
), "bugzilla", 10)
412 md
.preprocessors
.register(CVELinksPreprocessor(md
), "cve", 10)
415 class BugzillaLinksPreprocessor(markdown
.preprocessors
.Preprocessor
):
416 regex
= re
.compile(r
"(?:#(\d{5,}))", re
.I
)
418 def run(self
, lines
):
420 yield self
.regex
.sub(r
"[#\1](https://bugzilla.ipfire.org/show_bug.cgi?id=\1)", line
)
423 class CVELinksPreprocessor(markdown
.preprocessors
.Preprocessor
):
424 regex
= re
.compile(r
"(?:CVE)[\s\-](\d{4}\-\d+)")
426 def run(self
, lines
):
428 yield self
.regex
.sub(r
"[CVE-\1](https://cve.mitre.org/cgi-bin/cvename.cgi?name=\1)", line
)