summaryrefslogtreecommitdiff
path: root/quoins/controllers.py
diff options
context:
space:
mode:
Diffstat (limited to 'quoins/controllers.py')
-rw-r--r--quoins/controllers.py878
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
17from tg import controllers, expose, flash, require, validate, request, redirect, session
18from tg import TGController
19from pylons.controllers.util import abort
20from repoze.what import predicates
21from tw.api import WidgetsList
22import tw.forms as forms
23import tw.forms.fields as fields
24import tw.forms.validators as validators
25from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
26from model import *
27import pylons
28import cgi
29from genshi.input import HTML
30from genshi.filters import HTMLSanitizer
31from urlparse import urlsplit
32import os.path
33from tw.tinymce import TinyMCE
34import smtplib
35from email.mime.text import MIMEText
36from threading import Thread
37import logging
38log = logging.getLogger('quoins')
39
40import openid.consumer.consumer
41import openid.server.server
42import openid.extensions.sreg
43from openid.store.sqlstore import MySQLStore
44import MySQLdb
45import sqlalchemy.engine.url
46import types
47
48import xmlrpclib, sys, re
49from linkback import LinkBackHandler, PingBackURI, TrackBackURI
50import base64
51
52def b64encode(x):
53 return base64.encodestring(x)[:-1]
54
55def 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
68def 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
77def 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
94class 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
123class 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
145blog_post_form = BlogPostForm()
146
147
148class 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
169class 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
178blog_comment_form = BlogCommentForm()
179
180class 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
257class 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
305def 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
322class 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 = """
368A new comment has been posted to the %(blog_title)s post
369"%(post_title)s".
370
371%(approval)s
372
373Name: %(name)s
374URL: %(url)s
375Comment:
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)