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'' % 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"/> +
+

Log in

+ <%include file="/errors.mako"/> +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
diff --git a/scribeengine/templates/admin/register.mako b/scribeengine/templates/admin/register.mako new file mode 100644 index 0000000..0ba3a11 --- /dev/null +++ b/scribeengine/templates/admin/register.mako @@ -0,0 +1,28 @@ +<%inherit file="/base.mako"/> +
+

Register

+ <%include file="/errors.mako"/> +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
diff --git a/scribeengine/templates/base.mako b/scribeengine/templates/base.mako new file mode 100644 index 0000000..532a870 --- /dev/null +++ b/scribeengine/templates/base.mako @@ -0,0 +1,37 @@ + + + + + ${c.page_title} + + + + + +
+ +
+
+
+ ${next.body()} +
+ <%include file="/sidebar.mako"/> +
 
+
+
+ + + diff --git a/scribeengine/templates/blog/index.mako b/scribeengine/templates/blog/index.mako new file mode 100644 index 0000000..3287432 --- /dev/null +++ b/scribeengine/templates/blog/index.mako @@ -0,0 +1,21 @@ +<%inherit file="/base.mako"/> +% for post in c.posts: +<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %> +
+

${post.title}

+
+ ${h.literal(h.teaser(post.body, post.full_url))} +
+

+ + Read more +% if len(post.comments) == 0: + No comments +% elif len(post.comments) == 1: + 1 comment +% else: + ${len(post.comments)} comments +% endif +

+
+% endfor diff --git a/scribeengine/templates/blog/teaser.mako b/scribeengine/templates/blog/teaser.mako new file mode 100644 index 0000000..23ada47 --- /dev/null +++ b/scribeengine/templates/blog/teaser.mako @@ -0,0 +1,7 @@ +
+

${post.title}

+
+ ${h.literal(post.body)} +
+

18 comments

+
diff --git a/scribeengine/templates/blog/view.mako b/scribeengine/templates/blog/view.mako new file mode 100644 index 0000000..5445b19 --- /dev/null +++ b/scribeengine/templates/blog/view.mako @@ -0,0 +1,48 @@ +<%inherit file="/base.mako"/> +
+

${c.post.title}

+
Posted by ${c.post.user.nick} on ${c.post.created.strftime('%B %d, %Y')}
+
+ ${h.literal(c.post.body)} +
+
 
+% if len(c.post.comments) == 0: +

No Responses

+

 

+% elif len(c.post.comments) == 1: +

One Response

+% else: +

${len(c.post.comments)} Responses

+% endif +% if len(c.post.comments) > 0: +
    +% for num, comment in enumerate(c.post.comments): +
  1. + ${comment.user.nick} Says:
    + + ${comment.body} +
  2. +% endfor +
+% else: +% if c.post.comment_status != u'open': +

Comments are closed.

+% endif +% endif +% if c.post.comment_status == u'open': +

Leave a Reply

+% if not c.current_user: +

You must be logged in to post a comment.

+% else: +
 
+
+

Logged in as ${c.current_user.nick}. Logout »

+

+

+ + +

+
+% endif +% endif +
diff --git a/scribeengine/templates/calendar.mako b/scribeengine/templates/calendar.mako new file mode 100644 index 0000000..76d5975 --- /dev/null +++ b/scribeengine/templates/calendar.mako @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + +% for week in c.calendar.monthdays2calendar(c.thismonth.year, c.thismonth.month): + +% for day, weekday in week: +% if day == 0: + +% elif day == c.today.day: + +% else: + +% endif +% endfor + +% endfor + +
+ ${c.thismonth.strftime('%B %Y')} +
SMTWTFS
« Oct Dec »
 ${day}${day}
diff --git a/scribeengine/templates/email/test.mako b/scribeengine/templates/email/test.mako new file mode 100644 index 0000000..7df1536 --- /dev/null +++ b/scribeengine/templates/email/test.mako @@ -0,0 +1,5 @@ +Dear ${c.name}, + +This is a test e-mail. Please ignore it. + +Thanks. diff --git a/scribeengine/templates/errors.mako b/scribeengine/templates/errors.mako new file mode 100644 index 0000000..0d1f878 --- /dev/null +++ b/scribeengine/templates/errors.mako @@ -0,0 +1,16 @@ +% if c.form_errors and len(c.form_errors) > 0: +
+

The following errors occurred:

+
    +% for field, message in c.form_errors.iteritems(): +
  • ${message}
  • +% endfor +
+
+% else: +
+

The following errors occurred:

+
    +
+
+% endif diff --git a/scribeengine/templates/post/new.mako b/scribeengine/templates/post/new.mako new file mode 100644 index 0000000..37fe5cb --- /dev/null +++ b/scribeengine/templates/post/new.mako @@ -0,0 +1,21 @@ +<%inherit file="/base.mako"/> +
+

New Post

+ <%include file="/errors.mako"/> +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
diff --git a/scribeengine/templates/sidebar.mako b/scribeengine/templates/sidebar.mako new file mode 100644 index 0000000..e57ba24 --- /dev/null +++ b/scribeengine/templates/sidebar.mako @@ -0,0 +1,30 @@ + diff --git a/scribeengine/tests/__init__.py b/scribeengine/tests/__init__.py new file mode 100644 index 0000000..d51f6f2 --- /dev/null +++ b/scribeengine/tests/__init__.py @@ -0,0 +1,36 @@ +"""Pylons application test package + +This package assumes the Pylons environment is already loaded, such as +when this script is imported from the `nosetests --with-pylons=test.ini` +command. + +This module initializes the application via ``websetup`` (`paster +setup-app`) and provides the base testing objects. +""" +from unittest import TestCase + +from paste.deploy import loadapp +from paste.script.appinstall import SetupCommand +from pylons import config, url +from routes.util import URLGenerator +from webtest import TestApp + +import pylons.test + +__all__ = ['environ', 'url', 'TestController'] + +# Invoke websetup with the current config file +SetupCommand('setup-app').run([config['__file__']]) + +environ = {} + +class TestController(TestCase): + + def __init__(self, *args, **kwargs): + if pylons.test.pylonsapp: + wsgiapp = pylons.test.pylonsapp + else: + wsgiapp = loadapp('config:%s' % config['__file__']) + self.app = TestApp(wsgiapp) + url._push_object(URLGenerator(config['routes.map'], environ)) + TestCase.__init__(self, *args, **kwargs) diff --git a/scribeengine/tests/functional/__init__.py b/scribeengine/tests/functional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scribeengine/tests/functional/test_admin.py b/scribeengine/tests/functional/test_admin.py new file mode 100644 index 0000000..0424370 --- /dev/null +++ b/scribeengine/tests/functional/test_admin.py @@ -0,0 +1,7 @@ +from scribeengine.tests import * + +class TestAdminController(TestController): + + def test_index(self): + response = self.app.get(url(controller='admin', action='index')) + # Test response... diff --git a/scribeengine/tests/functional/test_blog.py b/scribeengine/tests/functional/test_blog.py new file mode 100644 index 0000000..8645947 --- /dev/null +++ b/scribeengine/tests/functional/test_blog.py @@ -0,0 +1,7 @@ +from scribeengine.tests import * + +class TestBlogController(TestController): + + def test_index(self): + response = self.app.get(url(controller='blog', action='index')) + # Test response... diff --git a/scribeengine/tests/functional/test_post.py b/scribeengine/tests/functional/test_post.py new file mode 100644 index 0000000..92ad32e --- /dev/null +++ b/scribeengine/tests/functional/test_post.py @@ -0,0 +1,7 @@ +from scribeengine.tests import * + +class TestPostController(TestController): + + def test_index(self): + response = self.app.get(url(controller='post', action='index')) + # Test response... diff --git a/scribeengine/tests/test_models.py b/scribeengine/tests/test_models.py new file mode 100644 index 0000000..e69de29 diff --git a/scribeengine/websetup.py b/scribeengine/websetup.py new file mode 100644 index 0000000..75568b3 --- /dev/null +++ b/scribeengine/websetup.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4 + +############################################################################### +# ScribeEngine - Open Source Blog Software # +# --------------------------------------------------------------------------- # +# Copyright (c) 2010 Raoul Snyman # +# --------------------------------------------------------------------------- # +# This program is free software; you can redistribute it and/or modify it # +# under the terms of the GNU General Public License as published by the Free # +# Software Foundation; version 2 of the License. # +# # +# This program is distributed in the hope that it will be useful, but WITHOUT # +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for # +# more details. # +# # +# You should have received a copy of the GNU General Public License along # +# with this program; if not, write to the Free Software Foundation, Inc., 59 # +# Temple Place, Suite 330, Boston, MA 02111-1307 USA # +############################################################################### + +""" +Setup the ScribeEngine application +""" +import logging +from datetime import datetime + +from scribeengine.config.environment import load_environment + +log = logging.getLogger(__name__) + +def setup_app(command, conf, vars): + """Place any commands to setup scribeengine here""" + load_environment(conf.global_conf, conf.local_conf) + + import hashlib + import hmac + from scribeengine.model.meta import metadata, Session, engine + from scribeengine.model import Category, Permission, Post, Variable, \ + User, Role + + # Create the tables if they don't already exist + metadata.create_all(bind=engine, checkfirst=True) + + blog_title = Variable(key=u'blog title', value=u'ScribeEngine') + blog_slogan = Variable(key=u'blog slogan', value=u'open source blog software') + + pylons_cat = Category(name=u'Pylons', url=u'pylons') + database_cat = Category(name=u'Database', url=u'database') + + perm_addposts = Permission(name=u'Add Posts') + perm_editmyposts = Permission(name=u'Edit My Posts') + perm_delmyposts = Permission(name=u'Delete My Posts') + + role_admin = Role(name=u'Administrator') + role_admin.permissions.extend([perm_addposts, perm_editmyposts, perm_delmyposts]) + + password = unicode(hmac.new(conf[u'security.salt'], u'omigosh', + hashlib.sha256).hexdigest(), u'utf-8') + user = User(email=u'raoul.snyman@saturnlaboratories.co.za', + password=password, nick=u'raoul') + user.roles.append(role_admin) + + Session.add_all([blog_title, blog_slogan, user]) + Session.commit() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..07a0365 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true + +[easy_install] +find_links = http://www.pylonshq.com/download/ + +[nosetests] +with-pylons = test.ini + +# Babel configuration +[compile_catalog] +domain = scribeengine +directory = scribeengine/i18n +statistics = true + +[extract_messages] +add_comments = TRANSLATORS: +output_file = scribeengine/i18n/scribeengine.pot +width = 80 + +[init_catalog] +domain = scribeengine +input_file = scribeengine/i18n/scribeengine.pot +output_dir = scribeengine/i18n + +[update_catalog] +domain = scribeengine +input_file = scribeengine/i18n/scribeengine.pot +output_dir = scribeengine/i18n +previous = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d53c50c --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +try: + from setuptools import setup, find_packages +except ImportError: + from ez_setup import use_setuptools + use_setuptools() + from setuptools import setup, find_packages + +setup( + name='ScribeEngine', + version='0.1', + description='', + author='', + author_email='', + url='', + install_requires=[ + "Pylons>=0.9.7", + "SQLAlchemy>=0.5", + ], + setup_requires=["PasteScript>=1.6.3"], + packages=find_packages(exclude=['ez_setup']), + include_package_data=True, + test_suite='nose.collector', + package_data={'scribeengine': ['i18n/*/LC_MESSAGES/*.mo']}, + #message_extractors={'scribeengine': [ + # ('**.py', 'python', None), + # ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}), + # ('public/**', 'ignore', None)]}, + zip_safe=False, + paster_plugins=['PasteScript', 'Pylons'], + entry_points=""" + [paste.app_factory] + main = scribeengine.config.middleware:make_app + + [paste.app_install] + main = pylons.util:PylonsInstaller + """, +) diff --git a/test.ini b/test.ini new file mode 100644 index 0000000..6a61263 --- /dev/null +++ b/test.ini @@ -0,0 +1,21 @@ +# +# ScribeEngine - Pylons testing environment configuration +# +# The %(here)s variable will be replaced with the parent directory of this file +# +[DEFAULT] +debug = true +# Uncomment and replace with the address which should receive any error reports +#email_to = you@yourdomain.com +smtp_server = localhost +error_email_from = paste@localhost + +[server:main] +use = egg:Paste#http +host = 127.0.0.1 +port = 5000 + +[app:main] +use = config:development.ini + +# Add additional test specific configuration options as necessary. diff --git a/themes/stargazer/public/images/img01.gif b/themes/stargazer/public/images/img01.gif new file mode 100644 index 0000000..88507b6 Binary files /dev/null and b/themes/stargazer/public/images/img01.gif differ diff --git a/themes/stargazer/public/images/img02.jpg b/themes/stargazer/public/images/img02.jpg new file mode 100644 index 0000000..ea1cc21 Binary files /dev/null and b/themes/stargazer/public/images/img02.jpg differ diff --git a/themes/stargazer/public/images/img03.jpg b/themes/stargazer/public/images/img03.jpg new file mode 100644 index 0000000..f2647c4 Binary files /dev/null and b/themes/stargazer/public/images/img03.jpg differ diff --git a/themes/stargazer/public/images/img04.jpg b/themes/stargazer/public/images/img04.jpg new file mode 100644 index 0000000..3b48a29 Binary files /dev/null and b/themes/stargazer/public/images/img04.jpg differ diff --git a/themes/stargazer/public/images/img05.gif b/themes/stargazer/public/images/img05.gif new file mode 100644 index 0000000..71a4c0b Binary files /dev/null and b/themes/stargazer/public/images/img05.gif differ diff --git a/themes/stargazer/public/images/img06.gif b/themes/stargazer/public/images/img06.gif new file mode 100644 index 0000000..ac11e60 Binary files /dev/null and b/themes/stargazer/public/images/img06.gif differ diff --git a/themes/stargazer/public/images/img07.gif b/themes/stargazer/public/images/img07.gif new file mode 100644 index 0000000..c11f25e Binary files /dev/null and b/themes/stargazer/public/images/img07.gif differ diff --git a/themes/stargazer/public/images/spacer.gif b/themes/stargazer/public/images/spacer.gif new file mode 100644 index 0000000..5bfd67a Binary files /dev/null and b/themes/stargazer/public/images/spacer.gif differ diff --git a/themes/stargazer/public/pylons-logo.gif b/themes/stargazer/public/pylons-logo.gif new file mode 100644 index 0000000..61b2d9a Binary files /dev/null and b/themes/stargazer/public/pylons-logo.gif differ diff --git a/themes/stargazer/public/scripts/ScribeEngine.Init.js b/themes/stargazer/public/scripts/ScribeEngine.Init.js new file mode 100644 index 0000000..3fdf8a3 --- /dev/null +++ b/themes/stargazer/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/themes/stargazer/public/scripts/ScribeEngine.Post.js b/themes/stargazer/public/scripts/ScribeEngine.Post.js new file mode 100644 index 0000000..53cf33f --- /dev/null +++ b/themes/stargazer/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/themes/stargazer/public/scripts/ScribeEngine.js b/themes/stargazer/public/scripts/ScribeEngine.js new file mode 100644 index 0000000..8421739 --- /dev/null +++ b/themes/stargazer/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/themes/stargazer/public/styles/style.css b/themes/stargazer/public/styles/style.css new file mode 100644 index 0000000..b1a42b2 --- /dev/null +++ b/themes/stargazer/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/themes/stargazer/templates/base.mako b/themes/stargazer/templates/base.mako new file mode 100644 index 0000000..9068453 --- /dev/null +++ b/themes/stargazer/templates/base.mako @@ -0,0 +1,37 @@ + + + + +${c.page_title} + + + + + + + +
+
+
+ ${next.body()} +
+ <%include file="/sidebar.mako"/> +
 
+
+
+ + + diff --git a/themes/stargazer/templates/blog/archive.mako b/themes/stargazer/templates/blog/archive.mako new file mode 100644 index 0000000..83a4f4e --- /dev/null +++ b/themes/stargazer/templates/blog/archive.mako @@ -0,0 +1,22 @@ +<%inherit file="/base.mako"/> +

${c.page_title}

+% for post in c.posts: +<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %> +
+

${post.title}

+
+ ${h.literal(h.teaser(post.body, post.full_url))} +
+

+ + Read more +% if len(post.comments) == 0: + No comments +% elif len(post.comments) == 1: + 1 comment +% else: + ${len(post.comments)} comments +% endif +

+
+% endfor diff --git a/themes/stargazer/templates/blog/index.mako b/themes/stargazer/templates/blog/index.mako new file mode 100644 index 0000000..3287432 --- /dev/null +++ b/themes/stargazer/templates/blog/index.mako @@ -0,0 +1,21 @@ +<%inherit file="/base.mako"/> +% for post in c.posts: +<% post.full_url = u'/archive/%s/%s/%s/%s' % (post.created.strftime('%Y'), post.created.strftime('%m'), post.created.strftime('%d'), post.url) %> +
+

${post.title}

+
+ ${h.literal(h.teaser(post.body, post.full_url))} +
+

+ + Read more +% if len(post.comments) == 0: + No comments +% elif len(post.comments) == 1: + 1 comment +% else: + ${len(post.comments)} comments +% endif +

+
+% endfor diff --git a/themes/stargazer/templates/blog/teaser.mako b/themes/stargazer/templates/blog/teaser.mako new file mode 100644 index 0000000..23ada47 --- /dev/null +++ b/themes/stargazer/templates/blog/teaser.mako @@ -0,0 +1,7 @@ +
+

${post.title}

+
+ ${h.literal(post.body)} +
+

18 comments

+
diff --git a/themes/stargazer/templates/blog/view.mako b/themes/stargazer/templates/blog/view.mako new file mode 100644 index 0000000..5445b19 --- /dev/null +++ b/themes/stargazer/templates/blog/view.mako @@ -0,0 +1,48 @@ +<%inherit file="/base.mako"/> +
+

${c.post.title}

+
Posted by ${c.post.user.nick} on ${c.post.created.strftime('%B %d, %Y')}
+
+ ${h.literal(c.post.body)} +
+
 
+% if len(c.post.comments) == 0: +

No Responses

+

 

+% elif len(c.post.comments) == 1: +

One Response

+% else: +

${len(c.post.comments)} Responses

+% endif +% if len(c.post.comments) > 0: +
    +% for num, comment in enumerate(c.post.comments): +
  1. + ${comment.user.nick} Says:
    + + ${comment.body} +
  2. +% endfor +
+% else: +% if c.post.comment_status != u'open': +

Comments are closed.

+% endif +% endif +% if c.post.comment_status == u'open': +

Leave a Reply

+% if not c.current_user: +

You must be logged in to post a comment.

+% else: +
 
+
+

Logged in as ${c.current_user.nick}. Logout »

+

+

+ + +

+
+% endif +% endif +
diff --git a/themes/stargazer/templates/calendar.mako b/themes/stargazer/templates/calendar.mako new file mode 100644 index 0000000..76d5975 --- /dev/null +++ b/themes/stargazer/templates/calendar.mako @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + +% for week in c.calendar.monthdays2calendar(c.thismonth.year, c.thismonth.month): + +% for day, weekday in week: +% if day == 0: + +% elif day == c.today.day: + +% else: + +% endif +% endfor + +% endfor + +
+ ${c.thismonth.strftime('%B %Y')} +
SMTWTFS
« Oct Dec »
 ${day}${day}
diff --git a/themes/stargazer/templates/post/new.mako b/themes/stargazer/templates/post/new.mako new file mode 100644 index 0000000..a36d8e5 --- /dev/null +++ b/themes/stargazer/templates/post/new.mako @@ -0,0 +1,20 @@ +<%inherit file="/base.mako"/> +
+

New Post

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
diff --git a/themes/stargazer/templates/sidebar.mako b/themes/stargazer/templates/sidebar.mako new file mode 100644 index 0000000..e57ba24 --- /dev/null +++ b/themes/stargazer/templates/sidebar.mako @@ -0,0 +1,30 @@ +