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