Initial import

This commit is contained in:
Raoul Snyman 2010-01-15 22:55:30 +02:00
commit 1803fbe824
91 changed files with 4216 additions and 0 deletions

3
MANIFEST.in Normal file
View File

@ -0,0 +1,3 @@
include scribeengine/config/deployment.ini_tmpl
recursive-include scribeengine/public *
recursive-include scribeengine/templates *

19
README.txt Normal file
View File

@ -0,0 +1,19 @@
This file is for you to describe the ScribeEngine application. Typically
you would include information such as the information below:
Installation and Setup
======================
Install ``ScribeEngine`` using easy_install::
easy_install ScribeEngine
Make a config file as follows::
paster make-config ScribeEngine config.ini
Tweak the config file as appropriate and then setup the application::
paster setup-app config.ini
Then you are ready to go.

View File

@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: ScribeEngine
Version: 0.1dev
Summary: UNKNOWN
Home-page: UNKNOWN
Author: UNKNOWN
Author-email: UNKNOWN
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN

View File

@ -0,0 +1,34 @@
MANIFEST.in
README.txt
setup.cfg
setup.py
ScribeEngine.egg-info/PKG-INFO
ScribeEngine.egg-info/SOURCES.txt
ScribeEngine.egg-info/dependency_links.txt
ScribeEngine.egg-info/entry_points.txt
ScribeEngine.egg-info/not-zip-safe
ScribeEngine.egg-info/paster_plugins.txt
ScribeEngine.egg-info/requires.txt
ScribeEngine.egg-info/top_level.txt
scribeengine/__init__.py
scribeengine/websetup.py
scribeengine/config/__init__.py
scribeengine/config/deployment.ini_tmpl
scribeengine/config/environment.py
scribeengine/config/middleware.py
scribeengine/config/routing.py
scribeengine/controllers/__init__.py
scribeengine/controllers/error.py
scribeengine/lib/__init__.py
scribeengine/lib/app_globals.py
scribeengine/lib/base.py
scribeengine/lib/helpers.py
scribeengine/model/__init__.py
scribeengine/model/meta.py
scribeengine/public/bg.png
scribeengine/public/favicon.ico
scribeengine/public/index.html
scribeengine/public/pylons-logo.gif
scribeengine/tests/__init__.py
scribeengine/tests/test_models.py
scribeengine/tests/functional/__init__.py

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,7 @@
[paste.app_factory]
main = scribeengine.config.middleware:make_app
[paste.app_install]
main = pylons.util:PylonsInstaller

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,2 @@
PasteScript
Pylons

View File

@ -0,0 +1,2 @@
Pylons>=0.9.7
SQLAlchemy>=0.5

View File

@ -0,0 +1 @@
scribeengine

97
development.ini Normal file
View File

@ -0,0 +1,97 @@
#
# ScribeEngine - Pylons development environment configuration
#
# The %(here)s variable will be replaced with the parent directory of this file
#
[DEFAULT]
debug = true
# Uncomment and replace with the address which should receive any error reports
#email_to = you@yourdomain.com
smtp_server = localhost
error_email_from = paste@localhost
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 5000
[app:main]
use = egg:ScribeEngine
full_stack = true
static_files = true
cache_dir = %(here)s/data
beaker.session.key = scribeengine
beaker.session.secret = somesecret
# If you'd like to fine-tune the individual locations of the cache data dirs
# for the Cache data, or the Session saves, un-comment the desired settings
# here:
#beaker.cache.data_dir = %(here)s/data/cache
#beaker.session.data_dir = %(here)s/data/sessions
# SQLAlchemy database URL
sqlalchemy.url = sqlite:///%(here)s/scribeengine.sqlite
# Images directory
paths.images = %(here)s/images
# Themes directory
paths.themes = %(here)s/themes
# Security settings
security.salt = xgH,{@1pgtU9,nLd
mail.on = true
mail.manager = immediate
mail.smtp.server = mail.saturnlaboratories.co.za
mail.smtp.username = raoul.snyman+saturnlaboratories.co.za
mail.smtp.password = 0miG0sh89
# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
# Debug mode will enable the interactive debugging tool, allowing ANYONE to
# execute malicious code after an exception is raised.
#set debug = false
# Logging configuration
[loggers]
keys = root, routes, scribeengine, sqlalchemy
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[logger_routes]
level = INFO
handlers =
qualname = routes.middleware
# "level = DEBUG" logs the route matched and routing variables.
[logger_scribeengine]
level = DEBUG
handlers =
qualname = scribeengine
[logger_sqlalchemy]
level = INFO
handlers =
qualname = sqlalchemy.engine
# "level = INFO" logs SQL queries.
# "level = DEBUG" logs SQL queries and results.
# "level = WARN" logs neither. (Recommended for production systems.)
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

19
docs/index.txt Normal file
View File

@ -0,0 +1,19 @@
scribeengine
++++++++++++
This is the main index page of your documentation. It should be written in
`reStructuredText format <http://docutils.sourceforge.net/rst.html>`_.
You can generate your documentation in HTML format by running this command::
setup.py pudge
For this to work you will need to download and install `buildutils`_,
`pudge`_, and `pygments`_. The ``pudge`` command is disabled by
default; to ativate it in your project, run::
setup.py addcommand -p buildutils.pudge_command
.. _buildutils: http://pypi.python.org/pypi/buildutils
.. _pudge: http://pudge.lesscode.org/
.. _pygments: http://pygments.org/

276
ez_setup.py Normal file
View File

@ -0,0 +1,276 @@
#!python
"""Bootstrap setuptools installation
If you want to use setuptools in your package's setup.py, just include this
file in the same directory with it, and add this to the top of your setup.py::
from ez_setup import use_setuptools
use_setuptools()
If you want to require a specific version of setuptools, set a download
mirror, or use an alternate download directory, you can do so by supplying
the appropriate options to ``use_setuptools()``.
This file can also be run as a script to install or upgrade setuptools.
"""
import sys
DEFAULT_VERSION = "0.6c9"
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
md5_data = {
'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f',
'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2',
'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc',
'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167',
'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64',
'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d',
'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20',
'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab',
'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53',
'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2',
'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e',
'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372',
'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902',
'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de',
'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b',
'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03',
'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a',
'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6',
'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a',
}
import sys, os
try: from hashlib import md5
except ImportError: from md5 import md5
def _validate_md5(egg_name, data):
if egg_name in md5_data:
digest = md5(data).hexdigest()
if digest != md5_data[egg_name]:
print >>sys.stderr, (
"md5 validation of %s failed! (Possible download problem?)"
% egg_name
)
sys.exit(2)
return data
def use_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
download_delay=15
):
"""Automatically find/download setuptools and make it available on sys.path
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end with
a '/'). `to_dir` is the directory where setuptools will be downloaded, if
it is not already available. If `download_delay` is specified, it should
be the number of seconds that will be paused before initiating a download,
should one be required. If an older version of setuptools is installed,
this routine will print a message to ``sys.stderr`` and raise SystemExit in
an attempt to abort the calling script.
"""
was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules
def do_download():
egg = download_setuptools(version, download_base, to_dir, download_delay)
sys.path.insert(0, egg)
import setuptools; setuptools.bootstrap_install_from = egg
try:
import pkg_resources
except ImportError:
return do_download()
try:
pkg_resources.require("setuptools>="+version); return
except pkg_resources.VersionConflict, e:
if was_imported:
print >>sys.stderr, (
"The required version of setuptools (>=%s) is not available, and\n"
"can't be installed while this script is running. Please install\n"
" a more recent version first, using 'easy_install -U setuptools'."
"\n\n(Currently using %r)"
) % (version, e.args[0])
sys.exit(2)
else:
del pkg_resources, sys.modules['pkg_resources'] # reload ok
return do_download()
except pkg_resources.DistributionNotFound:
return do_download()
def download_setuptools(
version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
delay = 15
):
"""Download setuptools from a specified location and return its filename
`version` should be a valid setuptools version number that is available
as an egg for download under the `download_base` URL (which should end
with a '/'). `to_dir` is the directory where the egg will be downloaded.
`delay` is the number of seconds to pause before an actual download attempt.
"""
import urllib2, shutil
egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
url = download_base + egg_name
saveto = os.path.join(to_dir, egg_name)
src = dst = None
if not os.path.exists(saveto): # Avoid repeated downloads
try:
from distutils import log
if delay:
log.warn("""
---------------------------------------------------------------------------
This script requires setuptools version %s to run (even to display
help). I will attempt to download it for you (from
%s), but
you may need to enable firewall access for this script first.
I will start the download in %d seconds.
(Note: if this machine does not have network access, please obtain the file
%s
and place it in this directory before rerunning this script.)
---------------------------------------------------------------------------""",
version, download_base, delay, url
); from time import sleep; sleep(delay)
log.warn("Downloading %s", url)
src = urllib2.urlopen(url)
# Read/write all in one block, so we don't create a corrupt file
# if the download is interrupted.
data = _validate_md5(egg_name, src.read())
dst = open(saveto,"wb"); dst.write(data)
finally:
if src: src.close()
if dst: dst.close()
return os.path.realpath(saveto)
def main(argv, version=DEFAULT_VERSION):
"""Install or upgrade setuptools and EasyInstall"""
try:
import setuptools
except ImportError:
egg = None
try:
egg = download_setuptools(version, delay=0)
sys.path.insert(0,egg)
from setuptools.command.easy_install import main
return main(list(argv)+[egg]) # we're done here
finally:
if egg and os.path.exists(egg):
os.unlink(egg)
else:
if setuptools.__version__ == '0.0.1':
print >>sys.stderr, (
"You have an obsolete version of setuptools installed. Please\n"
"remove it from your system entirely before rerunning this script."
)
sys.exit(2)
req = "setuptools>="+version
import pkg_resources
try:
pkg_resources.require(req)
except pkg_resources.VersionConflict:
try:
from setuptools.command.easy_install import main
except ImportError:
from easy_install import main
main(list(argv)+[download_setuptools(delay=0)])
sys.exit(0) # try to force an exit
else:
if argv:
from setuptools.command.easy_install import main
main(argv)
else:
print "Setuptools version",version,"or greater has been installed."
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
def update_md5(filenames):
"""Update our built-in md5 registry"""
import re
for name in filenames:
base = os.path.basename(name)
f = open(name,'rb')
md5_data[base] = md5(f.read()).hexdigest()
f.close()
data = [" %r: %r,\n" % it for it in md5_data.items()]
data.sort()
repl = "".join(data)
import inspect
srcfile = inspect.getsourcefile(sys.modules[__name__])
f = open(srcfile, 'rb'); src = f.read(); f.close()
match = re.search("\nmd5_data = {\n([^}]+)}", src)
if not match:
print >>sys.stderr, "Internal error!"
sys.exit(2)
src = src[:match.start(1)] + repl + src[match.end(1):]
f = open(srcfile,'w')
f.write(src)
f.close()
if __name__=='__main__':
if len(sys.argv)>2 and sys.argv[1]=='--md5update':
update_md5(sys.argv[2:])
else:
main(sys.argv[1:])

0
scribeengine/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,63 @@
#
# ScribeEngine - Pylons configuration
#
# The %(here)s variable will be replaced with the parent directory of this file
#
[DEFAULT]
debug = true
email_to = you@yourdomain.com
smtp_server = localhost
error_email_from = paste@localhost
[server:main]
use = egg:Paste#http
host = 0.0.0.0
port = 5000
[app:main]
use = egg:ScribeEngine
full_stack = true
static_files = true
cache_dir = %(here)s/data
beaker.session.key = scribeengine
beaker.session.secret = ${app_instance_secret}
app_instance_uuid = ${app_instance_uuid}
# If you'd like to fine-tune the individual locations of the cache data dirs
# for the Cache data, or the Session saves, un-comment the desired settings
# here:
#beaker.cache.data_dir = %(here)s/data/cache
#beaker.session.data_dir = %(here)s/data/sessions
# SQLAlchemy database URL
sqlalchemy.url = sqlite:///production.db
# WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*
# Debug mode will enable the interactive debugging tool, allowing ANYONE to
# execute malicious code after an exception is raised.
set debug = false
# Logging configuration
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s

View File

@ -0,0 +1,55 @@
"""Pylons environment configuration"""
import os
from mako.lookup import TemplateLookup
from pylons import config
from pylons.error import handle_mako_error
from sqlalchemy import engine_from_config
from scribeengine.lib import app_globals
from scribeengine.lib import helpers
from scribeengine.config.routing import make_map
from scribeengine.model import init_model, Variable
from scribeengine.model.meta import Session
def load_environment(global_conf, app_conf):
"""Configure the Pylons environment via the ``pylons.config``
object
"""
# Setup the SQLAlchemy database engine
engine = engine_from_config(app_conf, 'sqlalchemy.')
init_model(engine)
# Pull out theme variable
theme = Session.query(Variable).get(u'theme')
if not theme:
theme_name = u'stargazer'
else:
theme_name = theme.value
# Pylons paths
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
theme_dir = os.path.join(app_conf[u'paths.themes'], theme_name)
paths = dict(root=root,
controllers=os.path.join(root, 'controllers'),
static_files=os.path.join(theme_dir, 'public'),
templates=[os.path.join(theme_dir, 'templates'),
os.path.join(root, 'templates')])
# Initialize config with the basic options
config.init_app(global_conf, app_conf, package='scribeengine', paths=paths)
config['routes.map'] = make_map()
config['pylons.app_globals'] = app_globals.Globals()
config['pylons.h'] = helpers
# Create the Mako TemplateLookup, with the default auto-escaping
config['pylons.app_globals'].mako_lookup = TemplateLookup(
directories=paths['templates'],
error_handler=handle_mako_error,
module_directory=os.path.join(app_conf['cache_dir'], 'templates'),
input_encoding='utf-8', default_filters=['escape'],
imports=['from webhelpers.html import escape'])
# CONFIGURATION OPTIONS HERE (note: all config options will override
# any Pylons config options)

View File

@ -0,0 +1,69 @@
"""Pylons middleware initialization"""
from beaker.middleware import CacheMiddleware, SessionMiddleware
from paste.cascade import Cascade
from paste.registry import RegistryManager
from paste.urlparser import StaticURLParser
from paste.deploy.converters import asbool
from pylons import config
from pylons.middleware import ErrorHandler, StatusCodeRedirect
from pylons.wsgiapp import PylonsApp
from routes.middleware import RoutesMiddleware
from scribeengine.config.environment import load_environment
def make_app(global_conf, full_stack=True, static_files=True, **app_conf):
"""Create a Pylons WSGI application and return it
``global_conf``
The inherited configuration for this application. Normally from
the [DEFAULT] section of the Paste ini file.
``full_stack``
Whether this application provides a full WSGI stack (by default,
meaning it handles its own exceptions and errors). Disable
full_stack when this application is "managed" by another WSGI
middleware.
``static_files``
Whether this application serves its own static files; disable
when another web server is responsible for serving them.
``app_conf``
The application's local configuration. Normally specified in
the [app:<name>] section of the Paste ini file (where <name>
defaults to main).
"""
# Configure the Pylons environment
load_environment(global_conf, app_conf)
# The Pylons WSGI app
app = PylonsApp()
# Routing/Session/Cache Middleware
app = RoutesMiddleware(app, config['routes.map'])
app = SessionMiddleware(app, config)
app = CacheMiddleware(app, config)
# CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares)
if asbool(full_stack):
# Handle Python exceptions
app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
# Display error documents for 401, 403, 404 status codes (and
# 500 when debug is disabled)
if asbool(config['debug']):
app = StatusCodeRedirect(app)
else:
app = StatusCodeRedirect(app, [400, 401, 403, 404, 500])
# Establish the Registry for this application
app = RegistryManager(app)
if asbool(static_files):
# Serve static files
static_app = StaticURLParser(config['pylons.paths']['static_files'])
app = Cascade([static_app, app])
return app

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Routes configuration
The more specific and detailed routes should be defined first so they
may take precedent over the more generic routes. For more information
refer to the routes manual at http://routes.groovie.org/docs/
"""
from pylons import config
from routes import Mapper
def make_map():
"""Create, configure and return the routes Mapper"""
map = Mapper(directory=config['pylons.paths']['controllers'],
always_scan=config['debug'])
map.minimization = False
# The ErrorController route (handles 404/500 error pages); it should
# likely stay at the top, ensuring it can always be resolved
map.connect('/error/{action}', controller='error')
map.connect('/error/{action}/{id}', controller='error')
# CUSTOM ROUTES HERE
map.connect('/archive/{year}', controller='blog', action='archive')
map.connect('/archive/{year}/{month}', controller='blog', action='archive')
map.connect('/archive/{year}/{month}/{day}', controller='blog', action='archive')
map.connect('/archive/{year}/{month}/{day}/{url}', controller='blog', action='view')
map.connect('/{controller}/{action}')
map.connect('/{controller}/{action}/{id}')
map.connect('/', controller='blog', action='index')
return map

View File

View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import logging
import string
import random
from scribeengine.lib.base import *
from scribeengine.lib.validation.client import JSString, JSEmail
from scribeengine.lib.validation.server import UnicodeString, Email, FieldsMatch
from scribeengine.lib import utils
from scribeengine.model import User
from scribeengine.model.meta import Session
log = logging.getLogger(__name__)
class AdminController(BaseController):
def index(self):
h.redirect_to('/admin/login')
def register(self):
c.page_title = u'Register'
return render(u'/admin/register.mako')
@jsvalidate(u'register-form')
def register_jsschema(self):
return {
u'email': JSEmail(required=True, message=u'You haven\'t typed in an e-mail address.'),
u'password': JSString(required=True, message=u'You haven\'t typed in a password.'),
u'confirm-password': JSString(required=True, equalTo=u'password', message=u'Your passwords don\'t match.')
}
def register_schema(self):
return {
'email': Email(not_empty=True, messages={'empty': u'You haven\'t typed in an e-mail address.'}),
'password': UnicodeString(not_empty=True, messages={'empty': u'You haven\'t typed in a password.'}),
'confirm': [FieldsMatch('password', 'confirm-passsword', messages={'invalid': u'Your passwords don\'t match.'})]
}
def register_POST(self):
activation_code = u''.join(random.sample(string.letters + string.digits, 40))
user = User(
nick=c.form_values[u'nick'],
email=c.form_values[u'email'],
password=utils.hash_password(c.form_values[u'password']),
activation_key=activation_code
)
Session.add(user)
Session.commit()
h.redirect_to('/')
def login(self):
c.page_title = u'Login'
return render(u'/admin/login.mako')
@jsvalidate(u'login-form')
def login_jsschema(self):
return {
u'email': JSEmail(required=True, message=u'You haven\'t typed in an e-mail address.'),
u'password': JSString(required=True, message=u'You haven\'t typed in a password.')
}
def login_schema(self):
return {
'email': Email(not_empty=True, messages={'empty': u'You haven\'t typed in an e-mail address.'}),
'password': UnicodeString(not_empty=True, messages={'empty': u'You haven\'t typed in a password.'})
}
def login_POST(self):
log.debug('Logging in as "%s" with password "%s"', c.form_values[u'email'], c.form_values[u'password'])
user = Session.query(User).filter_by(email=c.form_values[u'email']).first()
password = utils.hash_password(c.form_values[u'password'])
log.debug(user)
if not user or user.password != password:
log.debug('Username or password are incorrect.')
h.flash.set_message(u'Your username or password are incorrect.', u'error')
h.redirect_to('/login')
elif user and user.password == password:
log.debug('Logged in successfully.')
redirect_url = str(session.get(u'redirect_url', u'/'))
session[u'REMOTE_USER'] = user.id
if u'redirect_url' in session:
del session[u'redirect_url']
session.save()
h.flash.set_message(u'You have logged in successfully.', u'success')
h.redirect_to(redirect_url)
else:
log.debug('"user" is None.')
del session[u'REMOTE_USER']
session.save()
h.flash.set_message(u'There was a problem logging you in.', u'error')
h.redirect_to('/login')
def logout(self):
del session[u'REMOTE_USER']
session.save()
h.redirect_to('/')

View File

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import logging
from datetime import datetime
from scribeengine.lib.base import *
from scribeengine.lib import utils
from scribeengine.model import Post
from scribeengine.model.meta import Session
log = logging.getLogger(__name__)
class BlogController(BaseController):
def index(self):
c.posts = Session.query(Post)\
.filter_by(status=u'published')\
.order_by(Post.created.desc())\
.all()
return render(u'/blog/index.mako')
def archive(self, year=None, month=None, day=None):
if day and month and year:
start_date = datetime(int(year), int(month), int(day), 0, 0, 0, 0)
end_date = datetime(int(year), int(month), int(day), 23, 59, 59, 99999)
c.page_title = u'Archive: %s' % start_date.strftime('%d %B %Y')
elif month and year and not day:
start_date = utils.month_first_day(datetime(int(year), int(month), 1))
end_date = utils.month_last_day(datetime(int(year), int(month), 1))
c.page_title = u'Archive: %s' % start_date.strftime('%B %Y')
elif year and not month:
start_date = datetime(int(year), 1, 1, 0, 0, 0, 0)
end_date = datetime(int(year), 12, 31, 23, 59, 59, 99999)
c.page_title = u'Archive: %s' % start_date.strftime('%Y')
else:
start_date = None
end_date = None
c.posts = Session.query(Post)
if start_date and end_date:
c.posts = c.posts\
.filter(Post.created >= start_date)\
.filter(Post.created <= end_date)
c.posts = c.posts.order_by(Post.created.desc()).all()
return render(u'/blog/archive.mako')
def view(self, url):
c.post = Session.query(Post)\
.filter_by(url=url)\
.filter_by(status=u'published')\
.first()
c.page_title = c.post.title
return render(u'/blog/view.mako')

View File

@ -0,0 +1,46 @@
import cgi
from paste.urlparser import PkgResourcesParser
from pylons import request
from pylons.controllers.util import forward
from pylons.middleware import error_document_template
from webhelpers.html.builder import literal
from scribeengine.lib.base import BaseController
class ErrorController(BaseController):
"""Generates error documents as and when they are required.
The ErrorDocuments middleware forwards to ErrorController when error
related status codes are returned from the application.
This behaviour can be altered by changing the parameters to the
ErrorDocuments middleware in your config/middleware.py file.
"""
def document(self):
"""Render the error document"""
resp = request.environ.get('pylons.original_response')
content = literal(resp.body) or cgi.escape(request.GET.get('message', ''))
page = error_document_template % \
dict(prefix=request.environ.get('SCRIPT_NAME', ''),
code=cgi.escape(request.GET.get('code', str(resp.status_int))),
message=content)
return page
def img(self, id):
"""Serve Pylons' stock images"""
return self._serve_file('/'.join(['media/img', id]))
def style(self, id):
"""Serve Pylons' stock stylesheets"""
return self._serve_file('/'.join(['media/style', id]))
def _serve_file(self, path):
"""Call Paste's FileApp (a WSGI application) to serve the file
at the specified path
"""
request.environ['PATH_INFO'] = '/%s' % path
return forward(PkgResourcesParser('pylons', 'pylons'))

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import logging
from datetime import datetime
from scribeengine.lib.base import *
from scribeengine.lib import utils
from scribeengine.model import Post
from scribeengine.model.meta import Session
log = logging.getLogger(__name__)
class PostController(BaseController):
def index(self):
h.redirect_to('/')
@authenticate(u'Add Posts')
def new(self):
c.page_title = 'New Post'
return render(u'/post/new.mako')
@authenticate(u'Edit My Posts')
def edit(self, id=None):
c.page_title = 'New Post'
return render(u'/post/edit.mako')
def edit_POST(self, id=None):
url = utils.generate_url(c.form_values[u'title'])
if id is None:
post = Post()
post.user = c.current_user
else:
post = Session.query(Post).get(id)
post.modified = datetime.now()
post.title = c.form_values[u'title']
post.body = c.form_values[u'body']
post.status = u'published'
post.url = url
Session.add(post)
Session.commit()
h.redirect_to(str('/archive/%s/%s' % (post.created.strftime('%Y/%m/%d'), post.url)))

View File

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""The application's Globals object"""
class Globals(object):
"""Globals acts as a container for objects available throughout the
life of the application
"""
def __init__(self):
"""One instance of Globals is created during application
initialization and is available during requests via the
'app_globals' variable
"""
from turbomail.adapters import tm_pylons
tm_pylons.start_extension()

240
scribeengine/lib/base.py Normal file
View File

@ -0,0 +1,240 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The base Controller API
Provides the BaseController class for subclassing.
"""
from calendar import Calendar
from datetime import datetime
from decorator import decorator
import logging
from paste.request import construct_url
from webob.exc import HTTPMovedPermanently
from pylons import c, request, session, response, config
from pylons.controllers import WSGIController
from pylons.templating import render_mako
from sqlalchemy.sql.expression import asc, desc
from formencode import Schema, Invalid
from scribeengine.lib import helpers as h
from scribeengine.lib.validation import jsvalidate
from scribeengine.model.meta import Session
from scribeengine.model import Variable, User, Category, Page
log = logging.getLogger(__name__)
class BaseController(WSGIController):
def __before__(self):
#c.theme_name = Session.query(Configuration).get(u'theme').value
if session.get(u'REMOTE_USER'):
c.current_user = Session.query(User).get(session[u'REMOTE_USER'])
c.blog_title = Session.query(Variable).get(u'blog title').value
c.blog_slogan = Session.query(Variable).get(u'blog slogan').value
c.categories = Session.query(Category).order_by(Category.name.asc()).all()
c.pages = Session.query(Page).all()
c.calendar = Calendar(6)
c.today = datetime.today()
if not c.thismonth:
c.thismonth = datetime.now()
self._add_javascript(u'jquery.js')
if c.jsvalidation:
self._add_javascript(u'jquery.validate.js')
self._add_javascript(u'scribeengine.js')
self._add_jsinit(u'init.js')
def __call__(self, environ, start_response):
"""Invoke the Controller"""
# WSGIController.__call__ dispatches to the Controller method
# the request is routed to. This routing information is
# available in environ['pylons.routes_dict']
try:
# If there's a jsschema function, create a template context variable
# that will be used to call in the JavaScript validation.
route = u'/%s/%s' % (environ[u'pylons.routes_dict'][u'controller'],
environ[u'pylons.routes_dict'][u'action'])
action = environ[u'pylons.routes_dict'][u'action']
post = u'%s_POST' % action
jsschema = u'%s_jsschema' % action
schema = u'%s_schema' % action
if getattr(self, jsschema, None) is not None:
c.jsvalidation = route + u'_jsschema'
if environ[u'REQUEST_METHOD'].upper() == u'POST':
# Set up an initial, empty, set of form values.
c.form_values = {}
# Do validation according to schema here, even bfore it hits the <method>_POST
validators = getattr(self, schema, None)
if validators:
schema = self._make_schema(validators)
unvalidated_fields = {}
for key in schema.fields.keys():
if key in request.POST:
value = request.POST.getall(key)
if len(value) == 1:
unvalidated_fields[key] = value[0]
elif len(value) == 0:
unvalidated_fields[key] = None
else:
unvalidated_fields[key] = value
success, results = self._validate(schema, unvalidated_fields)
# Any error messages plus the form values are put into the
# c variable so that it is available to the called method.
c.form_errors = results[u'errors']
c.form_values = results[u'values']
if not success:
# If validation failed, go back to the original method.
return WSGIController.__call__(self, environ, start_response)
# Run through all of the POST variables, and make sure that
# we stick any remaining variables into c.form_values
for key in request.POST.keys():
if key not in c.form_values:
value = request.POST.getall(key)
if len(value) == 1:
c.form_values[key] = value[0]
elif len(value) == 0:
c.form_values[key] = None
else:
c.form_values[key] = value
# So, at this stage, we've done all is necessary, but this is
# a POST, so send us on to a POST method if it exists.
if getattr(self, post, None):
environ[u'pylons.routes_dict'][u'action'] = post
return WSGIController.__call__(self, environ, start_response)
finally:
Session.remove()
def _make_schema(self, validators_function):
"""
_make_schema is a method which is used to automatically convert a
dictionary of FormEncode validators into a schema.
"""
validators = validators_function()
return Schema(**validators)
def _validate(self, schema, params):
"""
Validate the (usually POST) request parameters against a FormEncode
schema.
Returns either:
`(True, values)`
Validation passed, and the values returned are converted (ie, of
the correct type and normalised - leading and/or trailing spaces
removed, etc.)
`(False, errors)`
Validation failed, and the errors returned are a dictionary:
`values`
The values as given in the request
`errors`
The validation errors (as many as can be given)
"""
if callable(schema):
validator = schema()
else:
validator = schema
# Remove 'submit', so that the schema doesn't need to contain it
if u'submit' in params:
del params[u'submit']
try:
values = validator.to_python(params)
except Invalid, e:
if e.error_dict:
# Standard case - each item in the schema that fails is added
# to error_dict with the key being the item name and the value
# a FormEncode error object.
error_dict = e.error_dict.copy()
else:
# On full-form validation failure (for example, sending additional
# data), the form isn't validated, and thus there is no error_dict
error_dict = {}
error_dict[None] = e.unpack_errors()
return False, {u'values': params, u'errors': error_dict}
return True, {u'values': values, u'errors': {}}
def _add_javascript(self, filename, subdir=u'global'):
"""
This method dynamically adds javascript files to the <head> section
of the template.
"""
if not getattr(c, u'scripts', None):
c.scripts = []
c.scripts.append((filename, subdir))
def _add_jsinit(self, filename):
"""
This method dynamically includes a special javascript initialisation
file to the <head> section of the template.
"""
c.jsinit = filename
def _add_stylesheet(self, filename, subdir=None):
"""
This method dynamically adds javascript files to the <head> section
of the template.
"""
if not getattr(c, u'styles', None):
c.styles = []
if subdir is None:
subdir = c.theme_name
c.styles.append(filename, subdir)
def authenticate(permission=None):
"""
A decorator used to check the access level of a member against a permission.
"""
def validate(func, self, *args, **kwargs):
if session.get(u'REMOTE_USER'):
user = Session.query(User).get(session[u'REMOTE_USER'])
if user:
if permission and not user.has_permission(permission):
h.flash.set_message(
u'You don\'t have access to that area.', u'error')
h.redirect_to('/')
return func(self, *args, **kwargs)
else:
h.flash.set_message(
u'You don\'t have access to that area.', u'error')
h.redirect_to('/')
else:
session[u'redirect_url'] = request.environ[u'PATH_INFO']
session.save()
h.flash.set_message(u'You need to be logged in to do that.', u'error')
h.redirect_to('/login')
return decorator(validate)
def render(template):
if request.environ['PATH_INFO'] == '/':
c.page_title = u'%s | %s ' % (c.blog_title, c.blog_slogan)
else:
c.page_title = u'%s | %s ' % (c.page_title, c.blog_title)
return render_mako(template)

View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Helper functions
Consists of functions to typically be used within templates, but also
available to Controllers. This module is available to both as 'h'.
"""
from routes import url_for
from webhelpers.html import escape, HTML, literal, url_escape
from webhelpers.date import distance_of_time_in_words
from pylons.controllers.util import redirect_to
class Flash(object):
def set_message(self, message_text, message_type):
session = self._get_session()
session[u'flash.text'] = message_text
session[u'flash.type'] = message_type
session.save()
def has_message(self):
session = self._get_session()
return u'flash.text' in session
def get_message_text(self):
session = self._get_session()
message_text = session.pop(u'flash.text', None)
if not message_text:
return None
session.save()
return message_text
def get_message_type(self):
session = self._get_session()
message_type = session.pop(u'flash.type', None)
if not message_type:
return None
session.save()
return message_type
def _get_session(self):
from pylons import session
return session
def teaser(text, url):
position = text.find(u'</p>')
if position > 0:
return text[:position]
elif len(text) > 300:
text = text[:297]
position = len(text) - 1
while position > 0 and text[position] not in [u'<', u'>']:
position -= 1
if position != 0 and text[position + 1] == u'/':
position -= 1
while position > 0 and text[position] not in [u'<']:
position -= 1
if position != 0 and text[position] == u'<':
text = text[:position]
return text
flash = Flash()

113
scribeengine/lib/utils.py Normal file
View File

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
import re
import hashlib
import hmac
import string
from random import choice
from datetime import datetime
from pylons import config
from turbomail import Message
from scribeengine.lib.base import render, c
def send_mail(template, mail_to, mail_from, subject, variables={}, attachments=[]):
"""
Sends an e-mail using the template ``template``.
``template``
The template to use.
``mail_to``
One or more addresses to send the e-mail to.
``mail_from``
The address to send e-mail from.
``subject``
The subject of the e-mail.
``variables``
Variables to be used in the template.
``attachments``
If you want to attach files to the e-mail, use this list.
"""
for name, value in variables.iteritems():
setattr(c, name, value)
message = Message(mail_from, mail_to, subject)
message.plain = render(template)
message.send()
def generate_url(title):
"""
Generate a friendly URL from a blog post title.
``title``
The title of the blog post.
"""
return re.sub(r'[^a-zA-Z0-9]+', u'-', title.lower())
def hash_password(password):
"""
Return an HMAC SHA256 hash of a password.
``password``
The password to hash.
"""
return unicode(hmac.new(config[u'security.salt'], password,
hashlib.sha256).hexdigest(), 'utf-8')
def generate_key(length):
"""
Generate a random set of letters and numbers of length ``length``. Usually
used to generate activation keys.
``length``
The length of the key.
"""
return ''.join([choice(string.letters + string.digits) for i in range(length)])
def month_first_day(datetime):
"""
Returns a modified datetime with the day being midnight of the first day of
the month, given a datetime object.
"""
return datetime.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
def month_last_day(datetime):
"""
Returns a modified datetime with the day being the last day of the month,
given a datetime object.
"""
if datetime.month in [1, 3, 5, 7, 8, 10, 12]:
day = 31
elif datetime.month in [4, 6, 9, 11]:
day = 30
else:
if datetime.year % 4 == 0 and datetime.year % 100 != 0 or datetime.year % 400 == 0:
day = 29
else:
day = 28
return datetime.replace(day=day, hour=23, minute=59, second=59, microsecond=99999)

View File

@ -0,0 +1,177 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Validation helper classes and methods.
"""
import logging
from formencode.htmlfill import FillingParser
from decorator import decorator
log = logging.getLogger(__name__)
class ClassFillingParser(FillingParser):
"""
This parser inherits from the base htmlfill.FillingParser, but overrides
the behaviour.
If it encounters the class `error_${name}` on an element and the name given
is in the error dictionary, then the `form-error` class is added to the
element. Otherwise, the class is removed.
If it encounters the class `errormessage_${name}` on an element and the name
given is in the error dictionary, then the content of the element is
replaced with the error from the error dictionary. Otherwise, the element
is removed.
The class `error_` (ie, no name) will give any form errors.
"""
def __init__(self, *args, **kwargs):
"""
Set up the filling parser.
"""
self.formname = kwargs.pop(u'formname', None)
self.in_errormessage = None
return FillingParser.__init__(self, *args, **kwargs)
def handle_starttag(self, tag, attrs, startend=False):
"""
Handle the start of an HTML tag.
"""
# Find any class attribute on the element
class_attr = None
for key, value in attrs:
if key == u'class':
class_attr = value
break
# Only have to handle the case where there is a class.
if class_attr:
classes = class_attr.split(u' ')
for class_name in classes:
# Replace error_${name} with "error" if name is in error dict
if class_name.startswith(u'error_'):
classes = [cls for cls in classes if cls != class_name]
field = class_name[6:]
if field in self.errors:
classes.append(u'form-error')
self.set_attr(attrs, u'class', u' '.join(classes))
self.write_tag(tag, attrs, startend)
self.skip_next = True
return
# Replace the contents of elements with class
# errormessage_${name} with the error from the error
# dictionary (or delete the element entirely if there is no
# such error).
if class_name.startswith(u'errormessage_'):
field = class_name[13:]
self.in_errormessage = tag
self.skip_error = True
# form errors
if not field:
field = None
if field in self.errors:
classes = [cls for cls in classes if cls != class_name]
classes.append(u'form-error')
self.set_attr(attrs, u'class', u' '.join(classes))
self.write_tag(tag, attrs, startend)
self.write_text(htmlliteral(unicode(self.errors[field])).text)
self.write_text(u'</%s>' % tag)
self.skip_next = True
return
return FillingParser.handle_starttag(self, tag, attrs, startend=False)
def handle_endtag(self, tag):
"""
Handle the ending HTML tag.
"""
FillingParser.handle_endtag(self, tag)
# We're handling skipping of contents of an element with
# errormessage_* on it.
#
# After we encounter the end tag, we can stop ignoring elements.
if self.in_errormessage == tag:
self.skip_error = False
self.in_errormessage = None
self.skip_next = True
@classmethod
def html_error_fill(cls, form, errors_and_values):
"""
Create the custom ClassFillingParser, and pass through the values,
errors, and formname from the errors dictionary.
Converts the incoming form from UTF-8-encoded byte strings to Unicode
strings.
"""
p = cls(defaults=errors_and_values[u'form_values'],
errors=errors_and_values[u'form_errors'],
auto_error_formatter=False)
p.feed(form.decode(u'utf-8'))
p.close()
return p.text().encode(u'utf-8')
def jsvalidate(form_id):
"""
This decorator is used to generate JavaScript for client-side validate.
``form_id``
The HTML ``id`` of the form to be validated.
"""
def entangle(func, self, *args, **kwargs):
default_jscript = u"""/* jQuery Validation */
$ProjectHQ.Events.bind_load(function () {
$("#%s").validate({
errorClass: "form-error",
errorContainer: "#form-errors",
errorLabelContainer: "#form-errors > ul",
wrapper: "li",
rules: {
%s
},
messages: {
%s
}
});
});"""
validators = []
messages = []
jsschema = func(self, *args, **kwargs)
for name, validator in jsschema.iteritems():
validators.append(u'%s: {%s}' % (name, validator.to_javascript()))
if validator.get_message() is not None:
validator_message = validator.get_message()
if isinstance(validator_message, basestring):
messages.append(u'%s: "%s"' % (name, validator.get_message()))
else:
validator_messages = []
for key, value in validator_message.iteritems():
validator_messages.append(u'%s: "%s"' % (key, value))
messages.append(u'%s: {\n %s\n }' %
(name, ',\n '.join(validator_messages)))
jscript = default_jscript % (form_id, u',\n '.join(validators),
u',\n '.join(messages))
return jscript
return decorator(entangle)

View File

@ -0,0 +1,230 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Client-side validators.
"""
import logging
log = logging.getLogger(__name__)
class JSValidator(object):
"""
This is a class used to create a validation rule in javascript.
"""
def __init__(self, *args, **kwargs):
"""
The constructor is used to create the validator.
field
The name of the field.
required
Whether or not this field is required.
type
The type of field. Can be "string", "number", "email", "integer"
"""
if u'type' not in kwargs:
raise KeyError(u'"type" is a required argument.')
self.validators = {}
for key, arg in kwargs.iteritems():
self.validators[key] = arg
def to_javascript(self):
js_validators = []
for key, value in self.validators.iteritems():
if key == u'message':
continue
elif key == u'type':
if value in ['email', 'number', 'url']:
key = value
value = u'true'
else:
continue
#elif key == u'checked':
# key = 'required'
# if value:
# value = u'checked'
# else:
# value = u'unchecked'
elif key == u'condition':
conditions = value.split(u',')
values = []
for condition in conditions:
subconditions = condition.split(u';')
subvalues = []
for subcondition in subconditions:
if subcondition.find(u'==') >= 0:
parts = subcondition.split(u'==')
subvalues.append(u'$("%s").val() == %s' % (parts[0], parts[1]))
elif subcondition.find(u'>=') >= 0:
parts = subcondition.split(u'>=')
subvalues.append(u'$("%s").val() >= %s' % (parts[0], parts[1]))
elif subcondition.find(u'<=') >= 0:
parts = subcondition.split(u'<=')
subvalues.append(u'$("%s").val() <= %s' % (parts[0], parts[1]))
elif subcondition.find(u'>') >= 0:
parts = subcondition.split(u'>')
subvalues.append(u'$("%s").val() > %s' % (parts[0], parts[1]))
elif subcondition.find(u'<') >= 0:
parts = subcondition.split(u'<')
subvalues.append(u'$("%s").val() < %s' % (parts[0], parts[1]))
elif subcondition.find(u'!=') >= 0:
parts = subcondition.split(u'!=')
subvalues.append(u'$("%s").val() != %s' % (parts[0], parts[1]))
#elif subcondition.find(u'@') >= 0:
# parts = subcondition.split(u':')
# subvalues.append(u'$("%s").attr(%s)' % (parts[0], parts[1]))
elif subcondition.find(u':') >= 0:
parts = subcondition.split(u':')
subvalues.append(u'$("%s").is(":%s")' % (parts[0], parts[1]))
else:
subvalues.append(u'$("%s")' % subcondition)
values.append(u' && '.join(subvalues))
value = u'function () { return (%s); }' % u') || ('.join(values)
key = u'required'
elif isinstance(value, bool):
if value:
value = u'true'
else:
value = u'false'
elif isinstance(value, basestring):
if isinstance(value, str):
value = unicode(value, u'utf-8')
value = u'"%s"' % value
else:
value = unicode(value)
js_validators.append(u'%s: %s' % (key, value))
return u', '.join(js_validators)
def get_message(self):
"""
If a message is set for this validator, return it, else return None.
"""
if 'message' in self.validators:
return self.validators['message']
else:
return None
class JSNumber(JSValidator):
"""
This is a specialised version of JSValidator for numbers.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'number'
if 'condition' not in kwargs:
kwargs['required'] = True
JSValidator.__init__(self, *args, **kwargs)
class JSDigits(JSValidator):
"""
This is a specialised version of JSValidator for digits.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'digits'
JSValidator.__init__(self, *args, **kwargs)
class JSString(JSValidator):
"""
This is a specialised version of JSValidator for strings.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'string'
JSValidator.__init__(self, *args, **kwargs)
class JSName(JSValidator):
"""
This is a specialised version of JSValidator for names.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'string'
kwargs['realname'] = True
JSValidator.__init__(self, *args, **kwargs)
class JSAlphanumeric(JSValidator):
"""
This is a specialised version of JSValidator for alphanumeric strings.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'string'
kwargs['alphanumeric'] = True
JSValidator.__init__(self, *args, **kwargs)
class JSEmail(JSValidator):
"""
This is a specialised version of JSValidator for strings.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'email'
JSValidator.__init__(self, *args, **kwargs)
class JSUrl(JSValidator):
"""
This is a specialised version of JSValidator for strings.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'url'
JSValidator.__init__(self, *args, **kwargs)
class JSDate(JSValidator):
"""
This is a specialised version of JSValidator for dates.
"""
def __init__(self, *args, **kwargs):
kwargs['type'] = u'date'
JSValidator.__init__(self, *args, **kwargs)
class JSDropdown(JSValidator):
"""
This is a copy of the DropdownValidator for Formencode.
"""
def __init__(self, *args, **kwargs):
if 'invalid_option' not in kwargs:
invalid_option = 0
else:
invalid_option = kwargs['invalid_option']
del kwargs['invalid_option']
if 'condition' not in kwargs:
kwargs['required'] = True
kwargs['notvalue'] = invalid_option
kwargs['type'] = u'string'
JSValidator.__init__(self, *args, **kwargs)
class JSCheckbox(JSValidator):
"""
A validator for maching sure checkboxes are checked.
"""
def __init__(self, *args, **kwargs):
if 'checked' not in kwargs:
kwargs['checked'] = True
kwargs['type'] = u'string'
JSValidator.__init__(self, *args, **kwargs)

View File

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Server-side validators.
"""
import logging
import re
from formencode.api import FancyValidator, Invalid
from formencode.validators import UnicodeString, Int, Email, FieldsMatch
log = logging.getLogger(__name__)
class Password(FancyValidator):
"""
This validator checks for a decently secure password. The password has to
contain a minimum of 6 characters, at least 1 number.
"""
regex = re.compile(r'^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[-.!@#%&]).{6,}$')
messages = {
u'insecure': u'Your password must be longer than 6 characters and '
u'must have at least 1 capital letter, 1 number and one '
u'of the following characters: - . ~ @ # %% &'
}
def _to_python(self, value, state):
# _to_python gets run before validate_python. Here we
# strip whitespace off the password, because leading and
# trailing whitespace in a password is too elite.
return value.strip()
def validate_python(self, value, state):
if len(value) < self.min:
raise Invalid(self.message(u'insecure', state), value, state)
if not self.regex.match(value):
raise Invalid(self.message(u'insecure', state), value, state)

View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
The application's model objects
"""
from sqlalchemy.orm import mapper, relation
from scribeengine.model import meta
from scribeengine.model.tables import categories_table, comments_table, \
pages_table, permissions_table, posts_table, roles_table, tags_table, \
users_table, variables_table, categories_posts_table, \
permissions_roles_table, posts_tags_table, roles_users_table
from scribeengine.model.classes import Category, Comment, Page, Permission, \
Post, Role, Tag, User, Variable
def init_model(engine):
"""Call me before using any of the tables or classes in the model"""
meta.Session.configure(bind=engine)
meta.engine = engine
mapper(Category, categories_table)
mapper(Comment, comments_table)
mapper(Page, pages_table)
mapper(Permission, permissions_table)
mapper(Post, posts_table,
properties={
u'categories': relation(Category, backref='posts', secondary=categories_posts_table),
u'comments': relation(Comment, backref=u'post'),
u'tags': relation(Tag, backref=u'posts', secondary=posts_tags_table)
}
)
mapper(Role, roles_table,
properties={
u'permissions': relation(Permission, backref=u'roles', secondary=permissions_roles_table)
}
)
mapper(Tag, tags_table)
mapper(User, users_table,
properties={
u'comments': relation(Comment, backref=u'user'),
#u'pages': relation(Page, backref=u'user'),
u'posts': relation(Post, backref=u'user'),
u'roles': relation(Role, backref=u'users', secondary=roles_users_table)
}
)
mapper(Variable, variables_table)

View File

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This module contains the Class definitions.
"""
class BaseModel(object):
"""
A base model class which all the other classes inherit from. This provides
all model classes with a set of utility methods.
"""
def __init__(self, **kwargs):
"""
This constructor will set all the classes properties based on the
keyword arguments supplied.
"""
for keyword, argument in kwargs.iteritems():
setattr(self, keyword, argument)
def __repr__(self):
if hasattr(self, 'id'):
return '<%s id=%s>' % (self.__name__, self.id)
class Category(BaseModel):
"""
This is a category for blog posts.
"""
pass
class Comment(BaseModel):
"""
All blog posts have comments. This is a single comment.
"""
pass
class Page(BaseModel):
"""
A page on the blog. This is separate from a blog entry, for things like
about pages.
"""
pass
class Permission(BaseModel):
"""
A single permission.
"""
pass
class Post(BaseModel):
"""
The most import part of all of this, the blog post.
"""
pass
class Role(BaseModel):
"""
A role defines a set of permissions.
"""
pass
class Tag(BaseModel):
"""
A tag, an unstructured category, for blog posts.
"""
pass
class User(BaseModel):
"""
The user.
"""
def has_permission(self, permission):
if isinstance(permission, basestring):
for role in self.roles:
for perm in role.permissions:
if perm.name == permission:
return True
return False
elif isinstance(permission, Permission):
for role in self.roles:
for perm in role.permissions:
if perm == permission:
return True
return False
else:
return False
class Variable(BaseModel):
"""
System variables.
"""
pass

View File

@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
SQLAlchemy Metadata and Session object
"""
from sqlalchemy import MetaData
from sqlalchemy.orm import scoped_session, sessionmaker
__all__ = ['Session', 'engine', 'metadata']
# SQLAlchemy database engine. Updated by model.init_model()
engine = None
# SQLAlchemy session manager. Updated by model.init_model()
Session = scoped_session(sessionmaker())
# Global metadata. If you have multiple databases with overlapping table
# names, you'll need a metadata for each database
metadata = MetaData()

View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
This module contains the table definitions.
"""
from datetime import datetime
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.types import Unicode, Integer, UnicodeText, DateTime
from scribeengine.model.meta import metadata
# Definition of the "categories" table
categories_table = Table(u'categories', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'name', Unicode(100), nullable=False),
Column(u'description', UnicodeText),
Column(u'url', Unicode(255), nullable=False, index=True, unique=True),
)
# Definition of the "comments" table
comments_table = Table(u'comments', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'post_id', Integer, ForeignKey(u'posts.id'), nullable=True),
Column(u'user_id', Integer, ForeignKey(u'users.id'), nullable=False),
Column(u'title', Unicode(100), nullable=False),
Column(u'body', UnicodeText, nullable=False),
Column(u'status', Unicode(10), default='moderated'),
Column(u'created', DateTime, default=datetime.now()),
Column(u'modified', DateTime, default=datetime.now())
)
# Definition of the "pages" table
pages_table = Table(u'pages', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'title', Unicode(255), nullable=False),
Column(u'body', UnicodeText),
Column(u'url', Unicode(255), nullable=False, index=True, unique=True),
Column(u'created', DateTime, default=datetime.now()),
Column(u'modified', DateTime, default=datetime.now())
)
# Definition of the "permissions" table
permissions_table = Table(u'permissions', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'name', Unicode(80), nullable=False, index=True),
Column(u'description', UnicodeText)
)
# Definition of the "posts" table
posts_table = Table(u'posts', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'user_id', Integer, ForeignKey(u'users.id'), nullable=False),
Column(u'title', Unicode(255), nullable=False, index=True),
Column(u'body', UnicodeText, nullable=False, index=True),
Column(u'url', Unicode(255), nullable=False, index=True),
Column(u'status', Unicode(10), default=u'draft', index=True),
Column(u'comment_status', Unicode(10), default=u'open'),
Column(u'created', DateTime, default=datetime.now()),
Column(u'modified', DateTime, default=datetime.now())
)
# Definition of the "roles" table
roles_table = Table(u'roles', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'name', Unicode(80), nullable=False, index=True),
Column(u'description', UnicodeText)
)
# Definition of the "tags" table
tags_table = Table(u'tags', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'name', Unicode(100), nullable=False),
Column(u'url', Unicode(255), nullable=False, index=True),
)
# Definition of the "users" table
users_table = Table(u'users', metadata,
Column(u'id', Integer, primary_key=True),
Column(u'email', Unicode(200), nullable=False, index=True),
Column(u'password', Unicode(64), nullable=False),
Column(u'nick', Unicode(50), nullable=False, index=True),
Column(u'first_name', Unicode(100)),
Column(u'last_name', Unicode(100)),
Column(u'homepage', Unicode(200)),
Column(u'activation_key', Unicode(40))
)
# Definition of the "variables" table
variables_table = Table(u'variables', metadata,
Column(u'key', Unicode(100), primary_key=True, index=True),
Column(u'value', Unicode(100), nullable=False),
Column(u'type', Unicode(10), default=u'string')
)
# Definition of the "categories_posts" table
categories_posts_table = Table(u'categories_posts', metadata,
Column(u'category_id', Integer, ForeignKey(u'categories.id'), primary_key=True),
Column(u'post_id', Integer, ForeignKey(u'posts.id'), primary_key=True)
)
# Definition of the "permissions_roles" bridging table
permissions_roles_table = Table(u'permissions_roles', metadata,
Column(u'permission_id', Integer, ForeignKey(u'permissions.id'), primary_key=True),
Column(u'role_id', Integer, ForeignKey(u'roles.id'), primary_key=True)
)
# Definition of the "posts_tags" table
posts_tags_table = Table(u'posts_tags', metadata,
Column(u'post_id', Integer, ForeignKey(u'posts.id'), primary_key=True),
Column(u'tag_id', Integer, ForeignKey(u'tags.id'), primary_key=True)
)
# Definition of the "roles_users" bridging table
roles_users_table = Table(u'roles_users', metadata,
Column(u'user_id', Integer, ForeignKey(u'users.id'), primary_key=True),
Column(u'role_id', Integer, ForeignKey(u'roles.id'), primary_key=True)
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,22 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/
$(document).ready(function () {
ScribeEngine.Events.init();
});

View File

@ -0,0 +1,18 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/

View File

@ -0,0 +1,237 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/
window["ScribeEngine"] = {
Namespace: {
/**
* Create a Javascript namespace.
* Based on: http://code.google.com/p/namespacedotjs/
* Idea behind this is to created nested namespaces that are not ugly.
* Example:
* Namespace(foo.bar);
* foo.bar..myFunction = function () { } ;
*/
create: function (name, attributes) {
var parts = name.split('.'),
ns = window,
i = 0;
// find the deepest part of the namespace
// that is already defined
for(; i < parts.length && parts[i] in ns; i++)
ns = ns[parts[i]];
// initialize any remaining parts of the namespace
for(; i < parts.length; i++)
ns = ns[parts[i]] = {};
// copy the attributes into the namespace
for (var attr in attributes)
ns[attr] = attributes[attr];
},
exists: function (namespace) {
/**
* Determine the namespace of a page
*/
page_namespace = $ScribeEngine.Namespace.get_page_namespace();
return (namespace == page_namespace);
},
get_page_namespace: function () {
return $("#content > h2").attr("id");
}
}
};
Array.prototype.append = function (elem) {
this[this.length] = elem;
}
ScribeEngine.Namespace.create("ScribeEngine.Events", {
// Local variables
onload_functions: Array(),
// Functions
load: function (func) {
this.onload_functions.append(func);
},
click: function (selector, func) {
$(selector).bind("click", func);
},
change: function (selector, func) {
$(selector).bind("change", func);
},
submit: function (selector, func) {
$(selector).bind("submit", func);
},
blur: function (selector, func) {
$(selector).bind("blur", func);
},
paste: function (selector, func) {
$(selector).bind("paste", func);
},
keyup: function (selector, func) {
$(selector).bind("keyup", func);
},
keydown: function (selector, func) {
$(selector).bind("keydown", func);
},
keypress: function (selector, func) {
$(selector).bind("keypress", func);
},
get_element: function(event) {
var targ;
if (!event) {
var event = window.event;
}
if (event.target) {
targ = event.target;
}
else if (event.srcElement) {
targ = event.srcElement;
}
if (targ.nodeType == 3) {
// defeat Safari bug
targ = targ.parentNode;
}
return $(targ);
},
init: function () {
for (idx in this.onload_functions) {
func = this.onload_functions[idx];
func();
}
}
});
ScribeEngine.Namespace.create("ScribeEngine.Widgets", {
/**
* Adds a datepicker to an element.
*/
datepicker: function (selector)
{
$(selector).datepicker({showButtonPanel: true, dateFormat: "dd/mm/yy"});
}
});
ScribeEngine.Namespace.create("ScribeEngine.General", {
/**
* Fades out a message
*/
fade_message: function ()
{
$("#message").hide().fadeIn("slow", function() {
setTimeout("$('#message').fadeOut('slow');", 1500);
});
},
/**
* Checks for a message and fades it in and out.
*/
show_message: function ()
{
if ($("#message"))
{
setTimeout("$ScribeEngine.General.fade_message()", 500);
}
},
/**
* Dynamically hide anything on the page that has a "jshidden" class.
*/
hide_elements: function ()
{
$(".jshidden").hide();
},
/**
* Do all the funny things required to toggle a fieldset
*/
perform_toggle: function (fieldset)
{
content = $('> div', fieldset);
if (fieldset.is('.collapsed'))
{
fieldset.removeClass('collapsed');
content.slideDown('normal');
}
else
{
content.slideUp('normal',
function()
{
fieldset.addClass('collapsed');
}
);
}
},
/**
* Find the fieldset to toggle, and then perform the toggle
*/
toggle_fieldset: function ()
{
fieldset = $(this).parent().parent();
$ScribeEngine.General.perform_toggle(fieldset);
return false;
},
/**
* Sets the active project
*/
set_project: function ()
{
$('#project-selector > div.content').hide();
$('#project-throbber').show();
project_id = $("#CurrentProject").val();
$.get('/dashboard/ajax_project/' + project_id, function () {
window.location.reload();
});
},
/**
* Initialises collapsible fieldsets
*/
init_fieldsets: function ()
{
$("fieldset.collapsible > legend").each(function() {
legend = $(this);
legend_text = legend.text();
legend.text("");
legend.append(
$("<a>").attr("href", "#").attr("title", "Expand/collapse details")
.text(legend_text).click($ScribeEngine.General.toggle_fieldset));
});
$("fieldset.collapsed").each(function() {
$("> div.content", this).slideUp("normal");
});
},
/**
* Initialises elastic textareas
*/
init_textareas: function ()
{
$("textarea").elastic();
}
});
/**
* Global onload
*
* This function below will be executed on all page views.
*/
ScribeEngine.Events.load(function () {
// Hide hidden elements
ScribeEngine.General.hide_elements();
// Initialise collapsible fieldsets
ScribeEngine.General.init_fieldsets();
// Initialise elastic textareas
ScribeEngine.General.init_textareas();
// Show any flash messages
ScribeEngine.General.show_message();
});

View File

@ -0,0 +1,318 @@
/*
Design by Free CSS Templates
http://www.freecsstemplates.org
Released for free under a Creative Commons Attribution 2.5 License
*/
* {
margin: 0;
padding: 0;
}
body {
background: #000000 url(../images/img01.gif) repeat-x;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 13px;
color: #999999;
}
h1, h2, h3 {
}
h1 {
font-size: 3em;
}
h2 {
letter-spacing: -1px;
font-size: 2em;
}
h3 {
font-size: 1em;
}
p, ul, ol {
margin-top: 1.8em;
line-height: 180%;
}
ul, ol {
margin-left: 3em;
}
blockquote {
margin-left: 3em;
margin-right: 3em;
}
a {
color: #CCCCCC;
}
a:hover {
text-decoration: none;
color: #FFFFFF;
}
hr {
display: none;
}
/* Header */
#header {
width: 960px;
height: 80px;
margin: 0 auto;
background: url(../images/img02.jpg);
}
#header h1, #header h2 {
float: left;
margin: 0;
text-transform: uppercase;
color: #FFFFFF;
}
#header h1 {
padding: 30px 0 0 20px;
font-size: 3em;
}
#header h2 {
padding: 47px 0 0 8px;
font-size: 1.8em;
font-style: italic;
}
#header a {
text-decoration: none;
color: #FFFFFF;
}
/* Menu */
#menu {
width: 960px;
height: 51px;
margin: 0 auto;
background: url(../images/img03.jpg);
}
#menu ul {
margin: 0;
padding: 0;
list-style: none;
line-height: 51px;
}
#menu li {
float: left;
padding: 0 10px 0 20px;
line-height: 51px;
}
#menu a {
text-decoration: none;
letter-spacing: -1px;
font-size: 1.2em;
font-weight: bold;
line-height: 51px;
}
#menu a:hover {
text-decoration: underline;
}
/* Page */
#page {
width: 920px;
margin: 0 auto;
padding: 30px 20px 20px 20px;
background: url(../images/img04.jpg) no-repeat;
}
/* Content */
#content {
float: left;
width: 605px;
}
.post {
margin-bottom: 40px;
}
.post .title {
border-bottom: 1px solid #454545;
}
.post .title a {
text-decoration: none;
}
.post .entry {
padding: 0 20px;
}
.post .meta {
height: 20px;
padding: 15px 20px;
background: url(../images/img05.gif) no-repeat;
line-height: normal;
}
.post .meta a {
text-decoration: none;
font-weight: bold;
}
.post .meta a:hover {
text-decoration: underline;
}
.post .meta .byline {
float: left;
}
.post .meta .comments {
float: right;
}
.post .meta .read-more {
float: right;
margin-left: 1em;
}
/* Sidebar */
#sidebar {
float: right;
width: 295px;
}
#sidebar ul {
margin: 0;
padding: 0;
list-style: none;
}
#sidebar li {
}
#sidebar li ul {
padding: 0 0 20px 20px;
list-style: square inside;
}
#sidebar h2 {
height: 50px;
padding: 13px 20px 0 20px;
background: url(../images/img06.gif) no-repeat;
font-size: 1.6em;
}
/* Search */
#search {
padding: 20px;
text-align: center;
}
#search input {
margin-bottom: 10px;
padding: 3px 5px;
background: #1F1F1F url(../images/img06.gif) no-repeat center center;
border: 1px solid #454545;
font: bold 1.2em "Trebuchet MS", Arial, Helvetica, sans-serif;
color: #FFFFFF;
}
#search #s {
width: 80%;
background: #1F1F1F;
}
/* Calendar */
#calendar {
padding-bottom: 20px;
}
#calendar table, #calendar caption {
width: 80%;
margin: 0 auto;
text-align: center;
}
#calendar caption {
text-transform: uppercase;
letter-spacing: .25em;
font-weight: bold;
}
#calendar thead th {
background: #333333;
}
#calendar tbody td {
background: #111111;
}
#calendar a {
text-decoration: none;
font-weight: bold;
}
#calendar a:hover {
text-decoration: underline;
}
/* Footer */
#footer {
width: 960px;
margin: 0 auto;
padding: 20px 0;
background: url(../images/img07.gif) no-repeat;
}
#footer p {
margin: 0;
line-height: normal;
text-align: center;
}
/* Forms */
form {
margin-top: 1em;
}
label {
display: block;
margin-bottom: 0.3em;
}
fieldset {
border: none;
margin: 0;
padding: 0;
}
.form-text {
font-size: 1.5em;
padding: 4px 6px;
width: 593px;
}
.form-textarea {
height: 12em;
width: 605px;
}
.form-item {
margin-bottom: 1em;
}

View File

@ -0,0 +1,20 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title">Log in</h2>
<%include file="/errors.mako"/>
<form id="post-new" action="${h.url_for('/admin/login')}" method="post">
<fieldset>
<div class="form-item">
<label for="login-email">E-mail:</label>
<input type="text" name="email" id="login-email" class="form-text" />
</div>
<div class="form-item">
<label for="login-password">Password:</label>
<input type="password" name="password" id="login-password" class="form-text" />
</div>
<div class="form-item">
<input type="submit" name="action" value="Login"/>
</div>
</fieldset>
</form>
</div>

View File

@ -0,0 +1,28 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title">Register</h2>
<%include file="/errors.mako"/>
<form id="post-new" action="${h.url_for('/admin/register')}" method="post">
<fieldset>
<div class="form-item">
<label for="register-nick">Nick:</label>
<input type="text" name="nick" id="register-nick" class="form-text" />
</div>
<div class="form-item">
<label for="register-email">E-mail:</label>
<input type="text" name="email" id="register-email" class="form-text" />
</div>
<div class="form-item">
<label for="register-password">Password:</label>
<input type="password" name="password" id="register-password" class="form-text" />
</div>
<div class="form-item">
<label for="register-confirm-password">Confirm Password:</label>
<input type="password" name="confirm-password" id="register-confirm-password" class="form-text" />
</div>
<div class="form-item">
<input type="submit" name="action" value="Register"/>
</div>
</fieldset>
</form>
</div>

View File

@ -0,0 +1,37 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>${c.page_title}</title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<link href="/styles/style.css" rel="stylesheet" type="text/css" media="screen" />
</head>
<body>
<div id="header">
<h1><a href="${h.url_for('/')}" title="${c.blog_title}">${c.blog_title}</a></h1>
<h2>${c.blog_slogan}</h2>
</div>
<div id="menu">
<ul>
<li><a href="${h.url_for('/')}">Home</a></li>
% for page in c.pages:
<li><a href="${page.url}">${page.name}</a></li>
% endfor
</ul>
</div>
<hr />
<div id="page">
<div id="content">
${next.body()}
</div>
<%include file="/sidebar.mako"/>
<div style="clear: both;">&nbsp;</div>
</div>
<hr />
<div id="footer">
<p>Copyright &copy; 2010 Raoul and Hannah Snyman. Powered by ScribeEngine. Design by <a href="http://www.freecsstemplates.org/">Free CSS Templates</a>.</p>
</div>
</body>
</html>

View File

@ -0,0 +1,21 @@
<%inherit file="/base.mako"/>
% for post in c.posts:
<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %>
<div class="post">
<h2 class="title"><a href="${post.full_url}">${post.title}</a></h2>
<div class="entry">
${h.literal(h.teaser(post.body, post.full_url))}
</div>
<p class="meta">
<span class="byline">Posted by ${post.user.nick} on ${post.created.strftime('%B %d, %Y')}</span>
<a href="${post.full_url}" class="read-more">Read more</a>
% if len(post.comments) == 0:
<a href="${post.full_url}#comments" class="comments">No comments</a>
% elif len(post.comments) == 1:
<a href="${post.full_url}#comments" class="comments">1 comment</a>
% else:
<a href="${post.full_url}#comments" class="comments">${len(post.comments)} comments</a>
% endif
</p>
</div>
% endfor

View File

@ -0,0 +1,7 @@
<div class="post">
<h2 class="title"><a href="${h.url_for(year=post.created.strftime('%Y'), month=post.created.strftime('%m'), day=post.created.strftime('%d'), url=post.url)}">${post.title}</a></h2>
<div class="entry">
${h.literal(post.body)}
</div>
<p class="meta"><span class="byline">Posted by ${post.user.first_name} on ${post.created.strftime('%B %d, %Y')}</span> <a href="${h.url_for(year=post.created.strftime('%Y'), month=post.created.strftime('%m'), day=post.created.strftime('%d'), url=post.url)}#comments" class="comments">18 comments</a></p>
</div>

View File

@ -0,0 +1,48 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title"><a href="${h.url_for(year=c.post.created.strftime('%Y'), month=c.post.created.strftime('%m'), day=c.post.created.strftime('%d'), url=c.post.url)}">${c.post.title}</a></h2>
<div class="info">Posted by ${c.post.user.nick} on ${c.post.created.strftime('%B %d, %Y')}</div>
<div class="entry">
${h.literal(c.post.body)}
</div>
<div>&nbsp;</div>
% if len(c.post.comments) == 0:
<h3 id="comments">No Responses</h3>
<p>&nbsp;</p>
% elif len(c.post.comments) == 1:
<h3 id="comments">One Response</h3>
% else:
<h3 id="comments">${len(c.post.comments)} Responses</h3>
% endif
% if len(c.post.comments) > 0:
<ol class="commentlist">
% for num, comment in enumerate(c.post.comments):
<li id="comment-${comment.id}">
<cite>${comment.user.nick}</cite> Says:<br />
<small class="commentmetadata"><a href="#comment-<?php comment_ID() ?>" title="">${comment.created.strftime('%B %d, %Y')} at ${comment.created.strftime('%H:%M')}</a> <a href="edit">edit</a></small>
${comment.body}
</li>
% endfor
</ol>
% else:
% if c.post.comment_status != u'open':
<p class="nocomments">Comments are closed.</p>
% endif
% endif
% if c.post.comment_status == u'open':
<h3 id="respond">Leave a Reply</h3>
% if not c.current_user:
<p>You must be <a href="/login">logged in</a> to post a comment.</p>
% else:
<div>&nbsp;</div>
<form action="${h.url_for('/comment/edit')}" method="post" id="commentform">
<p>Logged in as <em>${c.current_user.nick}</em>. <a href="/logout" title="Log out of this account">Logout &raquo;</a></p>
<p><textarea name="comment" id="comment" cols="80" rows="10" tabindex="4" style="width: 605px;"></textarea></p>
<p>
<input name="submit" type="submit" id="submit" tabindex="5" value="Submit Comment" />
<input type="hidden" name="post_id" value="${c.post.id}" />
</p>
</form>
% endif
% endif
</div>

View File

@ -0,0 +1,38 @@
<table summary="Calendar">
<caption>
${c.thismonth.strftime('%B %Y')}
</caption>
<thead>
<tr>
<th abbr="Sunday" scope="col" title="Sunday">S</th>
<th abbr="Monday" scope="col" title="Monday">M</th>
<th abbr="Tuesday" scope="col" title="Tuesday">T</th>
<th abbr="Wednesday" scope="col" title="Wednesday">W</th>
<th abbr="Thursday" scope="col" title="Thursday">T</th>
<th abbr="Friday" scope="col" title="Friday">F</th>
<th abbr="Saturday" scope="col" title="Saturday">S</th>
</tr>
</thead>
<tfoot>
<tr>
<td abbr="October" colspan="3" id="prev"><a href="#" title="View posts for October 2007">&laquo; Oct</a></td>
<td class="pad">&nbsp;</td>
<td abbr="December" colspan="3" id="next"><a href="#" title="View posts for October 2007">Dec &raquo;</a></td>
</tr>
</tfoot>
<tbody>
% for week in c.calendar.monthdays2calendar(c.thismonth.year, c.thismonth.month):
<tr>
% for day, weekday in week:
% if day == 0:
<td class="pad">&nbsp;</td>
% elif day == c.today.day:
<td id="today">${day}</td>
% else:
<td>${day}</td>
% endif
% endfor
</tr>
% endfor
</tbody>
</table>

View File

@ -0,0 +1,5 @@
Dear ${c.name},
This is a test e-mail. Please ignore it.
Thanks.

View File

@ -0,0 +1,16 @@
% if c.form_errors and len(c.form_errors) > 0:
<div id="form-errors">
<p>The following errors occurred:</p>
<ul>
% for field, message in c.form_errors.iteritems():
<li>${message}</li>
% endfor
</ul>
</div>
% else:
<div id="form-errors" clsas="hidden">
<p>The following errors occurred:</p>
<ul>
</ul>
</div>
% endif

View File

@ -0,0 +1,21 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title">New Post</h2>
<%include file="/errors.mako"/>
<form id="post-new" action="${h.url_for('/post/edit')}" method="post">
<fieldset>
<div class="form-item">
<!-- <label for="post-title">Title:</label> -->
<input type="text" name="title" id="post-title" class="form-text" />
</div>
<div class="form-item">
<!-- <label for="post-body">Body:</label> -->
<textarea name="body" id="post-body" class="form-textarea"></textarea>
</div>
<div class="form-item">
<input type="submit" name="action" value="Save Draft"/>
<input type="submit" name="action" value="Save &amp; Publish"/>
</div>
</fieldset>
</form>
</div>

View File

@ -0,0 +1,30 @@
<div id="sidebar">
<ul>
<li id="search">
<form id="searchform" method="get" action="/search">
<div>
<input type="text" name="keywords" id="s" size="15" />
<br />
<input name="submit" type="submit" value="Search" />
</div>
</form>
</li>
<li id="calendar">
<h2>Calendar</h2>
<div id="calendar_wrap">
<%include file="/calendar.mako"/>
</div>
</li>
% if len(c.categories) > 0:
<li>
<h2>Categories</h2>
<ul>
% for category in c.categories:
<li><a href="${h.url_for('/category/%s' % str(category.url))}" title="${category.name}">${category.name}</a> (${len(category.posts)}) </li>
% endfor
</ul>
</li>
% endif
</ul>
<div style="clear: both; height: 40px;">&nbsp;</div>
</div>

View File

@ -0,0 +1,36 @@
"""Pylons application test package
This package assumes the Pylons environment is already loaded, such as
when this script is imported from the `nosetests --with-pylons=test.ini`
command.
This module initializes the application via ``websetup`` (`paster
setup-app`) and provides the base testing objects.
"""
from unittest import TestCase
from paste.deploy import loadapp
from paste.script.appinstall import SetupCommand
from pylons import config, url
from routes.util import URLGenerator
from webtest import TestApp
import pylons.test
__all__ = ['environ', 'url', 'TestController']
# Invoke websetup with the current config file
SetupCommand('setup-app').run([config['__file__']])
environ = {}
class TestController(TestCase):
def __init__(self, *args, **kwargs):
if pylons.test.pylonsapp:
wsgiapp = pylons.test.pylonsapp
else:
wsgiapp = loadapp('config:%s' % config['__file__'])
self.app = TestApp(wsgiapp)
url._push_object(URLGenerator(config['routes.map'], environ))
TestCase.__init__(self, *args, **kwargs)

View File

@ -0,0 +1,7 @@
from scribeengine.tests import *
class TestAdminController(TestController):
def test_index(self):
response = self.app.get(url(controller='admin', action='index'))
# Test response...

View File

@ -0,0 +1,7 @@
from scribeengine.tests import *
class TestBlogController(TestController):
def test_index(self):
response = self.app.get(url(controller='blog', action='index'))
# Test response...

View File

@ -0,0 +1,7 @@
from scribeengine.tests import *
class TestPostController(TestController):
def test_index(self):
response = self.app.get(url(controller='post', action='index'))
# Test response...

View File

66
scribeengine/websetup.py Normal file
View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
###############################################################################
# ScribeEngine - Open Source Blog Software #
# --------------------------------------------------------------------------- #
# Copyright (c) 2010 Raoul Snyman #
# --------------------------------------------------------------------------- #
# This program is free software; you can redistribute it and/or modify it #
# under the terms of the GNU General Public License as published by the Free #
# Software Foundation; version 2 of the License. #
# #
# This program is distributed in the hope that it will be useful, but WITHOUT #
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or #
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for #
# more details. #
# #
# You should have received a copy of the GNU General Public License along #
# with this program; if not, write to the Free Software Foundation, Inc., 59 #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA #
###############################################################################
"""
Setup the ScribeEngine application
"""
import logging
from datetime import datetime
from scribeengine.config.environment import load_environment
log = logging.getLogger(__name__)
def setup_app(command, conf, vars):
"""Place any commands to setup scribeengine here"""
load_environment(conf.global_conf, conf.local_conf)
import hashlib
import hmac
from scribeengine.model.meta import metadata, Session, engine
from scribeengine.model import Category, Permission, Post, Variable, \
User, Role
# Create the tables if they don't already exist
metadata.create_all(bind=engine, checkfirst=True)
blog_title = Variable(key=u'blog title', value=u'ScribeEngine')
blog_slogan = Variable(key=u'blog slogan', value=u'open source blog software')
pylons_cat = Category(name=u'Pylons', url=u'pylons')
database_cat = Category(name=u'Database', url=u'database')
perm_addposts = Permission(name=u'Add Posts')
perm_editmyposts = Permission(name=u'Edit My Posts')
perm_delmyposts = Permission(name=u'Delete My Posts')
role_admin = Role(name=u'Administrator')
role_admin.permissions.extend([perm_addposts, perm_editmyposts, perm_delmyposts])
password = unicode(hmac.new(conf[u'security.salt'], u'omigosh',
hashlib.sha256).hexdigest(), u'utf-8')
user = User(email=u'raoul.snyman@saturnlaboratories.co.za',
password=password, nick=u'raoul')
user.roles.append(role_admin)
Session.add_all([blog_title, blog_slogan, user])
Session.commit()

31
setup.cfg Normal file
View File

@ -0,0 +1,31 @@
[egg_info]
tag_build = dev
tag_svn_revision = true
[easy_install]
find_links = http://www.pylonshq.com/download/
[nosetests]
with-pylons = test.ini
# Babel configuration
[compile_catalog]
domain = scribeengine
directory = scribeengine/i18n
statistics = true
[extract_messages]
add_comments = TRANSLATORS:
output_file = scribeengine/i18n/scribeengine.pot
width = 80
[init_catalog]
domain = scribeengine
input_file = scribeengine/i18n/scribeengine.pot
output_dir = scribeengine/i18n
[update_catalog]
domain = scribeengine
input_file = scribeengine/i18n/scribeengine.pot
output_dir = scribeengine/i18n
previous = true

37
setup.py Normal file
View File

@ -0,0 +1,37 @@
try:
from setuptools import setup, find_packages
except ImportError:
from ez_setup import use_setuptools
use_setuptools()
from setuptools import setup, find_packages
setup(
name='ScribeEngine',
version='0.1',
description='',
author='',
author_email='',
url='',
install_requires=[
"Pylons>=0.9.7",
"SQLAlchemy>=0.5",
],
setup_requires=["PasteScript>=1.6.3"],
packages=find_packages(exclude=['ez_setup']),
include_package_data=True,
test_suite='nose.collector',
package_data={'scribeengine': ['i18n/*/LC_MESSAGES/*.mo']},
#message_extractors={'scribeengine': [
# ('**.py', 'python', None),
# ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
# ('public/**', 'ignore', None)]},
zip_safe=False,
paster_plugins=['PasteScript', 'Pylons'],
entry_points="""
[paste.app_factory]
main = scribeengine.config.middleware:make_app
[paste.app_install]
main = pylons.util:PylonsInstaller
""",
)

21
test.ini Normal file
View File

@ -0,0 +1,21 @@
#
# ScribeEngine - Pylons testing environment configuration
#
# The %(here)s variable will be replaced with the parent directory of this file
#
[DEFAULT]
debug = true
# Uncomment and replace with the address which should receive any error reports
#email_to = you@yourdomain.com
smtp_server = localhost
error_email_from = paste@localhost
[server:main]
use = egg:Paste#http
host = 127.0.0.1
port = 5000
[app:main]
use = config:development.ini
# Add additional test specific configuration options as necessary.

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 667 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,22 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/
$(document).ready(function () {
ScribeEngine.Events.init();
});

View File

@ -0,0 +1,18 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/

View File

@ -0,0 +1,237 @@
/*****************************************************************************
* ScribeEngine - Open Source Blog Software *
* ------------------------------------------------------------------------- *
* Copyright (c) 2010 Raoul Snyman *
* ------------------------------------------------------------------------- *
* This program is free software; you can redistribute it and/or modify it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation; version 2 of the License. *
* *
* This program is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General *
* Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along *
* with this program; if not, write to the Free Software Foundation, Inc., *
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA *
*****************************************************************************/
window["ScribeEngine"] = {
Namespace: {
/**
* Create a Javascript namespace.
* Based on: http://code.google.com/p/namespacedotjs/
* Idea behind this is to created nested namespaces that are not ugly.
* Example:
* Namespace(foo.bar);
* foo.bar..myFunction = function () { } ;
*/
create: function (name, attributes) {
var parts = name.split('.'),
ns = window,
i = 0;
// find the deepest part of the namespace
// that is already defined
for(; i < parts.length && parts[i] in ns; i++)
ns = ns[parts[i]];
// initialize any remaining parts of the namespace
for(; i < parts.length; i++)
ns = ns[parts[i]] = {};
// copy the attributes into the namespace
for (var attr in attributes)
ns[attr] = attributes[attr];
},
exists: function (namespace) {
/**
* Determine the namespace of a page
*/
page_namespace = $ScribeEngine.Namespace.get_page_namespace();
return (namespace == page_namespace);
},
get_page_namespace: function () {
return $("#content > h2").attr("id");
}
}
};
Array.prototype.append = function (elem) {
this[this.length] = elem;
}
ScribeEngine.Namespace.create("ScribeEngine.Events", {
// Local variables
onload_functions: Array(),
// Functions
load: function (func) {
this.onload_functions.append(func);
},
click: function (selector, func) {
$(selector).bind("click", func);
},
change: function (selector, func) {
$(selector).bind("change", func);
},
submit: function (selector, func) {
$(selector).bind("submit", func);
},
blur: function (selector, func) {
$(selector).bind("blur", func);
},
paste: function (selector, func) {
$(selector).bind("paste", func);
},
keyup: function (selector, func) {
$(selector).bind("keyup", func);
},
keydown: function (selector, func) {
$(selector).bind("keydown", func);
},
keypress: function (selector, func) {
$(selector).bind("keypress", func);
},
get_element: function(event) {
var targ;
if (!event) {
var event = window.event;
}
if (event.target) {
targ = event.target;
}
else if (event.srcElement) {
targ = event.srcElement;
}
if (targ.nodeType == 3) {
// defeat Safari bug
targ = targ.parentNode;
}
return $(targ);
},
init: function () {
for (idx in this.onload_functions) {
func = this.onload_functions[idx];
func();
}
}
});
ScribeEngine.Namespace.create("ScribeEngine.Widgets", {
/**
* Adds a datepicker to an element.
*/
datepicker: function (selector)
{
$(selector).datepicker({showButtonPanel: true, dateFormat: "dd/mm/yy"});
}
});
ScribeEngine.Namespace.create("ScribeEngine.General", {
/**
* Fades out a message
*/
fade_message: function ()
{
$("#message").hide().fadeIn("slow", function() {
setTimeout("$('#message').fadeOut('slow');", 1500);
});
},
/**
* Checks for a message and fades it in and out.
*/
show_message: function ()
{
if ($("#message"))
{
setTimeout("$ScribeEngine.General.fade_message()", 500);
}
},
/**
* Dynamically hide anything on the page that has a "jshidden" class.
*/
hide_elements: function ()
{
$(".jshidden").hide();
},
/**
* Do all the funny things required to toggle a fieldset
*/
perform_toggle: function (fieldset)
{
content = $('> div', fieldset);
if (fieldset.is('.collapsed'))
{
fieldset.removeClass('collapsed');
content.slideDown('normal');
}
else
{
content.slideUp('normal',
function()
{
fieldset.addClass('collapsed');
}
);
}
},
/**
* Find the fieldset to toggle, and then perform the toggle
*/
toggle_fieldset: function ()
{
fieldset = $(this).parent().parent();
$ScribeEngine.General.perform_toggle(fieldset);
return false;
},
/**
* Sets the active project
*/
set_project: function ()
{
$('#project-selector > div.content').hide();
$('#project-throbber').show();
project_id = $("#CurrentProject").val();
$.get('/dashboard/ajax_project/' + project_id, function () {
window.location.reload();
});
},
/**
* Initialises collapsible fieldsets
*/
init_fieldsets: function ()
{
$("fieldset.collapsible > legend").each(function() {
legend = $(this);
legend_text = legend.text();
legend.text("");
legend.append(
$("<a>").attr("href", "#").attr("title", "Expand/collapse details")
.text(legend_text).click($ScribeEngine.General.toggle_fieldset));
});
$("fieldset.collapsed").each(function() {
$("> div.content", this).slideUp("normal");
});
},
/**
* Initialises elastic textareas
*/
init_textareas: function ()
{
$("textarea").elastic();
}
});
/**
* Global onload
*
* This function below will be executed on all page views.
*/
ScribeEngine.Events.load(function () {
// Hide hidden elements
ScribeEngine.General.hide_elements();
// Initialise collapsible fieldsets
ScribeEngine.General.init_fieldsets();
// Initialise elastic textareas
ScribeEngine.General.init_textareas();
// Show any flash messages
ScribeEngine.General.show_message();
});

View File

@ -0,0 +1,318 @@
/*
Design by Free CSS Templates
http://www.freecsstemplates.org
Released for free under a Creative Commons Attribution 2.5 License
*/
* {
margin: 0;
padding: 0;
}
body {
background: #000000 url(../images/img01.gif) repeat-x;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 13px;
color: #999999;
}
h1, h2, h3 {
}
h1 {
font-size: 3em;
}
h2 {
letter-spacing: -1px;
font-size: 2em;
}
h3 {
font-size: 1em;
}
p, ul, ol {
margin-top: 1.8em;
line-height: 180%;
}
ul, ol {
margin-left: 3em;
}
blockquote {
margin-left: 3em;
margin-right: 3em;
}
a {
color: #CCCCCC;
}
a:hover {
text-decoration: none;
color: #FFFFFF;
}
hr {
display: none;
}
/* Header */
#header {
width: 960px;
height: 80px;
margin: 0 auto;
background: url(../images/img02.jpg);
}
#header h1, #header h2 {
float: left;
margin: 0;
text-transform: uppercase;
color: #FFFFFF;
}
#header h1 {
padding: 30px 0 0 20px;
font-size: 3em;
}
#header h2 {
padding: 47px 0 0 8px;
font-size: 1.8em;
font-style: italic;
}
#header a {
text-decoration: none;
color: #FFFFFF;
}
/* Menu */
#menu {
width: 960px;
height: 51px;
margin: 0 auto;
background: url(../images/img03.jpg);
}
#menu ul {
margin: 0;
padding: 0;
list-style: none;
line-height: 51px;
}
#menu li {
float: left;
padding: 0 10px 0 20px;
line-height: 51px;
}
#menu a {
text-decoration: none;
letter-spacing: -1px;
font-size: 1.2em;
font-weight: bold;
line-height: 51px;
}
#menu a:hover {
text-decoration: underline;
}
/* Page */
#page {
width: 920px;
margin: 0 auto;
padding: 30px 20px 20px 20px;
background: url(../images/img04.jpg) no-repeat;
}
/* Content */
#content {
float: left;
width: 605px;
}
.post {
margin-bottom: 40px;
}
.post .title {
border-bottom: 1px solid #454545;
}
.post .title a {
text-decoration: none;
}
.post .entry {
padding: 0 20px;
}
.post .meta {
height: 20px;
padding: 15px 20px;
background: url(../images/img05.gif) no-repeat;
line-height: normal;
}
.post .meta a {
text-decoration: none;
font-weight: bold;
}
.post .meta a:hover {
text-decoration: underline;
}
.post .meta .byline {
float: left;
}
.post .meta .comments {
float: right;
}
.post .meta .read-more {
float: right;
margin-left: 1em;
}
/* Sidebar */
#sidebar {
float: right;
width: 295px;
}
#sidebar ul {
margin: 0;
padding: 0;
list-style: none;
}
#sidebar li {
}
#sidebar li ul {
padding: 0 0 20px 20px;
list-style: square inside;
}
#sidebar h2 {
height: 50px;
padding: 13px 20px 0 20px;
background: url(../images/img06.gif) no-repeat;
font-size: 1.6em;
}
/* Search */
#search {
padding: 20px;
text-align: center;
}
#search input {
margin-bottom: 10px;
padding: 3px 5px;
background: #1F1F1F url(../images/img06.gif) no-repeat center center;
border: 1px solid #454545;
font: bold 1.2em "Trebuchet MS", Arial, Helvetica, sans-serif;
color: #FFFFFF;
}
#search #s {
width: 80%;
background: #1F1F1F;
}
/* Calendar */
#calendar {
padding-bottom: 20px;
}
#calendar table, #calendar caption {
width: 80%;
margin: 0 auto;
text-align: center;
}
#calendar caption {
text-transform: uppercase;
letter-spacing: .25em;
font-weight: bold;
}
#calendar thead th {
background: #333333;
}
#calendar tbody td {
background: #111111;
}
#calendar a {
text-decoration: none;
font-weight: bold;
}
#calendar a:hover {
text-decoration: underline;
}
/* Footer */
#footer {
width: 960px;
margin: 0 auto;
padding: 20px 0;
background: url(../images/img07.gif) no-repeat;
}
#footer p {
margin: 0;
line-height: normal;
text-align: center;
}
/* Forms */
form {
margin-top: 1em;
}
label {
display: block;
margin-bottom: 0.3em;
}
fieldset {
border: none;
margin: 0;
padding: 0;
}
.form-text {
font-size: 1.5em;
padding: 4px 6px;
width: 593px;
}
.form-textarea {
height: 12em;
width: 605px;
}
.form-item {
margin-bottom: 1em;
}

View File

@ -0,0 +1,37 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>${c.page_title}</title>
<meta name="keywords" content="" />
<meta name="description" content="" />
<link href="/styles/style.css" rel="stylesheet" type="text/css" media="screen" />
</head>
<body>
<div id="header">
<h1><a href="${h.url_for('/')}" title="${c.blog_title}">${c.blog_title}</a></h1>
<h2>${c.blog_slogan}</h2>
</div>
<div id="menu">
<ul>
<li><a href="${h.url_for('/')}">Home</a></li>
% for page in c.pages:
<li><a href="${page.url}">${page.name}</a></li>
% endfor
</ul>
</div>
<hr />
<div id="page">
<div id="content">
${next.body()}
</div>
<%include file="/sidebar.mako"/>
<div style="clear: both;">&nbsp;</div>
</div>
<hr />
<div id="footer">
<p>Copyright &copy; 2010 Raoul and Hannah Snyman. Powered by ScribeEngine. Design by <a href="http://www.freecsstemplates.org/">Free CSS Templates</a>.</p>
</div>
</body>
</html>

View File

@ -0,0 +1,22 @@
<%inherit file="/base.mako"/>
<h2 class="title">${c.page_title}</h2>
% for post in c.posts:
<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %>
<div class="post">
<h2 class="title"><a href="${post.full_url}">${post.title}</a></h2>
<div class="entry">
${h.literal(h.teaser(post.body, post.full_url))}
</div>
<p class="meta">
<span class="byline">Posted by ${post.user.nick} on ${post.created.strftime('%B %d, %Y')}</span>
<a href="${post.full_url}" class="read-more">Read more</a>
% if len(post.comments) == 0:
<a href="${post.full_url}#comments" class="comments">No comments</a>
% elif len(post.comments) == 1:
<a href="${post.full_url}#comments" class="comments">1 comment</a>
% else:
<a href="${post.full_url}#comments" class="comments">${len(post.comments)} comments</a>
% endif
</p>
</div>
% endfor

View File

@ -0,0 +1,21 @@
<%inherit file="/base.mako"/>
% for post in c.posts:
<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %>
<div class="post">
<h2 class="title"><a href="${post.full_url}">${post.title}</a></h2>
<div class="entry">
${h.literal(h.teaser(post.body, post.full_url))}
</div>
<p class="meta">
<span class="byline">Posted by ${post.user.nick} on ${post.created.strftime('%B %d, %Y')}</span>
<a href="${post.full_url}" class="read-more">Read more</a>
% if len(post.comments) == 0:
<a href="${post.full_url}#comments" class="comments">No comments</a>
% elif len(post.comments) == 1:
<a href="${post.full_url}#comments" class="comments">1 comment</a>
% else:
<a href="${post.full_url}#comments" class="comments">${len(post.comments)} comments</a>
% endif
</p>
</div>
% endfor

View File

@ -0,0 +1,7 @@
<div class="post">
<h2 class="title"><a href="${h.url_for(year=post.created.strftime('%Y'), month=post.created.strftime('%m'), day=post.created.strftime('%d'), url=post.url)}">${post.title}</a></h2>
<div class="entry">
${h.literal(post.body)}
</div>
<p class="meta"><span class="byline">Posted by ${post.user.first_name} on ${post.created.strftime('%B %d, %Y')}</span> <a href="${h.url_for(year=post.created.strftime('%Y'), month=post.created.strftime('%m'), day=post.created.strftime('%d'), url=post.url)}#comments" class="comments">18 comments</a></p>
</div>

View File

@ -0,0 +1,48 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title"><a href="${h.url_for(year=c.post.created.strftime('%Y'), month=c.post.created.strftime('%m'), day=c.post.created.strftime('%d'), url=c.post.url)}">${c.post.title}</a></h2>
<div class="info">Posted by ${c.post.user.nick} on ${c.post.created.strftime('%B %d, %Y')}</div>
<div class="entry">
${h.literal(c.post.body)}
</div>
<div>&nbsp;</div>
% if len(c.post.comments) == 0:
<h3 id="comments">No Responses</h3>
<p>&nbsp;</p>
% elif len(c.post.comments) == 1:
<h3 id="comments">One Response</h3>
% else:
<h3 id="comments">${len(c.post.comments)} Responses</h3>
% endif
% if len(c.post.comments) > 0:
<ol class="commentlist">
% for num, comment in enumerate(c.post.comments):
<li id="comment-${comment.id}">
<cite>${comment.user.nick}</cite> Says:<br />
<small class="commentmetadata"><a href="#comment-<?php comment_ID() ?>" title="">${comment.created.strftime('%B %d, %Y')} at ${comment.created.strftime('%H:%M')}</a> <a href="edit">edit</a></small>
${comment.body}
</li>
% endfor
</ol>
% else:
% if c.post.comment_status != u'open':
<p class="nocomments">Comments are closed.</p>
% endif
% endif
% if c.post.comment_status == u'open':
<h3 id="respond">Leave a Reply</h3>
% if not c.current_user:
<p>You must be <a href="/login">logged in</a> to post a comment.</p>
% else:
<div>&nbsp;</div>
<form action="${h.url_for('/comment/edit')}" method="post" id="commentform">
<p>Logged in as <em>${c.current_user.nick}</em>. <a href="/logout" title="Log out of this account">Logout &raquo;</a></p>
<p><textarea name="comment" id="comment" cols="80" rows="10" tabindex="4" style="width: 605px;"></textarea></p>
<p>
<input name="submit" type="submit" id="submit" tabindex="5" value="Submit Comment" />
<input type="hidden" name="post_id" value="${c.post.id}" />
</p>
</form>
% endif
% endif
</div>

View File

@ -0,0 +1,38 @@
<table summary="Calendar">
<caption>
${c.thismonth.strftime('%B %Y')}
</caption>
<thead>
<tr>
<th abbr="Sunday" scope="col" title="Sunday">S</th>
<th abbr="Monday" scope="col" title="Monday">M</th>
<th abbr="Tuesday" scope="col" title="Tuesday">T</th>
<th abbr="Wednesday" scope="col" title="Wednesday">W</th>
<th abbr="Thursday" scope="col" title="Thursday">T</th>
<th abbr="Friday" scope="col" title="Friday">F</th>
<th abbr="Saturday" scope="col" title="Saturday">S</th>
</tr>
</thead>
<tfoot>
<tr>
<td abbr="October" colspan="3" id="prev"><a href="#" title="View posts for October 2007">&laquo; Oct</a></td>
<td class="pad">&nbsp;</td>
<td abbr="December" colspan="3" id="next"><a href="#" title="View posts for October 2007">Dec &raquo;</a></td>
</tr>
</tfoot>
<tbody>
% for week in c.calendar.monthdays2calendar(c.thismonth.year, c.thismonth.month):
<tr>
% for day, weekday in week:
% if day == 0:
<td class="pad">&nbsp;</td>
% elif day == c.today.day:
<td id="today">${day}</td>
% else:
<td>${day}</td>
% endif
% endfor
</tr>
% endfor
</tbody>
</table>

View File

@ -0,0 +1,20 @@
<%inherit file="/base.mako"/>
<div class="post">
<h2 class="title">New Post</h2>
<form id="post-new" action="${h.url_for('/post/edit')}" method="post">
<fieldset>
<div class="form-item">
<!-- <label for="post-title">Title:</label> -->
<input type="text" name="title" id="post-title" class="form-text" />
</div>
<div class="form-item">
<!-- <label for="post-body">Body:</label> -->
<textarea name="body" id="post-body" class="form-textarea"></textarea>
</div>
<div class="form-item">
<input type="submit" name="action" value="Save Draft"/>
<input type="submit" name="action" value="Save &amp; Publish"/>
</div>
</fieldset>
</form>
</div>

View File

@ -0,0 +1,30 @@
<div id="sidebar">
<ul>
<li id="search">
<form id="searchform" method="get" action="/search">
<div>
<input type="text" name="keywords" id="s" size="15" />
<br />
<input name="submit" type="submit" value="Search" />
</div>
</form>
</li>
<li id="calendar">
<h2>Calendar</h2>
<div id="calendar_wrap">
<%include file="/calendar.mako"/>
</div>
</li>
% if len(c.categories) > 0:
<li>
<h2>Categories</h2>
<ul>
% for category in c.categories:
<li><a href="${h.url_for('/category/%s' % str(category.url))}" title="${category.name}">${category.name}</a> (${len(category.posts)}) </li>
% endfor
</ul>
</li>
% endif
</ul>
<div style="clear: both; height: 40px;">&nbsp;</div>
</div>