summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames E. Blair <corvus@gnu.org>2009-07-26 19:26:45 -0700
committerJames E. Blair <corvus@gnu.org>2009-07-26 19:26:45 -0700
commit1a32a7e36c7e1d732c72acb30b8a6a6dc2fc7651 (patch)
treeb77ac5272b01f4bdc5657ff2dd07e96f0a4f892d
Initial checkin (TG 2.0).
-rw-r--r--quoins/__init__.py0
-rw-r--r--quoins/blog_controllers.py8
-rw-r--r--quoins/blog_model.py274
-rw-r--r--quoins/command.py103
-rw-r--r--quoins/controllers.py878
-rw-r--r--quoins/linkback.py184
-rw-r--r--quoins/model/__init__.py62
-rw-r--r--quoins/model/blog.py261
-rw-r--r--quoins/public/images/feed-icon-20x20.pngbin0 -> 1104 bytes
-rw-r--r--quoins/public/images/openid_small_logo.pngbin0 -> 916 bytes
-rw-r--r--quoins/templates/__init__.py0
-rw-r--r--quoins/templates/blog-master.html83
-rw-r--r--quoins/templates/delete_comment.html60
-rw-r--r--quoins/templates/delete_post.html89
-rw-r--r--quoins/templates/index.html71
-rw-r--r--quoins/templates/master.html24
-rw-r--r--quoins/templates/new_comment.html44
-rw-r--r--quoins/templates/new_post.html46
-rw-r--r--quoins/templates/post.html85
-rw-r--r--quoins/templates/save_post.html79
-rw-r--r--quoins/templates/sitetemplate.html18
-rw-r--r--quoins/templates/unapproved_comments.html78
-rw-r--r--setup.py49
23 files changed, 2496 insertions, 0 deletions
diff --git a/quoins/__init__.py b/quoins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/quoins/__init__.py
diff --git a/quoins/blog_controllers.py b/quoins/blog_controllers.py
new file mode 100644
index 0000000..40c27ae
--- /dev/null
+++ b/quoins/blog_controllers.py
@@ -0,0 +1,8 @@
1from tg import TGController, tmpl_context
2from tg import expose, flash, require, url, request, redirect
3
4class BlogController(TGController):
5 @expose(template="genshi:quoinstemplates.test1")
6 def test(self):
7 return dict()
8
diff --git a/quoins/blog_model.py b/quoins/blog_model.py
new file mode 100644
index 0000000..9a84010
--- /dev/null
+++ b/quoins/blog_model.py
@@ -0,0 +1,274 @@
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 sqlalchemy import *
18from sqlalchemy.orm import mapper, relation
19from sqlalchemy import Table, ForeignKey, Column
20from sqlalchemy.types import Integer, Unicode
21#from sqlalchemy.orm import relation, backref
22import tg
23from datetime import datetime
24
25#from bonglonglong.model import DeclarativeBase, metadata, DBSession
26metadata = tg.config['model'].metadata
27DBSession = tg.config['model'].DBSession
28
29TGUser = tg.config['sa_auth']['user_class']
30tguser_table = TGUser.__table__
31
32# Blog schema
33
34blog_table = Table('blog', metadata,
35 Column('id', Integer, primary_key=True),
36 Column('title', Unicode(255)),
37 Column('subtitle', Unicode(255)),
38 Column('allow_comments', Boolean, default=True, nullable=False),
39)
40
41post_table = Table('post', metadata,
42 Column('id', Integer, primary_key=True),
43 Column('blog_id', Integer, ForeignKey('blog.id',
44 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
45 Column('user_id', Integer, ForeignKey(tguser_table.c.user_id,
46 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
47 Column('title', Unicode(255)),
48 Column('teaser', TEXT),
49 Column('body', TEXT),
50 Column('created', DateTime, nullable=False, default=datetime.now),
51 Column('allow_comments', Boolean, nullable=False),
52 Column('published', Boolean, nullable=False, default=False, index=True),
53)
54
55media_table = Table('media', metadata,
56 Column('id', Integer, primary_key=True),
57 Column('post_id', Integer, ForeignKey('post.id',
58 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
59 Column('name', String(255), nullable=False, index=True),
60 Column('mimetype', String(255)),
61 Column('data', BLOB),
62 UniqueConstraint('post_id', 'name'),
63)
64
65tag_table = Table('tag', metadata,
66 Column('id', Integer, primary_key=True),
67 Column('name', Unicode(255)),
68)
69
70post_tag_table = Table('post_tag', metadata,
71 Column('id', Integer, primary_key=True),
72 Column('post_id', Integer, ForeignKey('post.id',
73 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
74 Column('tag_id', Integer, ForeignKey('tag.id',
75 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
76)
77
78comment_table = Table('comment', metadata,
79 Column('id', Integer, primary_key=True),
80 Column('parent_comment_id', Integer, ForeignKey('comment.id',
81 onupdate="CASCADE", ondelete="CASCADE"), index=True),
82 Column('post_id', Integer, ForeignKey('post.id',
83 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
84 Column('user_id', Integer, ForeignKey(tguser_table.c.user_id,
85 onupdate="CASCADE"), index=True),
86 Column('name', Unicode(255)),
87 Column('openid', String(255)),
88 Column('url', String(255)),
89 Column('title', Unicode(255)),
90 Column('body', TEXT),
91 Column('created', DateTime, nullable=False, default=datetime.now),
92 Column('approved', Boolean, nullable=False, default=False, index=True),
93)
94
95linkback_table = Table('linkback', metadata,
96 Column('id', Integer, primary_key=True),
97 Column('post_id', Integer, ForeignKey('post.id',
98 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
99 Column('url', String(255)),
100 Column('title', Unicode(255)),
101 Column('body', Unicode(255)),
102 Column('name', Unicode(255)),
103 Column('created', DateTime, nullable=False, default=datetime.now),
104)
105
106class Blog(object):
107 def get_tags(self):
108 # XXX this assumes that only one blog exists in the schema
109 return DBSession.query(Tag).all()
110 tags = property(get_tags)
111
112 def get_users(self):
113 return [user for user in TGUser.query.all() if 'blog-post' in [p.permission_name for p in user.permissions]]
114 authors = property(get_users)
115
116 def getPostsByTag(self, tagname):
117 posts = Post.query.filter(and_(post_table.c.blog_id==self.id,
118 post_table.c.id==post_tag_table.c.post_id,
119 post_tag_table.c.tag_id==tag_table.c.id,
120 tag_table.c.name==tagname,
121 post_table.c.published==True)).all()
122 return posts
123
124 def getYears(self):
125 years = {}
126 for p in self.published_posts:
127 x = years.get(p.created.year, 0)
128 years[p.created.year] = x+1
129 years = years.items()
130 years.sort(lambda a,b: cmp(a[0],b[0]))
131 years.reverse()
132 return years
133
134 def getPostsByDate(self, year=None, month=None, day=None):
135 posts = []
136 for p in self.published_posts:
137 if year and p.created.year!=year:
138 continue
139 if month and p.created.month!=month:
140 continue
141 if day and p.created.day!=day:
142 continue
143 posts.append(p)
144 return posts
145
146 def getPostsByAuthor(self, name):
147 posts = []
148 for p in self.published_posts:
149 if p.author.user_name==name:
150 posts.append(p)
151 return posts
152
153
154class Post(object):
155 def get_teaser_or_body(self):
156 if self.teaser: return self.teaser
157 return self.body
158 short_body = property(get_teaser_or_body)
159
160 def get_teaser_and_body(self):
161 if self.teaser:
162 return self.teaser + self.body
163 return self.body
164 long_body = property(get_teaser_and_body)
165
166 def tag(self, name):
167 t = Tag.query.filter_by(name=name).first()
168 if not t:
169 t = Tag()
170 t.name = name
171 self.tags.append(t)
172
173 def untag(self, name):
174 t = Tag.query.filter_by(name=name).first()
175 if len(t.posts)<2:
176 session.delete(t)
177 self.tags.remove(t)
178
179 def get_comments_and_linkbacks(self, trackbacks=1, pingbacks=0):
180 objects = self.approved_comments[:]
181 for x in self.linkbacks:
182 if (trackbacks and x.body) or (pingbacks and not x.body):
183 objects.append(x)
184 objects.sort(lambda a,b: cmp(a.created, b.created))
185 return objects
186 comments_and_links = property(get_comments_and_linkbacks)
187
188
189class Media(object):
190 pass
191
192class Tag(object):
193 pass
194
195class BaseComment(object):
196 def get_author_name(self):
197 if hasattr(self, 'author') and self.author:
198 return self.author.display_name
199 if self.name:
200 return self.name
201 return 'Anonymous'
202 author_name = property(get_author_name)
203
204 def get_author_url(self):
205 if hasattr(self, 'author') and self.author:
206 return self.author.url
207 if self.url:
208 return self.url
209 return None
210 author_url = property(get_author_url)
211
212class Comment(BaseComment):
213 pass
214
215class LinkBack(BaseComment):
216 pass
217
218mapper(Blog, blog_table,
219 properties=dict(posts=relation(Post,
220 order_by=desc(post_table.c.created)),
221 published_posts=relation(Post,
222 primaryjoin=and_(post_table.c.blog_id==blog_table.c.id,
223 post_table.c.published==True),
224 order_by=desc(post_table.c.created)),
225 unpublished_posts=relation(Post,
226 primaryjoin=and_(post_table.c.blog_id==blog_table.c.id,
227 post_table.c.published==False),
228 order_by=desc(post_table.c.created)),
229 unapproved_comments=relation(Comment, secondary=post_table,
230 primaryjoin=post_table.c.blog_id==blog_table.c.id,
231 secondaryjoin=and_(comment_table.c.post_id==post_table.c.id,
232 comment_table.c.approved==False))))
233
234mapper(Tag, tag_table,
235 order_by=desc(tag_table.c.name))
236
237mapper(Media, media_table,
238 properties=dict(post=relation(Post)))
239
240mapper(Post, post_table,
241 order_by=desc(post_table.c.created),
242 properties=dict(blog=relation(Blog),
243 author=relation(TGUser),
244 tags=relation(Tag, secondary=post_tag_table, backref='posts'),
245 comments=relation(Comment, cascade="all, delete-orphan"),
246 approved_comments=relation(Comment,
247 primaryjoin=and_(comment_table.c.post_id==post_table.c.id,
248 comment_table.c.approved==True)),
249 unapproved_comments=relation(Comment,
250 primaryjoin=and_(comment_table.c.post_id==post_table.c.id,
251 comment_table.c.approved==False)),
252 media=relation(Media),
253 linkbacks=relation(LinkBack)))
254
255mapper(Comment, comment_table,
256 order_by=comment_table.c.created,
257 properties=dict(post=relation(Post),
258 author=relation(TGUser)))
259
260mapper(LinkBack, linkback_table,
261 properties=dict(post=relation(Post)))
262
263def init_model(engine):
264 """Call me before using any of the tables or classes in the model."""
265
266 maker = sessionmaker(autoflush=True, autocommit=False,
267 extension=ZopeTransactionExtension())
268 DBSession = scoped_session(maker)
269
270 DeclarativeBase = declarative_base()
271 metadata = DeclarativeBase.metadata
272
273 DBSession.configure(bind=engine)
274 return DBSession
diff --git a/quoins/command.py b/quoins/command.py
new file mode 100644
index 0000000..2e6411a
--- /dev/null
+++ b/quoins/command.py
@@ -0,0 +1,103 @@
1import os
2from paste.script import command
3from paste.deploy import appconfig
4
5def get_config(self, config_spec):
6 section = self.options.section_name
7 if section is None:
8 if '#' in config_spec:
9 config_spec, section = config_spec.split('#', 1)
10 else:
11 section = 'main'
12 if not ':' in section:
13 plain_section = section
14 section = 'app:'+section
15 else:
16 plain_section = section.split(':', 1)[0]
17 if not config_spec.startswith('config:'):
18 config_spec = 'config:' + config_spec
19 if plain_section != 'main':
20 config_spec += '#' + plain_section
21 config_file = config_spec[len('config:'):].split('#', 1)[0]
22 config_file = os.path.join(os.getcwd(), config_file)
23 self.logging_file_config(config_file)
24 conf = appconfig(config_spec, relative_to=os.getcwd())
25 return conf
26
27class OpenIDCommand(command.Command):
28 max_args = 1
29 min_args = 1
30
31 usage = "CONFIG_FILE"
32 summary = "Setup Quoins OpenID tables"
33 group_name = "Quoins"
34
35 parser = command.Command.standard_parser(verbose=True)
36 parser.add_option('--name',
37 action='store',
38 dest='section_name',
39 default=None,
40 help='The name of the section to set up (default: app:main)')
41
42 def command(self):
43 config_file = self.args[0]
44 if self.verbose:
45 print "Using config file: %s" % config_file
46
47 conf = get_config(self, config_file)
48
49 from quoins.controllers import get_oid_connection
50 from openid.store.sqlstore import MySQLStore
51 import MySQLdb
52
53 conn = get_oid_connection(conf)
54 store = MySQLStore(conn)
55 try:
56 store.createTables()
57 except MySQLdb.OperationalError, message:
58 errorcode = message[0]
59 if errorcode == 1050:
60 print 'ok'
61 else:
62 raise
63
64class BlogCommand(command.Command):
65 max_args = 1
66 min_args = 1
67
68 usage = "CONFIG_FILE"
69 summary = "Create a new Quoins Blog"
70 group_name = "Quoins"
71
72 parser = command.Command.standard_parser(verbose=True)
73 parser.add_option('--name',
74 action='store',
75 dest='section_name',
76 default=None,
77 help='The name of the section to set up (default: app:main)')
78
79 def command(self):
80 config_file = self.args[0]
81 if self.verbose:
82 print "Using config file: %s" % config_file
83
84 conf = get_config(self, config_file)
85
86 from model import Blog, DBSession, init_model
87 from sqlalchemy import create_engine
88 init_model(create_engine(conf.get('sqlalchemy.url')))
89
90 title = raw_input("Blog title: ")
91 subtitle = raw_input("Blog subtitle: ")
92 comments = raw_input("Allow comments by default? (y/n) ")
93 if comments.strip().lower()=='y':
94 comments = True
95 else:
96 comments = False
97 b = Blog()
98 b.title = title.strip()
99 if subtitle:
100 b.subtitle = subtitle.strip()
101 b.allow_comments = comments
102 DBSession.add(b)
103 DBSession.flush()
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)
diff --git a/quoins/linkback.py b/quoins/linkback.py
new file mode 100644
index 0000000..404789c
--- /dev/null
+++ b/quoins/linkback.py
@@ -0,0 +1,184 @@
1# Quoins - A TurboGears blogging system.
2# Copyright (C) 2008 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 xml.etree.ElementTree import XMLTreeBuilder
18from HTMLParser import HTMLParser, HTMLParseError
19import urllib, urllib2
20import re
21import xmlrpclib
22
23class HTMLLinkParser(HTMLParser):
24 def __init__(self):
25 self.links = []
26 self.curlink_href = None
27 self.curlink_title = None
28 HTMLParser.__init__(self)
29
30 def handle_starttag(self, tag, attrs):
31 if tag != 'a': return
32 for k,v in attrs:
33 if k=='href':
34 self.curlink_href = v
35 self.curlink_title = ''
36
37 def handle_data(self, data):
38 if self.curlink_href and data:
39 self.curlink_title += data
40
41 def handle_endtag(self, tag):
42 if tag != 'a': return
43 if self.curlink_href:
44 title = self.curlink_title
45 if not title:
46 title = self.curlink_href
47 self.links.append((self.curlink_href, title))
48 self.curlink_href = None
49 self.curlink_title = None
50
51class HTMLLinkBackParser(HTMLParser):
52 def __init__(self):
53 self.links = []
54 HTMLParser.__init__(self)
55
56 def handle_starttag(self, tag, attrs):
57 href = rel = None
58 for k,v in attrs:
59 if k=='href':
60 href = v
61 if k=='rel':
62 rel = v
63 if href and rel in ['pingback', 'trackback']:
64 self.links.append((rel, href))
65
66class LinkBackURI(object):
67 def __init__(self, uri):
68 self.uri = uri
69
70class TrackBackURI(LinkBackURI):
71 def send(self, title='', excerpt='', url='', blog_name=''):
72 try:
73 msg = self._send(title, excerpt, url, blog_name)
74 except:
75 return 'Error sending TrackBack to %s' % self.uri
76 if msg:
77 return 'Remote error %s sending TrackBack to %s'%(msg, self.uri)
78 return 'Sent TrackBack to %s' % self.uri
79
80 def _send(self, title, excerpt, url, blog_name):
81 builder = XMLTreeBuilder()
82 data = urllib.urlencode(dict(
83 title=title,
84 excerpt=excerpt,
85 url=url,
86 blog_name=blog_name,
87 ))
88
89 req = urllib2.Request(self.uri, data)
90 response = urllib2.urlopen(req)
91 res = response.read()
92
93 builder.feed(res.strip())
94 tree = builder.close()
95 error = tree.find('error')
96 error = int(error.text)
97 if error:
98 message = tree.find('message')
99 return message.text
100 return None
101
102class PingBackURI(LinkBackURI):
103 def send(self, source_url='', target_url=''):
104 try:
105 msg = self._send(source_url, target_url)
106 except:
107 raise
108 return 'Error sending PingBack to %s' % self.uri
109 if msg:
110 return 'Remote error %s sending PingBack to %s'%(msg, self.uri)
111 return 'Sent PingBack to %s' % self.uri
112
113 def _send(self, source_url, target_url):
114 server = xmlrpclib.ServerProxy(self.uri)
115
116 try:
117 print 'ping', source_url, target_url
118 ret = server.pingback.ping(source_url, target_url)
119 print 'ok', ret
120 return None
121 except xmlrpclib.Error, v:
122 return v
123
124class LinkBackHandler(object):
125
126 def __init__(self, trackbacks=True, pingbacks=True):
127 self.support_trackbacks = trackbacks
128 self.support_pingbacks = pingbacks
129
130 def findURIs(self, text):
131 p = HTMLLinkParser()
132 p.feed(text)
133 p.close()
134 ret = []
135 for uri, title in p.links:
136 try:
137 lbs = self.findLinkBackURIs(uri)
138 ret.append((uri, title, lbs))
139 except ValueError:
140 pass
141 except HTMLParseError:
142 pass
143 except urllib2.HTTPError:
144 pass
145 return ret
146
147 TB_RE = re.compile(r'trackback:ping="([^"]+)"')
148 PB_RE = re.compile(r'<link rel="pingback" href="([^"]+)" ?/?>')
149
150 def findLinkBackURIs(self, uri):
151 found = {}
152 ret = []
153 req = urllib2.Request(uri)
154 response = urllib2.urlopen(req)
155 info = response.info()
156 res = response.read()
157 p = HTMLLinkBackParser()
158 p.feed(res)
159 p.close()
160 if self.support_trackbacks:
161 matches = self.TB_RE.findall(res)
162 for url in matches:
163 if url not in found:
164 found[url]=1
165 ret.append(TrackBackURI(url))
166 for rel, url in p.links:
167 if rel=='trackback' and url not in found:
168 found[url]=1
169 ret.append(TrackBackURI(url))
170 if self.support_pingbacks:
171 pb_header = info.get('X-Pingback', None)
172 if pb_header:
173 ret.append(PingBackURI(pb_header))
174 else:
175 matches = self.PB_RE.findall(res)
176 for url in matches:
177 if url not in found:
178 found[url]=1
179 ret.append(PingBackURI(url))
180 for rel, url in p.links:
181 if rel=='pingback' and url not in found:
182 found[url]=1
183 ret.append(PingBackURI(url))
184 return ret
diff --git a/quoins/model/__init__.py b/quoins/model/__init__.py
new file mode 100644
index 0000000..8f551c8
--- /dev/null
+++ b/quoins/model/__init__.py
@@ -0,0 +1,62 @@
1# -*- coding: utf-8 -*-
2"""The application's model objects"""
3
4from zope.sqlalchemy import ZopeTransactionExtension
5from sqlalchemy.orm import scoped_session, sessionmaker
6#from sqlalchemy import MetaData
7from sqlalchemy.ext.declarative import declarative_base
8
9# Global session manager: DBSession() returns the Thread-local
10# session object appropriate for the current web request.
11maker = sessionmaker(autoflush=True, autocommit=False,
12 extension=ZopeTransactionExtension())
13DBSession = scoped_session(maker)
14
15# Base class for all of our model classes: By default, the data model is
16# defined with SQLAlchemy's declarative extension, but if you need more
17# control, you can switch to the traditional method.
18DeclarativeBase = declarative_base()
19
20# There are two convenient ways for you to spare some typing.
21# You can have a query property on all your model classes by doing this:
22# DeclarativeBase.query = DBSession.query_property()
23# Or you can use a session-aware mapper as it was used in TurboGears 1:
24# DeclarativeBase = declarative_base(mapper=DBSession.mapper)
25
26# Global metadata.
27# The default metadata is the one from the declarative base.
28metadata = DeclarativeBase.metadata
29
30# If you have multiple databases with overlapping table names, you'll need a
31# metadata for each database. Feel free to rename 'metadata2'.
32#metadata2 = MetaData()
33
34#####
35# Generally you will not want to define your table's mappers, and data objects
36# here in __init__ but will want to create modules them in the model directory
37# and import them at the bottom of this file.
38#
39######
40
41def init_model(engine):
42 """Call me before using any of the tables or classes in the model."""
43
44 DBSession.configure(bind=engine)
45 # If you are using reflection to introspect your database and create
46 # table objects for you, your tables must be defined and mapped inside
47 # the init_model function, so that the engine is available if you
48 # use the model outside tg2, you need to make sure this is called before
49 # you use the model.
50
51 #
52 # See the following example:
53
54 #global t_reflected
55
56 #t_reflected = Table("Reflected", metadata,
57 # autoload=True, autoload_with=engine)
58
59 #mapper(Reflected, t_reflected)
60
61# Import your model modules here.
62from quoins.model.blog import *
diff --git a/quoins/model/blog.py b/quoins/model/blog.py
new file mode 100644
index 0000000..608a2f1
--- /dev/null
+++ b/quoins/model/blog.py
@@ -0,0 +1,261 @@
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 sqlalchemy import *
18from sqlalchemy.orm import mapper, relation
19from sqlalchemy import Table, ForeignKey, Column
20from sqlalchemy.types import Integer, Unicode
21#from sqlalchemy.orm import relation, backref
22import tg
23from datetime import datetime
24
25metadata = tg.config['quoins']['metadata']
26DBSession = tg.config['quoins']['session']
27TGUser = tg.config['quoins']['user']
28
29tguser_table = TGUser.__table__
30
31# Blog schema
32
33blog_table = Table('blog', metadata,
34 Column('id', Integer, primary_key=True),
35 Column('title', Unicode(255)),
36 Column('subtitle', Unicode(255)),
37 Column('allow_comments', Boolean, default=True, nullable=False),
38)
39
40post_table = Table('post', metadata,
41 Column('id', Integer, primary_key=True),
42 Column('blog_id', Integer, ForeignKey('blog.id',
43 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
44 Column('user_id', Integer, ForeignKey(tguser_table.c.user_id,
45 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
46 Column('title', Unicode(255)),
47 Column('teaser', TEXT),
48 Column('body', TEXT),
49 Column('created', DateTime, nullable=False, default=datetime.now),
50 Column('allow_comments', Boolean, nullable=False),
51 Column('published', Boolean, nullable=False, default=False, index=True),
52)
53
54media_table = Table('media', metadata,
55 Column('id', Integer, primary_key=True),
56 Column('post_id', Integer, ForeignKey('post.id',
57 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
58 Column('name', String(255), nullable=False, index=True),
59 Column('mimetype', String(255)),
60 Column('data', BLOB),
61 UniqueConstraint('post_id', 'name'),
62)
63
64tag_table = Table('tag', metadata,
65 Column('id', Integer, primary_key=True),
66 Column('name', Unicode(255)),
67)
68
69post_tag_table = Table('post_tag', metadata,
70 Column('id', Integer, primary_key=True),
71 Column('post_id', Integer, ForeignKey('post.id',
72 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
73 Column('tag_id', Integer, ForeignKey('tag.id',
74 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
75)
76
77comment_table = Table('comment', metadata,
78 Column('id', Integer, primary_key=True),
79 Column('parent_comment_id', Integer, ForeignKey('comment.id',
80 onupdate="CASCADE", ondelete="CASCADE"), index=True),
81 Column('post_id', Integer, ForeignKey('post.id',
82 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
83 Column('user_id', Integer, ForeignKey(tguser_table.c.user_id,
84 onupdate="CASCADE"), index=True),
85 Column('name', Unicode(255)),
86 Column('openid', String(255)),
87 Column('url', String(255)),
88 Column('title', Unicode(255)),
89 Column('body', TEXT),
90 Column('created', DateTime, nullable=False, default=datetime.now),
91 Column('approved', Boolean, nullable=False, default=False, index=True),
92)
93
94linkback_table = Table('linkback', metadata,
95 Column('id', Integer, primary_key=True),
96 Column('post_id', Integer, ForeignKey('post.id',
97 onupdate="CASCADE", ondelete="CASCADE"), nullable=False, index=True),
98 Column('url', String(255)),
99 Column('title', Unicode(255)),
100 Column('body', Unicode(255)),
101 Column('name', Unicode(255)),
102 Column('created', DateTime, nullable=False, default=datetime.now),
103)
104
105class Blog(object):
106 def get_tags(self):
107 # XXX this assumes that only one blog exists in the schema
108 return DBSession.query(Tag).all()
109 tags = property(get_tags)
110
111 def get_users(self):
112 return [user for user in DBSession.query(TGUser).all() if 'blog-post' in [p.permission_name for p in user.permissions]]
113 authors = property(get_users)
114
115 def getPostsByTag(self, tagname):
116 posts = DBSession.query(Post).filter(and_(post_table.c.blog_id==self.id,
117 post_table.c.id==post_tag_table.c.post_id,
118 post_tag_table.c.tag_id==tag_table.c.id,
119 tag_table.c.name==tagname,
120 post_table.c.published==True)).all()
121 return posts
122
123 def getYears(self):
124 years = {}
125 for p in self.published_posts:
126 x = years.get(p.created.year, 0)
127 years[p.created.year] = x+1
128 years = years.items()
129 years.sort(lambda a,b: cmp(a[0],b[0]))
130 years.reverse()
131 return years
132
133 def getPostsByDate(self, year=None, month=None, day=None):
134 posts = []
135 for p in self.published_posts:
136 if year and p.created.year!=year:
137 continue
138 if month and p.created.month!=month:
139 continue
140 if day and p.created.day!=day:
141 continue
142 posts.append(p)
143 return posts
144
145 def getPostsByAuthor(self, name):
146 posts = []
147 for p in self.published_posts:
148 if p.author.user_name==name:
149 posts.append(p)
150 return posts
151
152
153class Post(object):
154 def get_teaser_or_body(self):
155 if self.teaser: return self.teaser
156 return self.body
157 short_body = property(get_teaser_or_body)
158
159 def get_teaser_and_body(self):
160 if self.teaser:
161 return self.teaser + self.body
162 return self.body
163 long_body = property(get_teaser_and_body)
164
165 def tag(self, name):
166 t = DBSession.query(Tag).filter_by(name=name).first()
167 if not t:
168 t = Tag()
169 DBSession.add(t)
170 t.name = name
171 self.tags.append(t)
172
173 def untag(self, name):
174 t = DBSession.query(Tag).filter_by(name=name).first()
175 if len(t.posts)<2:
176 DBSession.delete(t)
177 self.tags.remove(t)
178
179 def get_comments_and_linkbacks(self, trackbacks=1, pingbacks=1):
180 objects = self.approved_comments[:]
181 for x in self.linkbacks:
182 if (trackbacks and x.body) or (pingbacks and not x.body):
183 objects.append(x)
184 objects.sort(lambda a,b: cmp(a.created, b.created))
185 return objects
186 comments_and_links = property(get_comments_and_linkbacks)
187
188
189class Media(object):
190 pass
191
192class Tag(object):
193 pass
194
195class BaseComment(object):
196 def get_author_name(self):
197 if hasattr(self, 'author') and self.author:
198 return self.author.display_name
199 if self.name:
200 return self.name
201 return 'Anonymous'
202 author_name = property(get_author_name)
203
204 def get_author_url(self):
205 if hasattr(self, 'author') and self.author:
206 return self.author.url
207 if self.url:
208 return self.url
209 return None
210 author_url = property(get_author_url)
211
212class Comment(BaseComment):
213 pass
214
215class LinkBack(BaseComment):
216 pass
217
218mapper(Blog, blog_table,
219 properties=dict(posts=relation(Post,
220 order_by=desc(post_table.c.created)),
221 published_posts=relation(Post,
222 primaryjoin=and_(post_table.c.blog_id==blog_table.c.id,
223 post_table.c.published==True),
224 order_by=desc(post_table.c.created)),
225 unpublished_posts=relation(Post,
226 primaryjoin=and_(post_table.c.blog_id==blog_table.c.id,
227 post_table.c.published==False),
228 order_by=desc(post_table.c.created)),
229 unapproved_comments=relation(Comment, secondary=post_table,
230 primaryjoin=post_table.c.blog_id==blog_table.c.id,
231 secondaryjoin=and_(comment_table.c.post_id==post_table.c.id,
232 comment_table.c.approved==False))))
233
234mapper(Tag, tag_table,
235 order_by=desc(tag_table.c.name))
236
237mapper(Media, media_table,
238 properties=dict(post=relation(Post)))
239
240mapper(Post, post_table,
241 order_by=desc(post_table.c.created),
242 properties=dict(blog=relation(Blog),
243 author=relation(TGUser),
244 tags=relation(Tag, secondary=post_tag_table, backref='posts'),
245 comments=relation(Comment, cascade="all, delete-orphan"),
246 approved_comments=relation(Comment,
247 primaryjoin=and_(comment_table.c.post_id==post_table.c.id,
248 comment_table.c.approved==True)),
249 unapproved_comments=relation(Comment,
250 primaryjoin=and_(comment_table.c.post_id==post_table.c.id,
251 comment_table.c.approved==False)),
252 media=relation(Media),
253 linkbacks=relation(LinkBack)))
254
255mapper(Comment, comment_table,
256 order_by=comment_table.c.created,
257 properties=dict(post=relation(Post),
258 author=relation(TGUser)))
259
260mapper(LinkBack, linkback_table,
261 properties=dict(post=relation(Post)))
diff --git a/quoins/public/images/feed-icon-20x20.png b/quoins/public/images/feed-icon-20x20.png
new file mode 100644
index 0000000..58cec04
--- /dev/null
+++ b/quoins/public/images/feed-icon-20x20.png
Binary files differ
diff --git a/quoins/public/images/openid_small_logo.png b/quoins/public/images/openid_small_logo.png
new file mode 100644
index 0000000..2829b00
--- /dev/null
+++ b/quoins/public/images/openid_small_logo.png
Binary files differ
diff --git a/quoins/templates/__init__.py b/quoins/templates/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/quoins/templates/__init__.py
diff --git a/quoins/templates/blog-master.html b/quoins/templates/blog-master.html
new file mode 100644
index 0000000..a8ad0cc
--- /dev/null
+++ b/quoins/templates/blog-master.html
@@ -0,0 +1,83 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/"
6 py:strip="">
7
8 <div py:match="div[@id='blog-content']" py:attrs="select('@*')">
9
10
11 <div id="blog-entries-column">
12 <div id="blog-entries">
13 <div py:replace="select('*|text()')" />
14 </div>
15 </div>
16
17 <div id="blog-navigation-column">
18 <div id="blog-navigation">
19 <span py:if="request.identity">
20 <h3>${tg.identity.user.display_name}</h3>
21 <ul>
22 <li><a href="${tg.url('/logout_handler')}">Logout</a></li>
23 <span py:if="'blog-post' in [x.permission_name for x in tg.identity.user.permissions]">
24 <li><a href="${quoins.url('/new_post')}">Write post</a></li>
25 <li><a href="${quoins.url('/unapproved_comments')}">Approve comments</a></li>
26 <li py:if="blog and blog.unpublished_posts">
27 <a href="${quoins.url('/unpublished_posts')}">Draft posts</a>
28 </li>
29 <li py:if="post">
30 <a href="${quoins.url('/edit_post/%s'%post.id)}">Edit post</a>
31 </li>
32 <li py:if="post">
33 <a href="${quoins.url('/delete_post/%s'%post.id)}">Delete post</a>
34 </li>
35 </span>
36 </ul>
37 </span>
38
39 <div id="blog-navigation-tags">
40 <h3>Tags</h3>
41 <ul>
42 <li py:for="tag in blog.tags">
43 <a href="${quoins.url('/tag/%s'%tag.name)}">${tag.name}</a>
44 </li>
45 </ul>
46 </div>
47
48 <div id="blog-navigation-archive">
49 <h3>Archive</h3>
50 <ul>
51 <li py:for="year in blog.getYears()">
52 <a href="${quoins.url('/archive/%s'%year[0])}">
53 ${year[0]}</a> (${year[1]})
54 </li>
55 </ul>
56 </div>
57
58 <div id="blog-navigation-authors">
59 <h3>Authors</h3>
60 <ul>
61 <li py:for="author in blog.authors">
62 <a href="${quoins.url('/author/%s'%author.user_name)}">
63 ${author.display_name}</a>
64 </li>
65 </ul>
66 </div>
67
68 <div id="blog-subscribe">
69 <a href="${quoins.url('/feed/rss2_0')}">
70 <img id="blog-feed-icon"
71 src="${tg.url('/static/images/feed-icon-20x20.png')}" />
72 Subscribe to ${blog.title}
73 </a>
74 </div>
75
76 </div> <!-- blog-navigation -->
77 </div> <!-- blog-navigation-column -->
78
79 <div id="blog-footer"></div>
80
81 </div> <!-- blog-content -->
82
83</html>
diff --git a/quoins/templates/delete_comment.html b/quoins/templates/delete_comment.html
new file mode 100644
index 0000000..c2dd171
--- /dev/null
+++ b/quoins/templates/delete_comment.html
@@ -0,0 +1,60 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12 </head>
13
14
15 <body>
16 <div id="main">
17 <div id="blog-content">
18
19 <div class="blog-post">
20
21 <div class="blog-comments" id="comments">
22
23 <form action="${quoins.url('/delete_comment')}" method="post">
24 <input type="hidden" name="id" value="${comment.id}" />
25 <input type="hidden" name="confirm" value="1" />
26
27 <p> Post:
28 <a href="${quoins.url('/post/%s'%comment.post.id)}">${comment.post.title}</a>
29 </p>
30 <p class="blog-comment-header">
31 <span class="blog-comment-number">
32 <a href="#comment-${comment.id}">
33 [ X ]
34 </a>
35 </span>
36 &nbsp;
37 <span class="blog-comment-user" py:if="comment.url">
38 <a href="${comment.url}">${comment.name}</a>
39 </span>
40 <span class="blog-comment-user" py:if="not comment.url">
41 ${comment.name}
42 </span>
43 <br />
44 <span class="blog-comment-date">
45 ${comment.created.strftime('%B %d, %Y at %I:%M %p')}
46 </span>
47 </p>
48
49 <div class="blog-comment" py:content="comment.body" />
50
51 <input type="submit" value="Delete comment"/>
52 </form>
53 </div> <!-- blog-comments -->
54 </div> <!-- blog-post -->
55
56 </div> <!-- blog-content -->
57 </div> <!-- main -->
58
59 </body>
60</html>
diff --git a/quoins/templates/delete_post.html b/quoins/templates/delete_post.html
new file mode 100644
index 0000000..e237dad
--- /dev/null
+++ b/quoins/templates/delete_post.html
@@ -0,0 +1,89 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12
13 <link rel="pingback" href="${quoins.absolute_url('/pingback/')}" />
14 </head>
15
16
17 <body>
18 <div id="main">
19 <div id="blog-content">
20
21 <div class="blog-post">
22
23 <b> Delete this post? </b>
24 <form action="${quoins.url('/delete_post')}" method="post">
25 <input type="hidden" name="id" value="${post.id}" />
26 <input type="hidden" name="confirm" value="1" />
27
28 <input type="submit" value="Delete post"/>
29 </form>
30
31 <div class="blog-post-head">
32 <h2><a title="Permanent link to ${post.title}"
33 href="${quoins.url('/post/%s'%post.id)}"
34 rel="bookmark">${post.title}</a>
35 </h2>
36 <div class="blog-post-dateline">
37 Posted by ${post.author.display_name}
38 on ${post.created.strftime('%B %d, %Y at %I:%M %p')}
39 </div>
40 </div> <!-- blog-post-head -->
41
42 <div class="blog-post-content">
43 <div py:content="quoins.get_html(post.long_body)" />
44
45 </div> <!-- blog-post_content -->
46
47 <div class="blog-comments">
48
49 <h3>${len(post.comments_and_links)} Comments</h3>
50
51 <ol>
52 <li py:for="i, comment in enumerate(post.comments_and_links)" id="comment-${comment.id}">
53 <p class="blog-comment-header">
54 <span class="blog-comment-number">
55 <a href="#comment-${comment.id}">
56 [${i + 1}]
57 </a>
58 </span>
59 &nbsp;
60 <span class="blog-comment-user" py:if="comment.author_url">
61 <a href="${comment.author_url}" rel="nofollow">${comment.author_name}</a>
62 </span>
63 <span class="blog-comment-user" py:if="not comment.author_url">
64 ${comment.author_name}
65 </span>
66 <br />
67 <span class="blog-comment-date">
68 ${comment.created.strftime('%B %d, %Y at %I:%M %p')}
69 </span>
70
71 <span py:if="'blog-post' in [x.permission_name for x in tg.identity.user.permissions]">
72 &nbsp; &nbsp;
73 <a href="${quoins.url('/delete_comment/%s'%comment.id)}">
74 Delete comment
75 </a>
76 </span>
77 </p>
78
79 <div class="blog-comment" py:content="comment.body" />
80 </li>
81 </ol>
82 </div> <!-- blog-comments -->
83 </div> <!-- blog-post -->
84
85 </div> <!-- blog-content -->
86 </div> <!-- main -->
87
88 </body>
89</html>
diff --git a/quoins/templates/index.html b/quoins/templates/index.html
new file mode 100644
index 0000000..0935c20
--- /dev/null
+++ b/quoins/templates/index.html
@@ -0,0 +1,71 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <link rel="alternate" type="application/rss+xml" title="${blog.title}" href="${quoins.url('/feed/rss2_0')}" />
12 <title>${blog.title}</title>
13 </head>
14
15
16 <body>
17
18 <div id="main" >
19 <div id="blog-content">
20
21 <div class="blog-post" py:for="post in posts">
22 <div class="blog-post-head">
23 <h3><a title="Permanent link to ${post.title}"
24 href="${quoins.url('/post/%s'%post.id)}">
25 ${post.title}</a>
26 </h3>
27 <p class="dateline">
28 Posted by
29 <a href="${quoins.url('/author/%s'%post.author.user_name)}">
30 ${post.author.display_name}
31 </a>
32 on ${post.created.strftime('%B %d, %Y at %I:%M %p')}
33 </p>
34 </div> <!-- blog-post-head -->
35
36 <div class="blog-post-content"
37 py:content="quoins.get_html(post.short_body)" />
38
39 <div class="blog-post-comments">
40 <a href="${quoins.url('/post/%s#comments'%post.id)}">
41 ${len(post.approved_comments)}
42 <span py:if="len(post.approved_comments)==1">comment</span>
43 <span py:if="len(post.approved_comments)!=1">comments</span>
44 </a>
45
46 </div> <!-- blog-post-comments -->
47
48 <div class="blog-post-foot">
49 <div class="tags" py:if="post.tags">
50 <span id="tags-label">Tags:</span>
51 <span id="tag" py:for="tag in post.tags">
52 <a title="Search for posts tagged ${tag.name}"
53 href="${quoins.url('/tag/%s'%tag.name)}">${tag.name}</a> </span>
54 </div>
55 </div>
56
57 </div> <!-- blog-post -->
58
59 <div class="blog-paginate">
60 <span py:if="prev is not None">
61 <a href="?start=${prev}">Previous five posts</a>
62 </span>
63 <span py:if="next is not None">
64 <a href="?start=${next}">Next five posts</a>
65 </span>
66 </div> <!-- blog-paginate -->
67 </div> <!-- blog-content -->
68 </div> <!-- main -->
69
70 </body>
71</html>
diff --git a/quoins/templates/master.html b/quoins/templates/master.html
new file mode 100644
index 0000000..cf82a1f
--- /dev/null
+++ b/quoins/templates/master.html
@@ -0,0 +1,24 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:py="http://genshi.edgewall.org/"
5 xmlns:xi="http://www.w3.org/2001/XInclude"
6 py:strip="">
7<head py:match="head" py:attrs="select('@*')">
8 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
9 <title py:replace="''">Your title goes here</title>
10 <meta py:replace="select('*')"/>
11 <link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/css/style.css')}" />
12</head>
13
14<body py:match="body" py:attrs="select('@*')">
15 <div id="content">
16 <py:with vars="flash=tg.flash_obj.render('flash', use_js=False)">
17 <div py:if="flash" py:content="XML(flash)" />
18 </py:with>
19 <div py:replace="select('*|text()')"/>
20 <!-- End of content -->
21 </div>
22</body>
23</html>
24
diff --git a/quoins/templates/new_comment.html b/quoins/templates/new_comment.html
new file mode 100644
index 0000000..1c1ae9d
--- /dev/null
+++ b/quoins/templates/new_comment.html
@@ -0,0 +1,44 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12 </head>
13
14
15 <body>
16 <div id="main">
17 <div id="blog-content">
18
19 <div class="blog-post">
20 <div class="blog-post-head">
21 <h2><a title="Permanent link to ${post.title}"
22 href="${quoins.url('/post/%s'%post.id)}"
23 rel="bookmark">${post.title}</a>
24 </h2>
25 <p class="dateline">
26 Posted by ${post.author.display_name}
27 on ${post.created.strftime('%B %d, %Y at %I:%M %p')}
28 </p>
29 </div> <!-- blog-post-head -->
30
31 <div class="blog-post-content">
32 <div py:content="quoins.get_html(post.long_body)" />
33
34 </div> <!-- blog-post_content -->
35
36 </div> <!-- blog-post -->
37
38 ${form(defaults, action=action)}
39
40 </div> <!-- blog-content -->
41 </div> <!-- main -->
42
43 </body>
44</html>
diff --git a/quoins/templates/new_post.html b/quoins/templates/new_post.html
new file mode 100644
index 0000000..0f38fd8
--- /dev/null
+++ b/quoins/templates/new_post.html
@@ -0,0 +1,46 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12 </head>
13
14
15 <body>
16 <div id="main">
17 <div id="blog-content">
18 <div py:if="post">
19 <p py:if="post.published">This post is published.</p>
20 <p py:if="not post.published">This post is a draft and is not yet published.</p>
21 </div>
22
23 ${form(defaults, action=quoins.url('/save_post'))}
24
25 <div id="blog-post-images" py:if="post and post.media">
26 <h3> Images </h3>
27 <ul>
28 <form action="${quoins.url('/delete_media')}" method="POST">
29 <input type="hidden" name="post_id" value="${post.id}" />
30 <li py:for="m in post.media">
31 <input type="checkbox" name="ids" value="${m.id}" /> <a href="${quoins.url(m)}">${m.name}</a><br />
32 <img src="${quoins.url(m)}" />
33 </li>
34 <div class="clear">
35 <input type="submit" value="Delete marked images" />
36 </div>
37 </form>
38 </ul>
39 </div>
40
41 </div>
42
43 </div> <!-- main -->
44
45 </body>
46</html>
diff --git a/quoins/templates/post.html b/quoins/templates/post.html
new file mode 100644
index 0000000..72ee748
--- /dev/null
+++ b/quoins/templates/post.html
@@ -0,0 +1,85 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12
13 <link rel="pingback" href="${quoins.absolute_url('/pingback/')}" />
14 </head>
15
16
17 <body>
18 <div id="main">
19 <div id="blog-content">
20
21 <div class="blog-post">
22 <div class="blog-post-head">
23 <h2><a title="Permanent link to ${post.title}"
24 href="${quoins.url('/post/%s'%post.id)}"
25 rel="bookmark">${post.title}</a>
26 </h2>
27 <div class="blog-post-dateline">
28 Posted by
29 <a href="${quoins.url('/author/%s'%post.author.user_name)}">
30 ${post.author.display_name}
31 </a>
32 on ${post.created.strftime('%B %d, %Y at %I:%M %p')}
33 </div>
34 </div> <!-- blog-post-head -->
35
36 <div class="blog-post-content">
37 <div py:content="quoins.get_html(post.long_body)" />
38
39 </div> <!-- blog-post_content -->
40
41 <div class="blog-comments">
42
43 <h3>${len(post.comments_and_links)} Comments</h3>
44
45 <ol>
46 <li py:for="i, comment in enumerate(post.comments_and_links)" id="comment-${comment.id}">
47 <p class="blog-comment-header">
48 <span class="blog-comment-number">
49 <a href="#comment-${comment.id}">
50 [${i + 1}]
51 </a>
52 </span>
53 &nbsp;
54 <span class="blog-comment-user" py:if="comment.author_url">
55 <a href="${comment.author_url}" rel="nofollow">${comment.author_name}</a>
56 </span>
57 <span class="blog-comment-user" py:if="not comment.author_url">
58 ${comment.author_name}
59 </span>
60 <br />
61 <span class="blog-comment-date">
62 ${comment.created.strftime('%B %d, %Y at %I:%M %p')}
63 </span>
64
65 <span py:if="tg.identity and 'blog-post' in [x.permission_name for x in tg.identity.user.permissions]">
66 &nbsp; &nbsp;
67 <a href="${quoins.url('/delete_comment/%s'%comment.id)}">
68 Delete comment
69 </a>
70 </span>
71 </p>
72
73 <div class="blog-comment" py:content="comment.body" />
74 </li>
75 </ol>
76 </div> <!-- blog-comments -->
77 </div> <!-- blog-post -->
78
79 <a py:if="post.allow_comments" href="${quoins.url('/new_comment/%s'%post.id)}">Comment on this post</a>
80
81 </div> <!-- blog-content -->
82 </div> <!-- main -->
83
84 </body>
85</html>
diff --git a/quoins/templates/save_post.html b/quoins/templates/save_post.html
new file mode 100644
index 0000000..d28c16b
--- /dev/null
+++ b/quoins/templates/save_post.html
@@ -0,0 +1,79 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12 </head>
13
14
15 <body>
16 <div id="main">
17 <div id="blog-content">
18
19<p py:if="uris">
20The following links were found in your post. If you would like to send
21TrackBacks or PingBacks to any of these links, make the appropriate
22selections below. If any TrackBack URLs you wish to use are not listed,
23enter one per line in the box below.
24</p>
25<p py:if="not uris">
26No links were found in your post. If you would like to send
27TrackBacks for this post, enter one per line in the box below.
28</p>
29
30<form action="${quoins.url('/send_linkbacks/%s'%post.id)}"
31 method="post"
32 class="blog-linkback-form">
33
34<div py:for="(uri, b64uri, title, lbs, best) in uris">
35<a href="${uri}">${title}</a>
36 <ul>
37 <li py:for="lb in lbs">
38 <label class="fieldlabel">
39 <input py:if="best==lb" type="radio" name="${b64uri}"
40 value="${lb.type}:${lb.uri}" checked="1" />
41 <input py:if="best!=lb" type="radio" name="${b64uri}"
42 value="${lb.type}:${lb.uri}" />
43 ${lb.type}: ${lb.uri}
44 </label>
45 </li>
46 <li py:if="lbs">
47<label class="fieldlabel">
48<input py:if="not best" type="radio" name="${b64uri}"
49 value="None:" checked="1" />
50<input py:if="best" type="radio" name="${b64uri}"
51 value="None:" />
52Don't send a TrackBack or PingBack for this URL
53</label>
54 </li>
55 <li py:if="not lbs">
56 No TrackBack or PingBack addresses found for this URL
57 </li>
58 </ul>
59</div>
60
61<div>
62<label class="fieldlabel">
63Additional TrackBack URLs:
64<textarea name="trackbacks" rows="4" cols="60">
65</textarea>
66</label>
67</div>
68
69<div>
70<input type="submit" name="submit" value="Submit" />
71</div>
72
73</form>
74
75 </div>
76 </div> <!-- main -->
77
78 </body>
79</html>
diff --git a/quoins/templates/sitetemplate.html b/quoins/templates/sitetemplate.html
new file mode 100644
index 0000000..39f4053
--- /dev/null
+++ b/quoins/templates/sitetemplate.html
@@ -0,0 +1,18 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2<html xmlns="http://www.w3.org/1999/xhtml"
3 xmlns:py="http://genshi.edgewall.org/"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 py:strip="">
6<head py:match="head" py:attrs="select('@*')">
7 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
8 <title py:replace="''">Your title goes here</title>
9 <link py:for="css in tg_css" py:replace="ET(css.display())" />
10 <link py:for="js in tg_js_head" py:replace="ET(js.display())" />
11 <meta py:replace="select('*')" />
12</head>
13<body py:match="body" py:attrs="select('@*')">
14 <div py:for="js in tg_js_bodytop" py:replace="ET(js.display())" />
15 <div py:replace="select('*|text()')" />
16 <div py:for="js in tg_js_bodybottom" py:replace="ET(js.display())" />
17</body>
18</html>
diff --git a/quoins/templates/unapproved_comments.html b/quoins/templates/unapproved_comments.html
new file mode 100644
index 0000000..3a17c0e
--- /dev/null
+++ b/quoins/templates/unapproved_comments.html
@@ -0,0 +1,78 @@
1<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
2 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
3<html xmlns="http://www.w3.org/1999/xhtml"
4 xmlns:xi="http://www.w3.org/2001/XInclude"
5 xmlns:py="http://genshi.edgewall.org/">
6 <xi:include href="master.html" />
7 <xi:include href="blog-master.html" />
8
9 <head>
10 <meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
11 <title>${blog.title}</title>
12 </head>
13
14
15 <body>
16 <div id="main">
17 <div id="blog-content">
18
19 <div class="blog-post">
20
21 <div class="blog-comments" id="comments">
22 <form action="${quoins.url('/approve_comments')}" method="post">
23
24 <h3>${len(comments)} unapproved
25 <span py:if="len(comments)==1">comment</span>
26 <span py:if="len(comments)!=1">comments</span>
27 </h3>
28
29 <ol>
30 <li py:for="i, comment in enumerate(comments)" id="comment-${comment.id}">
31 <p> Post:
32 <a href="${quoins.url('/post/%s'%comment.post.id)}">${comment.post.title}</a>
33 <br/>
34 <label>
35 <input type="radio" name="comment_${comment.id}" value="approve"/>
36 Approve
37 </label>
38 <label>
39 <input type="radio" name="comment_${comment.id}" value="delete"/>
40 Delete
41 </label>
42 <label>
43 <input type="radio" name="comment_${comment.id}" value="ignore"/>
44 Ignore
45 </label>
46 </p>
47 <p class="blog-comment-header">
48 <span class="blog-comment-number">
49 <a href="#comment-${comment.id}">
50 [${i + 1}]
51 </a>
52 </span>
53 &nbsp;
54 <span class="blog-comment-user" py:if="comment.url">
55 <a href="${comment.url}">${comment.name}</a>
56 </span>
57 <span class="blog-comment-user" py:if="not comment.url">
58 ${comment.name}
59 </span>
60 <br />
61 <span class="blog-comment-date">
62 ${comment.created.strftime('%B %d, %Y at %I:%M %p')}
63 </span>
64 </p>
65
66 <div class="blog-comment" py:content="comment.body" />
67 </li>
68 </ol>
69 <input type="submit" value="Submit changes"/>
70 </form>
71 </div> <!-- blog-comments -->
72 </div> <!-- blog-post -->
73
74 </div> <!-- blog-content -->
75 </div> <!-- main -->
76
77 </body>
78</html>
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..ca3cb4f
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,49 @@
1# -*- coding: utf-8 -*-
2try:
3 from setuptools import setup, find_packages
4except ImportError:
5 from ez_setup import use_setuptools
6 use_setuptools()
7 from setuptools import setup, find_packages
8
9setup(
10 name='quoins',
11 version='2.0',
12 description='',
13 author='',
14 author_email='',
15 #url='',
16 install_requires=[
17 "TurboGears2 >= 2.0b7",
18 "Catwalk >= 2.0.2",
19 "Babel >=0.9.4",
20 #can be removed iif use_toscawidgets = False
21 "toscawidgets >= 0.9.7.1",
22 "zope.sqlalchemy >= 0.4 ",
23 "repoze.tm2 >= 1.0a4",
24 "tw.tinymce >= 0.8",
25 "python-openid >= 2.2.4",
26 "MySQL-python >= 1.2.3c1",
27 "repoze.what-quickstart >= 1.0",
28 ],
29 setup_requires=["PasteScript >= 1.7"],
30 paster_plugins=['PasteScript', 'Pylons', 'TurboGears2', 'tg.devtools'],
31 packages=find_packages(exclude=['ez_setup']),
32 include_package_data=True,
33 test_suite='nose.collector',
34 tests_require=['WebTest', 'BeautifulSoup'],
35 package_data={'quoins': ['i18n/*/LC_MESSAGES/*.mo',
36 'templates/*/*',
37 'public/*/*']},
38 message_extractors={'quoins': [
39 ('**.py', 'python', None),
40 ('templates/**.mako', 'mako', None),
41 ('templates/**.html', 'genshi', None),
42 ('public/**', 'ignore', None)]},
43
44 entry_points="""
45 [paste.paster_command]
46 openid = quoins.command:OpenIDCommand
47 blog = quoins.command:BlogCommand
48 """,
49)