]>
Commit | Line | Data |
---|---|---|
0a6875dc MT |
1 | #!/usr/bin/python |
2 | ||
541c952b | 3 | import datetime |
c70a7c29 MT |
4 | import feedparser |
5 | import re | |
7e64f6a3 | 6 | import textile |
c70a7c29 | 7 | import unicodedata |
7e64f6a3 | 8 | |
0a6875dc MT |
9 | from . import misc |
10 | ||
11 | class 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 | |
190 | class 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() |