commit 1803fbe824e47bb944ff66ff4b64481836328852
Author: Raoul Snyman
Date: Fri Jan 15 22:55:30 2010 +0200
Initial import
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..02d6969
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,3 @@
+include scribeengine/config/deployment.ini_tmpl
+recursive-include scribeengine/public *
+recursive-include scribeengine/templates *
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..2c948e5
--- /dev/null
+++ b/README.txt
@@ -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.
diff --git a/ScribeEngine.egg-info/PKG-INFO b/ScribeEngine.egg-info/PKG-INFO
new file mode 100644
index 0000000..3a976cd
--- /dev/null
+++ b/ScribeEngine.egg-info/PKG-INFO
@@ -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
diff --git a/ScribeEngine.egg-info/SOURCES.txt b/ScribeEngine.egg-info/SOURCES.txt
new file mode 100644
index 0000000..c0490b3
--- /dev/null
+++ b/ScribeEngine.egg-info/SOURCES.txt
@@ -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
\ No newline at end of file
diff --git a/ScribeEngine.egg-info/dependency_links.txt b/ScribeEngine.egg-info/dependency_links.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ScribeEngine.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/ScribeEngine.egg-info/entry_points.txt b/ScribeEngine.egg-info/entry_points.txt
new file mode 100644
index 0000000..7839909
--- /dev/null
+++ b/ScribeEngine.egg-info/entry_points.txt
@@ -0,0 +1,7 @@
+
+ [paste.app_factory]
+ main = scribeengine.config.middleware:make_app
+
+ [paste.app_install]
+ main = pylons.util:PylonsInstaller
+
\ No newline at end of file
diff --git a/ScribeEngine.egg-info/not-zip-safe b/ScribeEngine.egg-info/not-zip-safe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ScribeEngine.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/ScribeEngine.egg-info/paster_plugins.txt b/ScribeEngine.egg-info/paster_plugins.txt
new file mode 100644
index 0000000..c24c7fe
--- /dev/null
+++ b/ScribeEngine.egg-info/paster_plugins.txt
@@ -0,0 +1,2 @@
+PasteScript
+Pylons
diff --git a/ScribeEngine.egg-info/requires.txt b/ScribeEngine.egg-info/requires.txt
new file mode 100644
index 0000000..b6f774b
--- /dev/null
+++ b/ScribeEngine.egg-info/requires.txt
@@ -0,0 +1,2 @@
+Pylons>=0.9.7
+SQLAlchemy>=0.5
\ No newline at end of file
diff --git a/ScribeEngine.egg-info/top_level.txt b/ScribeEngine.egg-info/top_level.txt
new file mode 100644
index 0000000..43ae899
--- /dev/null
+++ b/ScribeEngine.egg-info/top_level.txt
@@ -0,0 +1 @@
+scribeengine
diff --git a/development.ini b/development.ini
new file mode 100644
index 0000000..349fcbd
--- /dev/null
+++ b/development.ini
@@ -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
diff --git a/docs/index.txt b/docs/index.txt
new file mode 100644
index 0000000..74d410b
--- /dev/null
+++ b/docs/index.txt
@@ -0,0 +1,19 @@
+scribeengine
+++++++++++++
+
+This is the main index page of your documentation. It should be written in
+`reStructuredText format `_.
+
+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/
diff --git a/ez_setup.py b/ez_setup.py
new file mode 100644
index 0000000..d24e845
--- /dev/null
+++ b/ez_setup.py
@@ -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:])
+
+
+
+
+
+
diff --git a/scribeengine/__init__.py b/scribeengine/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scribeengine/config/__init__.py b/scribeengine/config/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scribeengine/config/deployment.ini_tmpl b/scribeengine/config/deployment.ini_tmpl
new file mode 100644
index 0000000..5f8c2aa
--- /dev/null
+++ b/scribeengine/config/deployment.ini_tmpl
@@ -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
diff --git a/scribeengine/config/environment.py b/scribeengine/config/environment.py
new file mode 100644
index 0000000..ca27317
--- /dev/null
+++ b/scribeengine/config/environment.py
@@ -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)
diff --git a/scribeengine/config/middleware.py b/scribeengine/config/middleware.py
new file mode 100644
index 0000000..b8aa7ee
--- /dev/null
+++ b/scribeengine/config/middleware.py
@@ -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:] section of the Paste ini file (where
+ 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
diff --git a/scribeengine/config/routing.py b/scribeengine/config/routing.py
new file mode 100644
index 0000000..6a91213
--- /dev/null
+++ b/scribeengine/config/routing.py
@@ -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
diff --git a/scribeengine/controllers/__init__.py b/scribeengine/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scribeengine/controllers/admin.py b/scribeengine/controllers/admin.py
new file mode 100644
index 0000000..7b99cf1
--- /dev/null
+++ b/scribeengine/controllers/admin.py
@@ -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('/')
+
diff --git a/scribeengine/controllers/blog.py b/scribeengine/controllers/blog.py
new file mode 100644
index 0000000..dca72f8
--- /dev/null
+++ b/scribeengine/controllers/blog.py
@@ -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')
diff --git a/scribeengine/controllers/error.py b/scribeengine/controllers/error.py
new file mode 100644
index 0000000..8c8f879
--- /dev/null
+++ b/scribeengine/controllers/error.py
@@ -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'))
diff --git a/scribeengine/controllers/post.py b/scribeengine/controllers/post.py
new file mode 100644
index 0000000..37d0204
--- /dev/null
+++ b/scribeengine/controllers/post.py
@@ -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)))
+
diff --git a/scribeengine/lib/__init__.py b/scribeengine/lib/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/scribeengine/lib/app_globals.py b/scribeengine/lib/app_globals.py
new file mode 100644
index 0000000..15e183d
--- /dev/null
+++ b/scribeengine/lib/app_globals.py
@@ -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()
+
diff --git a/scribeengine/lib/base.py b/scribeengine/lib/base.py
new file mode 100644
index 0000000..1ac180b
--- /dev/null
+++ b/scribeengine/lib/base.py
@@ -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 _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 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 section of the template.
+ """
+ c.jsinit = filename
+
+ def _add_stylesheet(self, filename, subdir=None):
+ """
+ This method dynamically adds javascript files to the 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)
+
diff --git a/scribeengine/lib/helpers.py b/scribeengine/lib/helpers.py
new file mode 100644
index 0000000..026daf8
--- /dev/null
+++ b/scribeengine/lib/helpers.py
@@ -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'
')
+ 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()
diff --git a/scribeengine/lib/utils.py b/scribeengine/lib/utils.py
new file mode 100644
index 0000000..caa9db1
--- /dev/null
+++ b/scribeengine/lib/utils.py
@@ -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)
diff --git a/scribeengine/lib/validation/__init__.py b/scribeengine/lib/validation/__init__.py
new file mode 100644
index 0000000..66849cb
--- /dev/null
+++ b/scribeengine/lib/validation/__init__.py
@@ -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)
+
diff --git a/scribeengine/lib/validation/client.py b/scribeengine/lib/validation/client.py
new file mode 100644
index 0000000..5d3d8df
--- /dev/null
+++ b/scribeengine/lib/validation/client.py
@@ -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)
diff --git a/scribeengine/lib/validation/server.py b/scribeengine/lib/validation/server.py
new file mode 100644
index 0000000..46414f9
--- /dev/null
+++ b/scribeengine/lib/validation/server.py
@@ -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)
+
diff --git a/scribeengine/model/__init__.py b/scribeengine/model/__init__.py
new file mode 100644
index 0000000..46dee34
--- /dev/null
+++ b/scribeengine/model/__init__.py
@@ -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)
diff --git a/scribeengine/model/classes.py b/scribeengine/model/classes.py
new file mode 100644
index 0000000..49ca412
--- /dev/null
+++ b/scribeengine/model/classes.py
@@ -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
diff --git a/scribeengine/model/meta.py b/scribeengine/model/meta.py
new file mode 100644
index 0000000..7f0e3d0
--- /dev/null
+++ b/scribeengine/model/meta.py
@@ -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()
diff --git a/scribeengine/model/tables.py b/scribeengine/model/tables.py
new file mode 100644
index 0000000..af15ec9
--- /dev/null
+++ b/scribeengine/model/tables.py
@@ -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)
+)
diff --git a/scribeengine/public/images/img01.gif b/scribeengine/public/images/img01.gif
new file mode 100644
index 0000000..88507b6
Binary files /dev/null and b/scribeengine/public/images/img01.gif differ
diff --git a/scribeengine/public/images/img02.jpg b/scribeengine/public/images/img02.jpg
new file mode 100644
index 0000000..ea1cc21
Binary files /dev/null and b/scribeengine/public/images/img02.jpg differ
diff --git a/scribeengine/public/images/img03.jpg b/scribeengine/public/images/img03.jpg
new file mode 100644
index 0000000..f2647c4
Binary files /dev/null and b/scribeengine/public/images/img03.jpg differ
diff --git a/scribeengine/public/images/img04.jpg b/scribeengine/public/images/img04.jpg
new file mode 100644
index 0000000..3b48a29
Binary files /dev/null and b/scribeengine/public/images/img04.jpg differ
diff --git a/scribeengine/public/images/img05.gif b/scribeengine/public/images/img05.gif
new file mode 100644
index 0000000..71a4c0b
Binary files /dev/null and b/scribeengine/public/images/img05.gif differ
diff --git a/scribeengine/public/images/img06.gif b/scribeengine/public/images/img06.gif
new file mode 100644
index 0000000..ac11e60
Binary files /dev/null and b/scribeengine/public/images/img06.gif differ
diff --git a/scribeengine/public/images/img07.gif b/scribeengine/public/images/img07.gif
new file mode 100644
index 0000000..c11f25e
Binary files /dev/null and b/scribeengine/public/images/img07.gif differ
diff --git a/scribeengine/public/images/spacer.gif b/scribeengine/public/images/spacer.gif
new file mode 100644
index 0000000..5bfd67a
Binary files /dev/null and b/scribeengine/public/images/spacer.gif differ
diff --git a/scribeengine/public/pylons-logo.gif b/scribeengine/public/pylons-logo.gif
new file mode 100644
index 0000000..61b2d9a
Binary files /dev/null and b/scribeengine/public/pylons-logo.gif differ
diff --git a/scribeengine/public/scripts/ScribeEngine.Init.js b/scribeengine/public/scripts/ScribeEngine.Init.js
new file mode 100644
index 0000000..3fdf8a3
--- /dev/null
+++ b/scribeengine/public/scripts/ScribeEngine.Init.js
@@ -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();
+});
diff --git a/scribeengine/public/scripts/ScribeEngine.Post.js b/scribeengine/public/scripts/ScribeEngine.Post.js
new file mode 100644
index 0000000..53cf33f
--- /dev/null
+++ b/scribeengine/public/scripts/ScribeEngine.Post.js
@@ -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 *
+ *****************************************************************************/
diff --git a/scribeengine/public/scripts/ScribeEngine.js b/scribeengine/public/scripts/ScribeEngine.js
new file mode 100644
index 0000000..8421739
--- /dev/null
+++ b/scribeengine/public/scripts/ScribeEngine.js
@@ -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(
+ $("").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();
+});
diff --git a/scribeengine/public/styles/style.css b/scribeengine/public/styles/style.css
new file mode 100644
index 0000000..b1a42b2
--- /dev/null
+++ b/scribeengine/public/styles/style.css
@@ -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;
+}
diff --git a/scribeengine/templates/admin/login.mako b/scribeengine/templates/admin/login.mako
new file mode 100644
index 0000000..c0e7af8
--- /dev/null
+++ b/scribeengine/templates/admin/login.mako
@@ -0,0 +1,20 @@
+<%inherit file="/base.mako"/>
+
+ ${comment.created.strftime('%B %d, %Y')} at ${comment.created.strftime('%H:%M')} edit + ${comment.body} +