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