]> git.ipfire.org Git - ipfire.org.git/blame - src/backend/blog.py
Remove download splash
[ipfire.org.git] / src / backend / blog.py
CommitLineData
0a6875dc
MT
1#!/usr/bin/python
2
c70a7c29
MT
3import feedparser
4import re
7e64f6a3 5import textile
c70a7c29 6import unicodedata
7e64f6a3 7
0a6875dc
MT
8from . import misc
9
10class Blog(misc.Object):
11 def _get_post(self, query, *args):
12 res = self.db.get(query, *args)
13
14 if res:
15 return Post(self.backend, res.id, data=res)
16
17 def _get_posts(self, query, *args):
18 res = self.db.query(query, *args)
19
20 for row in res:
21 yield Post(self.backend, row.id, data=row)
22
487417ad
MT
23 def get_by_id(self, id):
24 return self._get_post("SELECT * FROM blog \
25 WHERE id = %s", id)
26
0a6875dc
MT
27 def get_by_slug(self, slug):
28 return self._get_post("SELECT * FROM blog \
29 WHERE slug = %s AND published_at <= NOW()", slug)
30
31 def get_newest(self, limit=None):
32 return self._get_posts("SELECT * FROM blog \
33 WHERE published_at IS NOT NULL \
34 AND published_at <= NOW() \
35 ORDER BY published_at DESC LIMIT %s", limit)
36
37 def get_by_tag(self, tag, limit=None):
38 return self._get_posts("SELECT * FROM blog \
39 WHERE published_at IS NOT NULL \
40 AND published_at <= NOW() \
41 AND %s = ANY(tags) \
4bde7f18 42 ORDER BY published_at DESC LIMIT %s", tag, limit)
0a6875dc 43
cdf85ee7 44 def get_by_author(self, author, limit=None):
0a6875dc 45 return self._get_posts("SELECT * FROM blog \
cdf85ee7 46 WHERE (author = %s OR author_uid = %s) \
0a6875dc
MT
47 AND published_at IS NOT NULL \
48 AND published_at <= NOW() \
cdf85ee7
MT
49 ORDER BY published_at DESC LIMIT %s",
50 author.name, author.uid, limit)
0a6875dc 51
7e64f6a3
MT
52 def get_by_year(self, year):
53 return self._get_posts("SELECT * FROM blog \
54 WHERE EXTRACT(year FROM published_at) = %s \
55 AND published_at IS NOT NULL \
56 AND published_at <= NOW() \
57 ORDER BY published_at DESC", year)
58
0a6875dc
MT
59 def search(self, query, limit=None):
60 return self._get_posts("SELECT blog.* FROM blog \
61 LEFT JOIN blog_search_index search_index ON blog.id = search_index.post_id \
62 WHERE search_index.document @@ to_tsquery('english', %s) \
63 ORDER BY ts_rank(search_index.document, to_tsquery('english', %s)) DESC \
64 LIMIT %s", query, query, limit)
65
c70a7c29
MT
66 def _make_slug(self, s):
67 # Remove any non-ASCII characters
68 try:
69 s = unicodedata.normalize("NFKD", s)
70 except TypeError:
71 pass
72
73 # Remove excessive whitespace
74 s = re.sub(r"[^\w]+", " ", s)
75
76 slug = "-".join(s.split()).lower()
77
78 while True:
79 e = self.db.get("SELECT 1 FROM blog WHERE slug = %s", slug)
80 if not e:
81 break
82
83 slug += "-"
84
85 return slug
86
0a6875dc
MT
87 def refresh(self):
88 """
89 Needs to be called after a post has been changed
90 and updates the search index.
91 """
92 self.db.execute("REFRESH MATERIALIZED VIEW blog_search_index")
93
7e64f6a3
MT
94 @property
95 def years(self):
96 res = self.db.query("SELECT DISTINCT EXTRACT(year FROM published_at)::integer AS year \
97 FROM blog WHERE published_at IS NOT NULL AND published_at <= NOW() \
98 ORDER BY year DESC")
99
100 for row in res:
101 yield row.year
102
c70a7c29
MT
103 def update_feeds(self):
104 """
105 Updates all enabled feeds
106 """
107 for feed in self.db.query("SELECT * FROM blog_feeds WHERE enabled IS TRUE"):
108 try:
109 f = feedparser.parse(feed.url)
110 except Exception as e:
111 raise e
112
113 with self.db.transaction():
114 # Update name
115 self.db.execute("UPDATE blog_feeds SET name = %s \
116 WHERE id = %s", f.feed.title, feed.id)
117
118 # Walk through all entries
119 for entry in f.entries:
120 # Skip everything without the "blog.ipfire.org" tag
121 try:
122 tags = list((t.term for t in entry.tags))
123
124 if not "blog.ipfire.org" in tags:
125 continue
126 except AttributeError:
127 continue
128
129 # Get link to the posting site
130 link = entry.links[0].href
131
132 # Check if the entry has already been imported
133 res = self.db.get("SELECT id, (updated_at < %s) AS needs_update \
134 FROM blog WHERE feed_id = %s AND foreign_id = %s",
135 entry.updated, feed.id, entry.id)
136 if res:
137 # If the post needs to be updated, we do so
138 if res.needs_update:
139 self.db.execute("UPDATE blog SET title = %s, author = %s, \
140 published_at = %s, updated_at = %s, html = %s, link = %s, \
141 tags = %s WHERE id = %s", entry.title, entry.author,
142 entry.published, entry.updated, entry.summary, link,
143 feed.tags + tags, res.id)
144
145 # Done here
146 continue
147
148 # Insert the new post
149 self.db.execute("INSERT INTO blog(title, slug, author, \
150 published_at, html, link, tags, updated_at, feed_id, foreign_id) \
151 VALUES(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)",
152 entry.title, self._make_slug(entry.title), entry.author,
153 entry.published, entry.summary, link, feed.tags + tags,
154 entry.updated, feed.id, entry.id)
155
20277bf5
MT
156 # Mark feed as updated
157 self.db.execute("UPDATE blog_feeds SET last_updated_at = CURRENT_TIMESTAMP \
158 WHERE id = %s" % feed.id)
159
c70a7c29
MT
160 # Refresh the search index
161 with self.db.transaction():
162 self.refresh()
163
0a6875dc
MT
164
165class Post(misc.Object):
166 def init(self, id, data=None):
167 self.id = id
168 self.data = data
169
170 @property
171 def title(self):
172 return self.data.title
173
174 @property
175 def slug(self):
176 return self.data.slug
177
178 # XXX needs caching
179 @property
180 def author(self):
181 if self.data.author_uid:
182 return self.backend.accounts.get_by_uid(self.data.author_uid)
183
cdf85ee7
MT
184 return self.data.author
185
0a6875dc
MT
186 @property
187 def created_at(self):
188 return self.data.created_at
189
190 @property
191 def published_at(self):
192 return self.data.published_at
193
7e64f6a3
MT
194 @property
195 def markdown(self):
196 return self.data.markdown
197
0a6875dc
MT
198 @property
199 def html(self):
200 """
201 Returns this post as rendered HTML
202 """
7e64f6a3 203 return self.data.html or textile.textile(self.markdown.decode("utf-8"))
8ebc98d4
MT
204
205 @property
206 def tags(self):
207 return self.data.tags
1e76fec4
MT
208
209 @property
210 def link(self):
211 return self.data.link