diff options
Diffstat (limited to 'quoins/controllers.py')
-rw-r--r-- | quoins/controllers.py | 878 |
1 files changed, 878 insertions, 0 deletions
diff --git a/quoins/controllers.py b/quoins/controllers.py new file mode 100644 index 0000000..6b41cc1 --- /dev/null +++ b/quoins/controllers.py | |||
@@ -0,0 +1,878 @@ | |||
1 | # Quoins - A TurboGears blogging system. | ||
2 | # Copyright (C) 2008-2009 James E. Blair <corvus@gnu.org> | ||
3 | # | ||
4 | # This program is free software: you can redistribute it and/or modify | ||
5 | # it under the terms of the GNU General Public License as published by | ||
6 | # the Free Software Foundation, either version 3 of the License, or | ||
7 | # (at your option) any later version. | ||
8 | # | ||
9 | # This program is distributed in the hope that it will be useful, | ||
10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | # GNU General Public License for more details. | ||
13 | # | ||
14 | # You should have received a copy of the GNU General Public License | ||
15 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | ||
16 | |||
17 | from tg import controllers, expose, flash, require, validate, request, redirect, session | ||
18 | from tg import TGController | ||
19 | from pylons.controllers.util import abort | ||
20 | from repoze.what import predicates | ||
21 | from tw.api import WidgetsList | ||
22 | import tw.forms as forms | ||
23 | import tw.forms.fields as fields | ||
24 | import tw.forms.validators as validators | ||
25 | from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed | ||
26 | from model import * | ||
27 | import pylons | ||
28 | import cgi | ||
29 | from genshi.input import HTML | ||
30 | from genshi.filters import HTMLSanitizer | ||
31 | from urlparse import urlsplit | ||
32 | import os.path | ||
33 | from tw.tinymce import TinyMCE | ||
34 | import smtplib | ||
35 | from email.mime.text import MIMEText | ||
36 | from threading import Thread | ||
37 | import logging | ||
38 | log = logging.getLogger('quoins') | ||
39 | |||
40 | import openid.consumer.consumer | ||
41 | import openid.server.server | ||
42 | import openid.extensions.sreg | ||
43 | from openid.store.sqlstore import MySQLStore | ||
44 | import MySQLdb | ||
45 | import sqlalchemy.engine.url | ||
46 | import types | ||
47 | |||
48 | import xmlrpclib, sys, re | ||
49 | from linkback import LinkBackHandler, PingBackURI, TrackBackURI | ||
50 | import base64 | ||
51 | |||
52 | def b64encode(x): | ||
53 | return base64.encodestring(x)[:-1] | ||
54 | |||
55 | def get_oid_connection(config=None): | ||
56 | if config is None: | ||
57 | config = tg.config | ||
58 | backupuri = config.get('sqlalchemy.url') | ||
59 | uri = config.get('openid.store', backupuri) | ||
60 | u = sqlalchemy.engine.url.make_url(uri) | ||
61 | pw = u.password or '' | ||
62 | conn = MySQLdb.connect (host = u.host, | ||
63 | user = u.username, | ||
64 | passwd = pw, | ||
65 | db = u.database) | ||
66 | return conn | ||
67 | |||
68 | def fix_url(url): | ||
69 | parts = urlsplit(url) | ||
70 | if not parts[0]: | ||
71 | url = 'http://'+url | ||
72 | i = url.find('://') | ||
73 | if '/' not in url[i+3:]: | ||
74 | url = url + '/' | ||
75 | return url | ||
76 | |||
77 | def send_email(msg, frm, to): | ||
78 | host = tg.config.get('quoins.mailserver', None) | ||
79 | port = tg.config.get('quoins.mailport', 0) | ||
80 | helo = tg.config.get('quoins.mailhelo', None) | ||
81 | user = tg.config.get('quoins.mailuser', None) | ||
82 | pswd = tg.config.get('quoins.mailpass', None) | ||
83 | if not host: | ||
84 | log.warn('Quoins email notifications are not configured') | ||
85 | return #XXX log not sending mail | ||
86 | |||
87 | s = smtplib.SMTP(host, port, helo) | ||
88 | if user and pswd: | ||
89 | s.login(user, pswd) | ||
90 | s.sendmail(frm, [to], msg.as_string()) | ||
91 | s.close() | ||
92 | log.info('Sent mail to: %s' % to) | ||
93 | |||
94 | class SimpleForm(forms.Form): | ||
95 | template = """ | ||
96 | <form xmlns="http://www.w3.org/1999/xhtml" | ||
97 | xmlns:py="http://genshi.edgewall.org/" | ||
98 | name="${name}" | ||
99 | action="${action}" | ||
100 | method="${method}" | ||
101 | class="simpleform" | ||
102 | py:attrs="attrs" | ||
103 | > | ||
104 | <div py:for="field in hidden_fields" | ||
105 | py:replace="field.display(value_for(field), **args_for(field))" | ||
106 | /> | ||
107 | <div py:for="i, field in enumerate(fields)" | ||
108 | class="field ${i%2 and 'odd' or 'even'}"> | ||
109 | <label class="fieldlabel" for="${field.id}" py:content="field.label_text" /><br /> | ||
110 | <span py:if="field.help_text" class="fieldhelp" py:content="field.help_text" /><br py:if="field.help_text" /> | ||
111 | <span py:if="error_for(field)" class="fielderror" py:content="error_for(field)" /><br py:if="error_for(field)" /> | ||
112 | <span py:replace="field.display(value_for(field), **args_for(field))" /> | ||
113 | |||
114 | </div> | ||
115 | <!-- | ||
116 | <div class="field"> | ||
117 | <span py:content="submit.display(submit_text)" /> | ||
118 | </div> | ||
119 | --> | ||
120 | </form> | ||
121 | """ | ||
122 | |||
123 | class BlogPostForm(SimpleForm): | ||
124 | submit_text = "Post" | ||
125 | |||
126 | class fields(WidgetsList): | ||
127 | post_id = fields.HiddenField() | ||
128 | title = fields.TextField(validator=validators.NotEmpty(), | ||
129 | attrs=dict(size=60)) | ||
130 | tags = fields.TextField(attrs=dict(size=30)) | ||
131 | comments = fields.CheckBox(label='Allow comments') | ||
132 | body = TinyMCE(validator=validators.NotEmpty(), | ||
133 | new_options = dict( | ||
134 | plugins = "media", | ||
135 | convert_urls = False | ||
136 | )) | ||
137 | ## plugins = "pagebreak", | ||
138 | ## pagebreak_separator = "<!-- more -->", | ||
139 | ## )) | ||
140 | file = fields.FileField(label='Upload image') | ||
141 | save = fields.SubmitButton(label=' ', default='Upload or save draft', | ||
142 | validator=validators.FancyValidator(if_missing=''), | ||
143 | named_button=True) | ||
144 | |||
145 | blog_post_form = BlogPostForm() | ||
146 | |||
147 | |||
148 | class OpenIDField(fields.TextField): | ||
149 | template = """ | ||
150 | <span> | ||
151 | <input xmlns:py="http://purl.org/kid/ns#" | ||
152 | type="text" | ||
153 | name="${name}" | ||
154 | class="${css_class}" | ||
155 | id="${id}" | ||
156 | value="${value}" | ||
157 | py:attrs="attrs" | ||
158 | /><img src="${img_url}" /> | ||
159 | </span> | ||
160 | """ | ||
161 | params = ["attrs", "img_url"] | ||
162 | params_doc = {'attrs' : 'Dictionary containing extra (X)HTML attributes for' | ||
163 | ' the input tag', | ||
164 | 'img_url' : 'The URL for the OpenID image',} | ||
165 | attrs = {} | ||
166 | img_url = lambda x: tg.url('/images/openid_small_logo.png') | ||
167 | |||
168 | |||
169 | class BlogCommentForm(SimpleForm): | ||
170 | submit_text = "Comment" | ||
171 | |||
172 | class fields(WidgetsList): | ||
173 | id = fields.HiddenField() | ||
174 | name = fields.TextField() | ||
175 | url = OpenIDField(help_text='Enter your website or your OpenID here.') | ||
176 | body = fields.TextArea(validator=validators.NotEmpty()) | ||
177 | |||
178 | blog_comment_form = BlogCommentForm() | ||
179 | |||
180 | class Feed(TGController): | ||
181 | def get_feed_data(self, **kwargs): | ||
182 | # get the latest five blog entries in reversed order from SQLobject | ||
183 | blog = DBSession.query(Blog).get(1) | ||
184 | entries = [] | ||
185 | if kwargs.has_key('author'): | ||
186 | posts = blog.getPostsByAuthor(kwargs['author'])[:5] | ||
187 | else: | ||
188 | posts = blog.published_posts[:5] | ||
189 | for post in posts: | ||
190 | e = {} | ||
191 | e["title"] = post.title | ||
192 | e["published"] = post.created | ||
193 | e["author"] = post.author.display_name | ||
194 | e["email"] = post.author.email_address | ||
195 | e["link"] = self.blog_controller.absolute_url(post) | ||
196 | e["summary"] = post.short_body | ||
197 | tags = [tag.name for tag in post.tags] | ||
198 | if tags: | ||
199 | e["categories"] = tags | ||
200 | else: | ||
201 | e["categories"] = None | ||
202 | entries.append(e) | ||
203 | if blog.subtitle: | ||
204 | subtitle = blog.subtitle | ||
205 | else: | ||
206 | subtitle = '' | ||
207 | bloginfo = dict( | ||
208 | title = blog.title, | ||
209 | subtitle = subtitle, | ||
210 | link = self.blog_controller.absolute_url(), | ||
211 | description = u'The latest entries from %s' % blog.title, | ||
212 | id = self.blog_controller.absolute_url(), | ||
213 | entries = entries | ||
214 | ) | ||
215 | return bloginfo | ||
216 | |||
217 | @expose(content_type='application/atom+xml') | ||
218 | def atom1_0(self, **kw): | ||
219 | info = self.get_feed_data(**kw) | ||
220 | feed = Atom1Feed( | ||
221 | title=info['title'], | ||
222 | link=info['link'], | ||
223 | description=info['description'], | ||
224 | language=u"en", | ||
225 | ) | ||
226 | for entry in info['entries']: | ||
227 | feed.add_item(title=entry['title'], | ||
228 | link=entry['link'], | ||
229 | description=entry['summary'], | ||
230 | author_name=entry['author'], | ||
231 | author_email=entry['email'], | ||
232 | pubdate=entry['published'], | ||
233 | categories=entry['categories']) | ||
234 | return feed.writeString('utf-8') | ||
235 | |||
236 | @expose(content_type='application/rss+xml') | ||
237 | def rss2_0(self, **kw): | ||
238 | info = self.get_feed_data(**kw) | ||
239 | feed = Rss201rev2Feed( | ||
240 | title=info['title'], | ||
241 | link=info['link'], | ||
242 | description=info['description'], | ||
243 | language=u"en", | ||
244 | ) | ||
245 | for entry in info['entries']: | ||
246 | feed.add_item(title=entry['title'], | ||
247 | link=entry['link'], | ||
248 | description=entry['summary'], | ||
249 | author_name=entry['author'], | ||
250 | author_email=entry['email'], | ||
251 | pubdate=entry['published'], | ||
252 | categories=entry['categories']) | ||
253 | return feed.writeString('utf-8') | ||
254 | |||
255 | |||
256 | |||
257 | class Pingback(TGController): | ||
258 | # The index method is based on code at: | ||
259 | # http://www.dalkescientific.com/writings/diary/archive/2006/10/24/xmlrpc_in_turbogears.html | ||
260 | |||
261 | @expose(content_type='text/xml') | ||
262 | def index(self): | ||
263 | params, method = xmlrpclib.loads(request.body) | ||
264 | log.debug('Pingback method: %s' % method) | ||
265 | try: | ||
266 | if method != "pingback.ping": | ||
267 | raise AssertionError("method does not exist") | ||
268 | |||
269 | # Call the method, convert it into a 1-element tuple | ||
270 | # as expected by dumps | ||
271 | response = self.ping(*params) | ||
272 | response = xmlrpclib.dumps((response,), methodresponse=1) | ||
273 | except xmlrpclib.Fault, fault: | ||
274 | # Can't marshal the result | ||
275 | response = xmlrpclib.dumps(fault) | ||
276 | except: | ||
277 | # Some other error; send back some error info | ||
278 | response = xmlrpclib.dumps( | ||
279 | xmlrpclib.Fault(0, "%s:%s" % (sys.exc_type, sys.exc_value)) | ||
280 | ) | ||
281 | |||
282 | log.info('Pingback response: %s' % response) | ||
283 | return response | ||
284 | |||
285 | post_re = re.compile(r'^.*/post/(\d+)$') | ||
286 | def ping(self, sourceURI, targetURI): | ||
287 | m = self.post_re.match(targetURI) | ||
288 | if not m: | ||
289 | return xmlrpclib.Fault(0x21, 'Unable to parse targetURI.') | ||
290 | id = int(m.group(1)) | ||
291 | post = DBSession.query(Post).get(id) | ||
292 | if not post: | ||
293 | return xmlrpclib.Fault(0x20, 'Post not found.') | ||
294 | elif not post.allow_comments: | ||
295 | return xmlrpclib.Fault(0x31, 'Comments are closed on this post.') | ||
296 | for lb in post.linkbacks: | ||
297 | if lb.url == sourceURI: | ||
298 | return xmlrpclib.Fault(0x30, 'Pingback already registered.') | ||
299 | lb = LinkBack() | ||
300 | DBSession.add(lb) | ||
301 | lb.post = post | ||
302 | lb.url = sourceURI | ||
303 | return 'Linkback recorded.' | ||
304 | |||
305 | def post_paginate(start, posts, size): | ||
306 | start=int(start) | ||
307 | if start < 0: | ||
308 | start = 0 | ||
309 | if start > len(posts): | ||
310 | start = len(posts)-size | ||
311 | out_posts = posts[start:start+size] | ||
312 | next = prev = None | ||
313 | if len(posts)>start+size: | ||
314 | next = start+size | ||
315 | if start: | ||
316 | prev = start-size | ||
317 | if prev < 0: prev = 0 | ||
318 | return dict(posts = out_posts, | ||
319 | prev = prev, | ||
320 | next = next) | ||
321 | |||
322 | class BlogController(TGController): | ||
323 | feed = Feed() | ||
324 | pingback = Pingback() | ||
325 | |||
326 | def url(self, obj=None): | ||
327 | if obj is None: | ||
328 | u = tg.url(self.path) | ||
329 | elif isinstance(obj, basestring): | ||
330 | if obj.startswith('/'): obj = obj[1:] | ||
331 | u = tg.url(os.path.join(self.path, obj)) | ||
332 | elif isinstance(obj, Post): | ||
333 | u = tg.url(os.path.join(self.path, 'post', str(obj.id))) | ||
334 | elif isinstance(obj, Media): | ||
335 | u = tg.url(os.path.join(self.path, 'media', str(obj.post.id), str(obj.name))) | ||
336 | return u | ||
337 | |||
338 | def absolute_url(self, obj=None): | ||
339 | u = self.url(obj) | ||
340 | port = tg.config.get('server.webport') | ||
341 | if port == '80': | ||
342 | port = '' | ||
343 | else: | ||
344 | port = ':'+port | ||
345 | return 'http://%s%s%s'%(tg.config.get('server.webhost'), port, u) | ||
346 | |||
347 | def get_html(self, data): | ||
348 | return HTML(data) | ||
349 | |||
350 | def send_comment_email(self, comment): | ||
351 | post = comment.post | ||
352 | blog = post.blog | ||
353 | fromaddr = tg.config.get('quoins.mailfrom', '<>') | ||
354 | toaddr = post.author.email_address | ||
355 | d = {} | ||
356 | d['blog_title'] = blog.title | ||
357 | d['post_title'] = post.title | ||
358 | d['name'] = comment.name | ||
359 | d['url'] = comment.url | ||
360 | d['comment'] = comment.body | ||
361 | if comment.approved: | ||
362 | d['approval'] = "Approval is not required for this comment." | ||
363 | else: | ||
364 | d['approval'] = "This comment is not yet approved. To approve it, visit:\n\n" | ||
365 | d['approval'] += " %s" % self.absolute_url('/unapproved_comments') | ||
366 | |||
367 | message = """ | ||
368 | A new comment has been posted to the %(blog_title)s post | ||
369 | "%(post_title)s". | ||
370 | |||
371 | %(approval)s | ||
372 | |||
373 | Name: %(name)s | ||
374 | URL: %(url)s | ||
375 | Comment: | ||
376 | |||
377 | %(comment)s | ||
378 | """ % d | ||
379 | |||
380 | msg = MIMEText(message) | ||
381 | msg['From'] = fromaddr | ||
382 | msg['To'] = toaddr | ||
383 | msg['Subject'] = "New comment on %s" % post.title | ||
384 | |||
385 | t = Thread(target=send_email, args=(msg, fromaddr, toaddr)) | ||
386 | t.start() | ||
387 | |||
388 | def __init__(self, *args, **kw): | ||
389 | super(BlogController, self).__init__(*args, **kw) | ||
390 | self.path = kw.pop('path', '/') | ||
391 | self.post_paginate = kw.pop('paginate', 5) | ||
392 | self.feed.blog_controller = self | ||
393 | |||
394 | @expose(template="genshi:quoinstemplates.index") | ||
395 | def index(self, start=0): | ||
396 | pylons.response.headers['X-XRDS-Location']=self.absolute_url('/yadis') | ||
397 | blog = DBSession.query(Blog).get(1) | ||
398 | d = post_paginate(start, blog.published_posts, self.post_paginate) | ||
399 | |||
400 | d.update(dict(quoins = self, | ||
401 | blog = blog, | ||
402 | post = None, | ||
403 | author = None, | ||
404 | )) | ||
405 | return d | ||
406 | |||
407 | @expose(template="genshi:quoinstemplates.index") | ||
408 | def tag(self, tagname, start=0): | ||
409 | blog = DBSession.query(Blog).get(1) | ||
410 | posts = blog.getPostsByTag(tagname) | ||
411 | d = post_paginate(start, posts, self.post_paginate) | ||
412 | d.update(dict(quoins = self, | ||
413 | blog = blog, | ||
414 | post = None, | ||
415 | author = None)) | ||
416 | return d | ||
417 | |||
418 | @expose(template="genshi:quoinstemplates.index") | ||
419 | def archive(self, year='', month='', day='', start=0): | ||
420 | blog = DBSession.query(Blog).get(1) | ||
421 | try: year = int(year) | ||
422 | except: year = None | ||
423 | try: month = int(month) | ||
424 | except: month = None | ||
425 | try: day = int(day) | ||
426 | except: day = None | ||
427 | |||
428 | if not year: | ||
429 | flash('Please supply a year for the archive.') | ||
430 | redirect(self.url('/')) | ||
431 | posts = blog.getPostsByDate(year, month, day) | ||
432 | d = post_paginate(start, posts, self.post_paginate) | ||
433 | d.update(dict(quoins = self, | ||
434 | blog = blog, | ||
435 | post = None, | ||
436 | author = None)) | ||
437 | return d | ||
438 | |||
439 | @expose(template="genshi:quoinstemplates.index") | ||
440 | def author(self, name='', start=0): | ||
441 | blog = DBSession.query(Blog).get(1) | ||
442 | |||
443 | if not name: | ||
444 | flash('Please supply the name of an author.') | ||
445 | redirect(self.url('/')) | ||
446 | posts = blog.getPostsByAuthor(name) | ||
447 | d = post_paginate(start, posts, self.post_paginate) | ||
448 | d.update(dict(quoins = self, | ||
449 | blog = blog, | ||
450 | post = None, | ||
451 | author = name)) | ||
452 | return d | ||
453 | |||
454 | @expose(template="genshi:quoinstemplates.index") | ||
455 | @require(predicates.has_permission('blog-post')) | ||
456 | def unpublished_posts(self, start=0): | ||
457 | blog = DBSession.query(Blog).get(1) | ||
458 | posts = blog.unpublished_posts | ||
459 | d = post_paginate(start, posts, self.post_paginate) | ||
460 | d.update(dict(quoins = self, | ||
461 | blog = blog, | ||
462 | post = None, | ||
463 | author = None)) | ||
464 | return d | ||
465 | |||
466 | @expose(template="genshi:quoinstemplates.post") | ||
467 | def post(self, id): | ||
468 | post = DBSession.query(Post).get(id) | ||
469 | pylons.response.headers['X-Pingback']=self.absolute_url('/pingback/') | ||
470 | return dict(quoins = self, | ||
471 | blog = post.blog, | ||
472 | post = post) | ||
473 | |||
474 | @expose(template="genshi:quoinstemplates.new_comment") | ||
475 | def new_comment(self, id): | ||
476 | post = DBSession.query(Post).get(id) | ||
477 | if not post.allow_comments: | ||
478 | flash('This post does not allow comments.') | ||
479 | redirect(self.url(post)) | ||
480 | return dict(quoins = self, | ||
481 | blog = post.blog, | ||
482 | post = post, | ||
483 | form = blog_comment_form, | ||
484 | action = self.url('save_comment'), | ||
485 | defaults = {'id':id}) | ||
486 | |||
487 | @expose() | ||
488 | def yadis(self): | ||
489 | doc = """<?xml version="1.0" encoding="UTF-8"?> | ||
490 | <xrds:XRDS | ||
491 | xmlns:xrds="xri://$xrds" | ||
492 | xmlns:openid="http://openid.net/xmlns/1.0" | ||
493 | xmlns="xri://$xrd*($v*2.0)"> | ||
494 | <XRD> | ||
495 | <Service priority="0"> | ||
496 | <Type>http://specs.openid.net/auth/2.0/return_to</Type> | ||
497 | <URI>%s</URI> | ||
498 | </Service> | ||
499 | </XRD> | ||
500 | </xrds:XRDS> | ||
501 | """ % self.absolute_url('oid_comment') | ||
502 | return doc | ||
503 | |||
504 | @expose() | ||
505 | @validate(form=blog_comment_form, error_handler=new_comment) | ||
506 | def save_comment(self, id, name='', url='', body=''): | ||
507 | post = DBSession.query(Post).get(id) | ||
508 | if not post.allow_comments: | ||
509 | flash('This post does not allow comments.') | ||
510 | redirect(self.url(post)) | ||
511 | if name and ('%' in name or DBSession.query(TGUser).filter_by(display_name=name).first() or name.lower()=='anonymous' or name.lower().startswith('openid')): | ||
512 | flash('The name %s is not allowed.'%name) | ||
513 | return self.new_comment(id) | ||
514 | if not name: name = 'Anonymous' | ||
515 | if url: | ||
516 | store = MySQLStore(get_oid_connection()) | ||
517 | con = openid.consumer.consumer.Consumer(session, store) | ||
518 | url = fix_url(str(url)) | ||
519 | try: | ||
520 | req = con.begin(url) | ||
521 | req.addExtensionArg('http://openid.net/sreg/1.0', | ||
522 | 'optional', | ||
523 | 'fullname,nickname,email') | ||
524 | |||
525 | oid_url = req.redirectURL(self.absolute_url(), | ||
526 | self.absolute_url('oid_comment')) | ||
527 | session['oid_comment_body']=body | ||
528 | session['oid_comment_name']=name | ||
529 | session['oid_comment_post']=id | ||
530 | session['oid_comment_url']=url | ||
531 | session.save() | ||
532 | |||
533 | redirect(oid_url) | ||
534 | except openid.consumer.consumer.DiscoveryFailure: | ||
535 | # treat as anonymous content without openid | ||
536 | pass | ||
537 | |||
538 | c = Comment() | ||
539 | c.post = post | ||
540 | post.comments.append(c) | ||
541 | c.body = body | ||
542 | if request.identity: | ||
543 | c.author=request.identity['user'] | ||
544 | c.approved = True | ||
545 | flash('Your comment has been posted.') | ||
546 | else: | ||
547 | c.name = name | ||
548 | if url: c.url = url | ||
549 | flash('Your comment has been posted and is awaiting moderation.') | ||
550 | |||
551 | self.send_comment_email(c) | ||
552 | |||
553 | redirect(self.url(post)) | ||
554 | |||
555 | @expose() | ||
556 | def oid_comment(self, **kw): | ||
557 | store = MySQLStore(get_oid_connection()) | ||
558 | con = openid.consumer.consumer.Consumer(session, store) | ||
559 | port = tg.config.get('server.webport') | ||
560 | if port == '80': | ||
561 | port = '' | ||
562 | else: | ||
563 | port = ':'+port | ||
564 | path = 'http://%s%s%s'%(tg.config.get('server.webhost'), port, request.path) | ||
565 | ret = con.complete(kw, path) | ||
566 | |||
567 | session.save() | ||
568 | |||
569 | if ret.status == openid.consumer.consumer.SUCCESS: | ||
570 | sreg = ret.extensionResponse('http://openid.net/sreg/1.0', True) | ||
571 | name = session['oid_comment_name'] | ||
572 | if sreg.has_key('fullname'): | ||
573 | name = sreg.get('fullname') | ||
574 | elif sreg.has_key('nickname'): | ||
575 | name = sreg.get('nickname') | ||
576 | body = session['oid_comment_body'] | ||
577 | id = session['oid_comment_post'] | ||
578 | url = session['oid_comment_url'] | ||
579 | |||
580 | name = unicode(name) | ||
581 | post = DBSession.query(Post).get(id) | ||
582 | if not post.allow_comments: | ||
583 | flash('This post does not allow comments.') | ||
584 | redirect(self.url(post)) | ||
585 | if name and ('%' in name or DBSession.query(TGUser).filter_by(display_name=name).first() or name.lower()=='anonymous'): | ||
586 | name = u'OpenID: %s'%name | ||
587 | |||
588 | c = Comment() | ||
589 | c.post = post | ||
590 | post.comments.append(c) | ||
591 | c.approved = True | ||
592 | c.body = body | ||
593 | c.url = url | ||
594 | c.name = name | ||
595 | flash('Your comment has been posted.') | ||
596 | self.send_comment_email(c) | ||
597 | redirect(self.url(post)) | ||
598 | else: | ||
599 | id = session['oid_comment_post'] | ||
600 | post = DBSession.query(Post).get(id) | ||
601 | flash('OpenID authentication failed') | ||
602 | redirect(self.url('new_comment/%s'%post.id)) | ||
603 | |||
604 | @expose(template="genshi:quoinstemplates.delete_comment") | ||
605 | @require(predicates.has_permission('blog-post')) | ||
606 | def delete_comment(self, id, confirm=None): | ||
607 | comment = DBSession.query(Comment).get(id) | ||
608 | post = comment.post | ||
609 | if confirm: | ||
610 | DBSession.delete(comment) | ||
611 | flash('Comment deleted.') | ||
612 | redirect(self.url(post)) | ||
613 | return dict(quoins = self, | ||
614 | blog = comment.post.blog, | ||
615 | post = comment.post, | ||
616 | comment = comment) | ||
617 | |||
618 | @expose(template="genshi:quoinstemplates.unapproved_comments") | ||
619 | @require(predicates.has_permission('blog-post')) | ||
620 | def unapproved_comments(self): | ||
621 | blog = DBSession.query(Blog).get(1) | ||
622 | return dict(quoins = self, | ||
623 | blog=blog, | ||
624 | post=None, | ||
625 | comments = blog.unapproved_comments) | ||
626 | |||
627 | @expose() | ||
628 | @require(predicates.has_permission('blog-post')) | ||
629 | def approve_comments(self, **kwargs): | ||
630 | for name, value in kwargs.items(): | ||
631 | if not name.startswith('comment_'): continue | ||
632 | if value == 'ignore': continue | ||
633 | c, id = name.split('_') | ||
634 | comment = DBSession.query(Comment).get(int(id)) | ||
635 | if value == 'approve': | ||
636 | comment.approved = True | ||
637 | if value == 'delete': | ||
638 | DBSession.delete(comment) | ||
639 | flash('Your changes have been saved.') | ||
640 | redirect(self.url()) | ||
641 | |||
642 | @expose(content_type=controllers.CUSTOM_CONTENT_TYPE) | ||
643 | def media(self, post_id, name): | ||
644 | # Apparently TG2 is adopting Zope misfeatures and we | ||
645 | # don't get .ext in our name argument. | ||
646 | name = request.environ['PATH_INFO'].split('/')[-1] | ||
647 | post = DBSession.query(Post).get(post_id) | ||
648 | media = None | ||
649 | for m in post.media: | ||
650 | if m.name==name: | ||
651 | media = m | ||
652 | if not media: | ||
653 | abort(404) | ||
654 | |||
655 | pylons.response.headers['Content-Type'] = media.mimetype | ||
656 | return str(media.data) | ||
657 | |||
658 | @expose() | ||
659 | @require(predicates.has_permission('blog-post')) | ||
660 | def delete_media(self, post_id, ids=[]): | ||
661 | if type(ids) != type([]): | ||
662 | ids = [ids] | ||
663 | for id in ids: | ||
664 | media = DBSession.query(Media).get(id) | ||
665 | DBSession.delete(media) | ||
666 | DBSession.flush() | ||
667 | flash('Deleted image') | ||
668 | return self.edit_post(post_id) | ||
669 | |||
670 | @expose(template="genshi:quoinstemplates.new_post") | ||
671 | @require(predicates.has_permission('blog-post')) | ||
672 | def new_post(self, **args): | ||
673 | blog = DBSession.query(Blog).get(1) | ||
674 | return dict(quoins = self, | ||
675 | blog = blog, | ||
676 | post = None, | ||
677 | form = blog_post_form, | ||
678 | defaults = {'comments':blog.allow_comments}) | ||
679 | |||
680 | @expose(template="genshi:quoinstemplates.delete_post") | ||
681 | @require(predicates.has_permission('blog-post')) | ||
682 | def delete_post(self, id, confirm=None): | ||
683 | post = DBSession.query(Post).get(id) | ||
684 | if confirm: | ||
685 | for tag in post.tags[:]: | ||
686 | post.untag(tag.name) | ||
687 | DBSession.delete(post) | ||
688 | flash('Post deleted.') | ||
689 | redirect(self.url('/')) | ||
690 | return dict(quoins = self, | ||
691 | blog = post.blog, | ||
692 | post = post) | ||
693 | |||
694 | @expose(template="genshi:quoinstemplates.new_post") | ||
695 | @require(predicates.has_permission('blog-post')) | ||
696 | def edit_post(self, id): | ||
697 | post = DBSession.query(Post).get(id) | ||
698 | body = post.body | ||
699 | if post.teaser: | ||
700 | body = post.teaser + '\n----\n' + body | ||
701 | tags = ' '.join([t.name for t in post.tags]) | ||
702 | return dict(quoins = self, | ||
703 | blog = post.blog, | ||
704 | form = blog_post_form, | ||
705 | post = post, | ||
706 | defaults = {'comments':post.allow_comments, | ||
707 | 'title':post.title, | ||
708 | 'body':body, | ||
709 | 'tags':tags, | ||
710 | 'post_id':post.id}) | ||
711 | |||
712 | @expose(template="genshi:quoinstemplates.save_post") | ||
713 | @validate(form=blog_post_form, error_handler=new_post) | ||
714 | @require(predicates.has_permission('blog-post')) | ||
715 | def save_post(self, title, body, comments='', tags='', post_id='', file=None, save='', submit=''): | ||
716 | flashes = [] | ||
717 | body = body.encode('utf8') | ||
718 | if not tags: tags = '' | ||
719 | |||
720 | if post_id: | ||
721 | # Editing a post | ||
722 | post_id = int(post_id) | ||
723 | p = DBSession.query(Post).get(post_id) | ||
724 | blog = p.blog | ||
725 | for t in p.tags[:]: | ||
726 | p.untag(t.name) | ||
727 | else: | ||
728 | # Making a new post | ||
729 | blog = DBSession.query(Blog).get(1) | ||
730 | p = Post() | ||
731 | p.author=request.identity['user'] | ||
732 | p.blog=blog | ||
733 | DBSession.add(p) | ||
734 | p.title = title | ||
735 | p.body = body | ||
736 | |||
737 | teaser = '' | ||
738 | newbody = '' | ||
739 | body = body.replace('\r', '') | ||
740 | for line in body.split('\n'): | ||
741 | if line.strip()=='----' and not teaser: | ||
742 | teaser = newbody.strip()+'\n' | ||
743 | newbody = '' | ||
744 | else: | ||
745 | newbody += line+'\n' | ||
746 | if teaser and newbody: p.teaser = teaser | ||
747 | if teaser and not newbody: | ||
748 | newbody = teaser | ||
749 | p.body = newbody.strip()+'\n' | ||
750 | |||
751 | if comments: | ||
752 | p.allow_comments=True | ||
753 | else: | ||
754 | p.allow_comments=False | ||
755 | |||
756 | for tag in tags.split(): | ||
757 | tag = tag.lower().strip() | ||
758 | p.tag(tag) | ||
759 | |||
760 | if file is not None: | ||
761 | data = file.file.read() | ||
762 | if data: | ||
763 | media = Media() | ||
764 | media.post = p | ||
765 | media.name = file.filename | ||
766 | media.mimetype = file.type | ||
767 | media.data = data | ||
768 | DBSession.add(media) | ||
769 | flashes.append('File uploaded.') | ||
770 | |||
771 | if save: | ||
772 | p.published = False | ||
773 | else: | ||
774 | p.published = True | ||
775 | |||
776 | DBSession.flush() | ||
777 | |||
778 | if p.published: | ||
779 | handler = LinkBackHandler() | ||
780 | uris = handler.findURIs(body) | ||
781 | |||
782 | session['linkback_uris']=uris | ||
783 | session.save() | ||
784 | |||
785 | myuris = [] | ||
786 | for (uri, title, lbs) in uris: | ||
787 | best = None | ||
788 | for lb in lbs: | ||
789 | if isinstance(lb, TrackBackURI): | ||
790 | best = lb | ||
791 | lb.type = 'TrackBack' | ||
792 | if isinstance(lb, PingBackURI): | ||
793 | lb.type = 'PingBack' | ||
794 | myuris.append((uri, b64encode(uri), | ||
795 | title, lbs, best)) | ||
796 | flashes.append("Published post.") | ||
797 | else: | ||
798 | flashes.append("Saved draft post.") | ||
799 | |||
800 | |||
801 | flash('\n'.join(flashes)) | ||
802 | |||
803 | if not p.published: | ||
804 | return redirect('./edit_post/%s'%(p.id)) | ||
805 | |||
806 | return dict(quoins=self, | ||
807 | blog=blog, | ||
808 | post=p, | ||
809 | uris=myuris) | ||
810 | |||
811 | @expose() | ||
812 | @require(predicates.has_permission('blog-post')) | ||
813 | def send_linkbacks(self, id, **kw): | ||
814 | trackbacks = kw.pop('trackbacks', '') | ||
815 | blog = DBSession.query(Blog).get(1) | ||
816 | post = DBSession.query(Post).get(id) | ||
817 | |||
818 | flashes = [] | ||
819 | uris = session.pop('linkback_uris', []) | ||
820 | for (uri, title, lbs) in uris: | ||
821 | b64uri = b64encode(uri) | ||
822 | action = kw.pop(b64uri, None) | ||
823 | if not action: continue | ||
824 | type, target = action.split(':',1) | ||
825 | for lb in lbs: | ||
826 | if (isinstance(lb, TrackBackURI) | ||
827 | and type=='TrackBack' | ||
828 | and target==lb.uri): | ||
829 | msg = lb.send(post.title, post.short_body, | ||
830 | self.absolute_url(post), | ||
831 | blog.title) | ||
832 | flashes.append(msg) | ||
833 | if (isinstance(lb, PingBackURI) | ||
834 | and type=='PingBack' | ||
835 | and target==lb.uri): | ||
836 | msg = lb.send(self.absolute_url(post), uri) | ||
837 | flashes.append(msg) | ||
838 | for uri in trackbacks.split('\n'): | ||
839 | uri = uri.strip() | ||
840 | if not uri: continue | ||
841 | lb = TrackBackURI(uri) | ||
842 | msg = lb.send(post.title, post.short_body, | ||
843 | self.absolute_url(post), | ||
844 | blog.title) | ||
845 | flashes.append(msg) | ||
846 | |||
847 | flash('\n'.join(flashes)) | ||
848 | redirect (self.url()) | ||
849 | |||
850 | @expose() | ||
851 | def trackback(self, id, url='', title='', excerpt='', blog_name=''): | ||
852 | message = '' | ||
853 | post = DBSession.query(Post).get(id) | ||
854 | if not post: | ||
855 | message = 'Post not found.' | ||
856 | elif not post.allow_comments: | ||
857 | message = 'Comments are closed on this post.' | ||
858 | for lb in post.linkbacks: | ||
859 | if lb.url == url: | ||
860 | message = 'Trackback already registered.' | ||
861 | if not message: | ||
862 | lb = LinkBack() | ||
863 | DBSession.add(lb) | ||
864 | lb.post = post | ||
865 | lb.url = url | ||
866 | lb.title = title | ||
867 | lb.name = blog_name | ||
868 | lb.body = excerpt | ||
869 | if message: | ||
870 | error = 1 | ||
871 | message = "<message>%s</message>\n" % message | ||
872 | else: | ||
873 | error = 0 | ||
874 | return """<?xml version="1.0" encoding="utf-8"?> | ||
875 | <response> | ||
876 | <error>%s</error> | ||
877 | %s</response> | ||
878 | """ % (error, message) | ||