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 | ) | ||
