From 1803fbe824e47bb944ff66ff4b64481836328852 Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 15 Jan 2010 22:55:30 +0200 Subject: [PATCH] Initial import --- MANIFEST.in | 3 + README.txt | 19 ++ ScribeEngine.egg-info/PKG-INFO | 10 + ScribeEngine.egg-info/SOURCES.txt | 34 ++ ScribeEngine.egg-info/dependency_links.txt | 1 + ScribeEngine.egg-info/entry_points.txt | 7 + ScribeEngine.egg-info/not-zip-safe | 1 + ScribeEngine.egg-info/paster_plugins.txt | 2 + ScribeEngine.egg-info/requires.txt | 2 + ScribeEngine.egg-info/top_level.txt | 1 + development.ini | 97 ++++++ docs/index.txt | 19 ++ ez_setup.py | 276 +++++++++++++++ scribeengine/__init__.py | 0 scribeengine/config/__init__.py | 0 scribeengine/config/deployment.ini_tmpl | 63 ++++ scribeengine/config/environment.py | 55 +++ scribeengine/config/middleware.py | 69 ++++ scribeengine/config/routing.py | 56 +++ scribeengine/controllers/__init__.py | 0 scribeengine/controllers/admin.py | 118 +++++++ scribeengine/controllers/blog.py | 72 ++++ scribeengine/controllers/error.py | 46 +++ scribeengine/controllers/post.py | 63 ++++ scribeengine/lib/__init__.py | 0 scribeengine/lib/app_globals.py | 40 +++ scribeengine/lib/base.py | 240 +++++++++++++ scribeengine/lib/helpers.py | 84 +++++ scribeengine/lib/utils.py | 113 +++++++ scribeengine/lib/validation/__init__.py | 177 ++++++++++ scribeengine/lib/validation/client.py | 230 +++++++++++++ scribeengine/lib/validation/server.py | 58 ++++ scribeengine/model/__init__.py | 67 ++++ scribeengine/model/classes.py | 119 +++++++ scribeengine/model/meta.py | 40 +++ scribeengine/model/tables.py | 138 ++++++++ scribeengine/public/images/img01.gif | Bin 0 -> 399 bytes scribeengine/public/images/img02.jpg | Bin 0 -> 23025 bytes scribeengine/public/images/img03.jpg | Bin 0 -> 8052 bytes scribeengine/public/images/img04.jpg | Bin 0 -> 11048 bytes scribeengine/public/images/img05.gif | Bin 0 -> 810 bytes scribeengine/public/images/img06.gif | Bin 0 -> 667 bytes scribeengine/public/images/img07.gif | Bin 0 -> 2214 bytes scribeengine/public/images/spacer.gif | Bin 0 -> 43 bytes scribeengine/public/pylons-logo.gif | Bin 0 -> 2399 bytes .../public/scripts/ScribeEngine.Init.js | 22 ++ .../public/scripts/ScribeEngine.Post.js | 18 + scribeengine/public/scripts/ScribeEngine.js | 237 +++++++++++++ scribeengine/public/styles/style.css | 318 ++++++++++++++++++ scribeengine/templates/admin/login.mako | 20 ++ scribeengine/templates/admin/register.mako | 28 ++ scribeengine/templates/base.mako | 37 ++ scribeengine/templates/blog/index.mako | 21 ++ scribeengine/templates/blog/teaser.mako | 7 + scribeengine/templates/blog/view.mako | 48 +++ scribeengine/templates/calendar.mako | 38 +++ scribeengine/templates/email/test.mako | 5 + scribeengine/templates/errors.mako | 16 + scribeengine/templates/post/new.mako | 21 ++ scribeengine/templates/sidebar.mako | 30 ++ scribeengine/tests/__init__.py | 36 ++ scribeengine/tests/functional/__init__.py | 0 scribeengine/tests/functional/test_admin.py | 7 + scribeengine/tests/functional/test_blog.py | 7 + scribeengine/tests/functional/test_post.py | 7 + scribeengine/tests/test_models.py | 0 scribeengine/websetup.py | 66 ++++ setup.cfg | 31 ++ setup.py | 37 ++ test.ini | 21 ++ themes/stargazer/public/images/img01.gif | Bin 0 -> 399 bytes themes/stargazer/public/images/img02.jpg | Bin 0 -> 23025 bytes themes/stargazer/public/images/img03.jpg | Bin 0 -> 8052 bytes themes/stargazer/public/images/img04.jpg | Bin 0 -> 11048 bytes themes/stargazer/public/images/img05.gif | Bin 0 -> 810 bytes themes/stargazer/public/images/img06.gif | Bin 0 -> 667 bytes themes/stargazer/public/images/img07.gif | Bin 0 -> 2214 bytes themes/stargazer/public/images/spacer.gif | Bin 0 -> 43 bytes themes/stargazer/public/pylons-logo.gif | Bin 0 -> 2399 bytes .../public/scripts/ScribeEngine.Init.js | 22 ++ .../public/scripts/ScribeEngine.Post.js | 18 + .../stargazer/public/scripts/ScribeEngine.js | 237 +++++++++++++ themes/stargazer/public/styles/style.css | 318 ++++++++++++++++++ themes/stargazer/templates/base.mako | 37 ++ themes/stargazer/templates/blog/archive.mako | 22 ++ themes/stargazer/templates/blog/index.mako | 21 ++ themes/stargazer/templates/blog/teaser.mako | 7 + themes/stargazer/templates/blog/view.mako | 48 +++ themes/stargazer/templates/calendar.mako | 38 +++ themes/stargazer/templates/post/new.mako | 20 ++ themes/stargazer/templates/sidebar.mako | 30 ++ 91 files changed, 4216 insertions(+) create mode 100644 MANIFEST.in create mode 100644 README.txt create mode 100644 ScribeEngine.egg-info/PKG-INFO create mode 100644 ScribeEngine.egg-info/SOURCES.txt create mode 100644 ScribeEngine.egg-info/dependency_links.txt create mode 100644 ScribeEngine.egg-info/entry_points.txt create mode 100644 ScribeEngine.egg-info/not-zip-safe create mode 100644 ScribeEngine.egg-info/paster_plugins.txt create mode 100644 ScribeEngine.egg-info/requires.txt create mode 100644 ScribeEngine.egg-info/top_level.txt create mode 100644 development.ini create mode 100644 docs/index.txt create mode 100644 ez_setup.py create mode 100644 scribeengine/__init__.py create mode 100644 scribeengine/config/__init__.py create mode 100644 scribeengine/config/deployment.ini_tmpl create mode 100644 scribeengine/config/environment.py create mode 100644 scribeengine/config/middleware.py create mode 100644 scribeengine/config/routing.py create mode 100644 scribeengine/controllers/__init__.py create mode 100644 scribeengine/controllers/admin.py create mode 100644 scribeengine/controllers/blog.py create mode 100644 scribeengine/controllers/error.py create mode 100644 scribeengine/controllers/post.py create mode 100644 scribeengine/lib/__init__.py create mode 100644 scribeengine/lib/app_globals.py create mode 100644 scribeengine/lib/base.py create mode 100644 scribeengine/lib/helpers.py create mode 100644 scribeengine/lib/utils.py create mode 100644 scribeengine/lib/validation/__init__.py create mode 100644 scribeengine/lib/validation/client.py create mode 100644 scribeengine/lib/validation/server.py create mode 100644 scribeengine/model/__init__.py create mode 100644 scribeengine/model/classes.py create mode 100644 scribeengine/model/meta.py create mode 100644 scribeengine/model/tables.py create mode 100644 scribeengine/public/images/img01.gif create mode 100644 scribeengine/public/images/img02.jpg create mode 100644 scribeengine/public/images/img03.jpg create mode 100644 scribeengine/public/images/img04.jpg create mode 100644 scribeengine/public/images/img05.gif create mode 100644 scribeengine/public/images/img06.gif create mode 100644 scribeengine/public/images/img07.gif create mode 100644 scribeengine/public/images/spacer.gif create mode 100644 scribeengine/public/pylons-logo.gif create mode 100644 scribeengine/public/scripts/ScribeEngine.Init.js create mode 100644 scribeengine/public/scripts/ScribeEngine.Post.js create mode 100644 scribeengine/public/scripts/ScribeEngine.js create mode 100644 scribeengine/public/styles/style.css create mode 100644 scribeengine/templates/admin/login.mako create mode 100644 scribeengine/templates/admin/register.mako create mode 100644 scribeengine/templates/base.mako create mode 100644 scribeengine/templates/blog/index.mako create mode 100644 scribeengine/templates/blog/teaser.mako create mode 100644 scribeengine/templates/blog/view.mako create mode 100644 scribeengine/templates/calendar.mako create mode 100644 scribeengine/templates/email/test.mako create mode 100644 scribeengine/templates/errors.mako create mode 100644 scribeengine/templates/post/new.mako create mode 100644 scribeengine/templates/sidebar.mako create mode 100644 scribeengine/tests/__init__.py create mode 100644 scribeengine/tests/functional/__init__.py create mode 100644 scribeengine/tests/functional/test_admin.py create mode 100644 scribeengine/tests/functional/test_blog.py create mode 100644 scribeengine/tests/functional/test_post.py create mode 100644 scribeengine/tests/test_models.py create mode 100644 scribeengine/websetup.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test.ini create mode 100644 themes/stargazer/public/images/img01.gif create mode 100644 themes/stargazer/public/images/img02.jpg create mode 100644 themes/stargazer/public/images/img03.jpg create mode 100644 themes/stargazer/public/images/img04.jpg create mode 100644 themes/stargazer/public/images/img05.gif create mode 100644 themes/stargazer/public/images/img06.gif create mode 100644 themes/stargazer/public/images/img07.gif create mode 100644 themes/stargazer/public/images/spacer.gif create mode 100644 themes/stargazer/public/pylons-logo.gif create mode 100644 themes/stargazer/public/scripts/ScribeEngine.Init.js create mode 100644 themes/stargazer/public/scripts/ScribeEngine.Post.js create mode 100644 themes/stargazer/public/scripts/ScribeEngine.js create mode 100644 themes/stargazer/public/styles/style.css create mode 100644 themes/stargazer/templates/base.mako create mode 100644 themes/stargazer/templates/blog/archive.mako create mode 100644 themes/stargazer/templates/blog/index.mako create mode 100644 themes/stargazer/templates/blog/teaser.mako create mode 100644 themes/stargazer/templates/blog/view.mako create mode 100644 themes/stargazer/templates/calendar.mako create mode 100644 themes/stargazer/templates/post/new.mako create mode 100644 themes/stargazer/templates/sidebar.mako 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 0000000000000000000000000000000000000000..88507b62508f6bdd52322643eeb0eea7298c3c92 GIT binary patch literal 399 zcmZ?wbhEHbWMt50yvo49!NI}F$;r;n&dbZo$jHde&CSNf#>K_O!^0ybCB?_br>v|j zARr(qDap^zFE1}IC@3f)At53nA}uW~CnqN>D=RE4EG{mtsHn)q#3UvrCL|;zDk`d^ zq{Ph3%)-K=qM{-rBg4wds-U2ts;a7^qob{@t)ZczrKP2&rpCa)psA^;uC8umWTdOB z3*?dt6o0aSwdjCIke?XX);KgfI*2gzH8dC%WQfgUZ7Fe()mYc3lYF|N!aSin!s*!z z(-+-Vhw^p^e>jpNw<`SIlt0P#uaf`CHgY#}E3~O}Xm)Ap^s4u(YfsXcqM#-UQ4pPd;d;L%aOqv0AYbwuK)l5 literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/img02.jpg b/scribeengine/public/images/img02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea1cc21a8f951846cb3929f486cb2187a6758f00 GIT binary patch literal 23025 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 literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/img03.jpg b/scribeengine/public/images/img03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2647c4f8ef51cbd9a5978603064c1b44eed5e14 GIT binary patch literal 8052 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 literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/img04.jpg b/scribeengine/public/images/img04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b48a29d967a8fc541dfc1781ff58740bd466639 GIT binary patch literal 11048 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 literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/img06.gif b/scribeengine/public/images/img06.gif new file mode 100644 index 0000000000000000000000000000000000000000..ac11e60ed181d5128550574a4dd652a3fa1537fb GIT binary patch literal 667 zcmZ?wbhEHbRA)3|xXJ(m^78VouC59S3e3#RYHDhFdU_@%CieFBhK7cmoSYUG7Dh%! zA|fIzEG(?7tTHk(R#sMKW@dtdf^2MTQc_Y(OiT_A4mLJ6`uh4jJUq_M&YGH<*4EZ4 zDk_$imZGAf{QUf~va;sp=BB2m?Ck6k5)wi}LOMD+N=i!H+}v_{$v5G(*cnnKQXZVcVK8Rn2^JAVoA}-9IN>kR+M9{f9$C{|Ni&?h5}|*9(gWjZnnnmp5DIx2@@wx z&Sm3f(dL{od(PZ>^A}9w)MjU0xO~ORRjVhkvU6mu*|d4f)>X_L;@fub*}JbnoMZpt zBS(*L=o~wB`pgO)iL)0kUG9~Ty>jE`trA(m+xH*b6y$#R^w}wH<>#;8>{eEJ`|;CC z752~Hf6Qj*`T6%>50A-TX%7ScCLUH{t)9t_7Z0^_^Db!c*r?DYE^7R%`k>?DhfB`qM+Ub*tFU&QkFX2#u?foBc?C|Jo}RAu_c?H8r}1r0MVoH9 zyQVHYmsSLO_bznY`ue(?>52y5-P`;`^>SX{lf8UY@!o+-Z4JRaJKhS3^k~=nK0Ns8 d`3iZ92EV(Tn+>mSj#f)>++BV`tciia8UVAO@frXC literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/img07.gif b/scribeengine/public/images/img07.gif new file mode 100644 index 0000000000000000000000000000000000000000..c11f25e4b0a7f3e17b0aaf568273edf8fe86ae6d GIT binary patch literal 2214 zcmb`C`!gE|0)XR{s3xi?8m|;ZRn>HRxU_R)Ze2x$HMpptNdsJ2=YSfrKPpCwS|QR1Olyb?Out3_f$_jIFJ$x3{;6 ziHVt+*~N<&O-)TbJUn19*!lD4U0q#WTwEL+96UWey}Y~}9Uae|I|qeAot>TC-Q6!- zxZvdE>)QiZT)Y&^N5+U)T@e^;7uA$kv4Rd2$w4mOCV* zNADl_G&scgJUlWwHqKYxP}}# zBPf{9&n@lz5?K@&=o!QlS&Qgs9DD8bO@%1QB%k>P_HD6CL}xSmz3&giM0RHjw=(db zoZ1K~mR}R{t8|G?#Z4OPL!Vo@BD?U@O}EE%)arrORAe`|41h*=aXaFJ*0{$d1$<_}2~* zv&=KH4%OGW$*uY(eTCOY6?c0EJaLPncWuw$e<0oE)4S6Jj$uP}QT;u##ok|cR`@j< zi?hJ|)9%p&y$TWQ{pBAg2KtoZsiw&K=udR@&O9}Jb>dUMc3(L1+Wou10liW(L;5kG zanzudi@5dQ4GkV2>XbXHlS4yBBkx0*NuIews_>bmH(rU3=-2 zj9)8o$%;4r1ccs%trvM!q^`g6#p2hC{d*+quLH+@Md<5usiKmQExf4orbZ%q6AHYv zQ5NQKf8#B}o3QcDKh4JDeax*(o8`A-?{9vHPbO?uBxY@IRwf0V9|qj3xWDyVDweQS z{h()it0r^&lDPKq+%o?-p^VZ9pU4X=kh# zo1C(+Pv7led6RZKc|kk7)TvvSWnEum(`DWB$s}3NQr3=)CM>wT*Sk@XzSk$gl8k%w z-JYF2s#lJDZ$LGdzW+(LMcN-c((LRH0YLt8##AoF|sGf~aX;sf9X3Nxqq{1tj`FqTPP=Hl)t7hQ= zO{Q7QWM0uOJr+FFF6W3_wJT4wGVN*}C_pEC;h3RYEAna6t)qkYbfU8G0R2)2Dnq|n zmC~l)s?FZhi!p@(21#>e27K8BmT8cZX?unp3Nzqvw@Z*=?3X3EVh;BPw0np94A9jh z`KaTgBL&N+{pf%fynm#e3cq@+`WpA>SUsQ8eymx_-apm~3$LE&Mm>`NdI_%m#IQ@- zKRJ{$uNse3f=9+r^gtWj^c;#CZ~(H@5w^Gi?^d-+!YdRSutxr*0G;3g*~V*FaQ z0-cI#f(lsUL3Bk)Cc4Q*j9|hSjEd46UXz=a#r%sw@g@)43x{ z;0JHZ(9K>x5o~0_!MiG6^Z6MpJ39B^JqFz38y>;=zWSgXAJ$@OUB-!}AAF#oTl`ZZ zxCsjfALx;J0oiQsUBf{o1B?wUjNttYRaUW3*lU$+UNT(y+Y}mm9T&k*OHfwN^RPiQ zHa{a*StA7FLYNUev+H@a5)=+DU{7Sxl^^A3+)Z)BWbT5pPUoL&W?nY=)S#>fK=7d; zv3x6w^GhLdiYJx!sh;ZbWch#z9-zWky zj`O90u4*v`mJm=W$eHQ|6*h!Vh|cEB)EQK`PzW(5ygIJ|s>UOth~HOozGC5OLQDzq zHV!#UOqgvpXAxs*oY}TqH7N-~if1C{I;+*>)F@JdfHT)aSGQ)CkP^j60ewN;mcu9A z)p7)b26cNLgq#G5oF9g2C`D1^pB=gL<8Vz!Sqb@`Pvqa}rkKtuJ~=s<`};($hKhl- zriR~003=p);iFp9;kj?8vB1&}bP7rYUDS<-OmviH^F(fk zI%cSKXIUX?!^2t6LPU3%SMWBxZ|K=ErJdzC)TZAZJtuymvx3Ik4EVR6n`BL`VxqRL e)#!Pt(bQ@IZ!4%@&(AES){0Rn{vZ>;jsF6jhn++K literal 0 HcmV?d00001 diff --git a/scribeengine/public/images/spacer.gif b/scribeengine/public/images/spacer.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bfd67a2d6f72ac3a55cbfcea5866e841d22f5d9 GIT binary patch literal 43 mcmZ?wbhEHbWMp7uXkdT>#h)yUAf^t80Ld^gF}W}@SOWlZ0R#L1 literal 0 HcmV?d00001 diff --git a/scribeengine/public/pylons-logo.gif b/scribeengine/public/pylons-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..61b2d9ab369e8d161b5c2fd9e484be28d11b2486 GIT binary patch literal 2399 zcmV-l383~zNk%w1VLJgt0M!Ekii(Q!^YeOnbtx$+T3TAy*Vi2#9ipP5=;-K3NJtbD z6eT4k#>U3-%rYS%A)k<085tS2wzh6zD6Nz%JUlzzzB!G1F2bonA0QtxGBGG9Crm^p z8yp)P8yp)N8XFuO92^`P8X6-bA|4(dA0HnlCnqK*CL$stARr(hA0Qte9w;a%BO@au zBqb*(C?q2zBqSs#Cnpsa78n^B7Z(>77#9~878n>95D*X&5)vvZDjORcD=RB+Zf^h9 zF#rGmA^8LW00093EC2ui06PIh000L6z=3c`EE78iP# zn0OZ!j&TG(prN9pq@V}~J%5r;2?PQHiUg~(kan53m<BnYCEvX<`=hwL5I_sL=bWM4J%JH}Gro)>@?PEuf77zjY_qfas{D|ITB zq1augaDFaF?Fo&({hjgvz72@C>7gsDUZWR^*p5^5?s z3Xw#->F5thI-yma3^mY2oDG?a}kf@GE2>J`5O7z%Tq2VN9tgFI6Iz*$;N*iLCXHI+Twz3TY<*pn3 zltC-C5`g1v?hw-d;5(HDVww_glF}}Z%^qJRIoD5HoMC&YZ41_@+dkt9gM^pi|n*}M)VLbcy15MO9E1xSwxU#b{YT!Esjm| z17I)oK-V=tfwN2`-dl<{fQ!;Lv-KEJxhNwgkwMY)8Syb+MkxEY)ru*`xUF<1KFtNI zcl+?!pafn2#N7%4+jl7E3Y~coyXV`)=Apbd`Y19+ zf>l)mMv7#pLMAwoIC(3D?TDAdKq<;!KV;lFJXnDZHco_bDo!k{fWR)1>R3xKVDQq% z1PW3AO-nPm;r6m9q=A{S2LkY51N3LcBEBd92}ooDis*zl{=!71iX-?2pp6LDF&KAr zq`dU_pCk0hY4!*s<2vX%5Ehbu%M-&DDJ8=$dUBH5pyLvVr$SARVv9KBq*M^8!9PN; zl)jW76An3s8g4^@E0kgp-WLTIzN!ee6a_B#_)0~=N|dV$Vx3I+M;IP~m}78eKmy=R z_bF3=O#EMd_;^cQlERgd5@$e=gUxIvjGNs=WiB65rtA^HfUA_H2}KpDJRX6I!;G3x zs8UaRVhW%6L?boDAkPCe!GNEHB5(j0of=NFlMho307X%shOUm3)M)@jBkCommBE7l z0kPyjS=cHOWpF_pvD7##X>YYwvvqb1pM(t*e zrG0II$RfDw4X_#8o60tb=Uua%Byb=bk}4K7zt{EK zKcV#?01NoSjB71#cRL0I##g@Q>=1+B(YYc)_@JFVqS5waVGO&tU>dG%f!pw1t0qgq z(5&w^?kl$jMwo&_7_mE0(&85z*aEFg}*DP+z`w#PvGG5bo_-2a%*TyT!FoaapEI@|fqc+RuAy6jk(SX2tMO@eyR z@?3UGQ_aFi9#A9lS;N-J^-1Q6D-j04pUabpmQfDWR3;W>o6vPVCU zPW3mp(P=r1%N3ewvb_=+1qxXD)|2jm(qNNBRr|UK-{tYIBeBR~8~ee*IPwzS<~%tBB5+Sty*v$M_ZZgatyKk~M?$L%v_lKb4~HgWUP&2DzDlGN>vceagb RV|m*<+V7V2y~z*+06TypDp&vj literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..88507b62508f6bdd52322643eeb0eea7298c3c92 GIT binary patch literal 399 zcmZ?wbhEHbWMt50yvo49!NI}F$;r;n&dbZo$jHde&CSNf#>K_O!^0ybCB?_br>v|j zARr(qDap^zFE1}IC@3f)At53nA}uW~CnqN>D=RE4EG{mtsHn)q#3UvrCL|;zDk`d^ zq{Ph3%)-K=qM{-rBg4wds-U2ts;a7^qob{@t)ZczrKP2&rpCa)psA^;uC8umWTdOB z3*?dt6o0aSwdjCIke?XX);KgfI*2gzH8dC%WQfgUZ7Fe()mYc3lYF|N!aSin!s*!z z(-+-Vhw^p^e>jpNw<`SIlt0P#uaf`CHgY#}E3~O}Xm)Ap^s4u(YfsXcqM#-UQ4pPd;d;L%aOqv0AYbwuK)l5 literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/img02.jpg b/themes/stargazer/public/images/img02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ea1cc21a8f951846cb3929f486cb2187a6758f00 GIT binary patch literal 23025 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 literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/img03.jpg b/themes/stargazer/public/images/img03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f2647c4f8ef51cbd9a5978603064c1b44eed5e14 GIT binary patch literal 8052 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 literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/img04.jpg b/themes/stargazer/public/images/img04.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3b48a29d967a8fc541dfc1781ff58740bd466639 GIT binary patch literal 11048 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 literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/img06.gif b/themes/stargazer/public/images/img06.gif new file mode 100644 index 0000000000000000000000000000000000000000..ac11e60ed181d5128550574a4dd652a3fa1537fb GIT binary patch literal 667 zcmZ?wbhEHbRA)3|xXJ(m^78VouC59S3e3#RYHDhFdU_@%CieFBhK7cmoSYUG7Dh%! zA|fIzEG(?7tTHk(R#sMKW@dtdf^2MTQc_Y(OiT_A4mLJ6`uh4jJUq_M&YGH<*4EZ4 zDk_$imZGAf{QUf~va;sp=BB2m?Ck6k5)wi}LOMD+N=i!H+}v_{$v5G(*cnnKQXZVcVK8Rn2^JAVoA}-9IN>kR+M9{f9$C{|Ni&?h5}|*9(gWjZnnnmp5DIx2@@wx z&Sm3f(dL{od(PZ>^A}9w)MjU0xO~ORRjVhkvU6mu*|d4f)>X_L;@fub*}JbnoMZpt zBS(*L=o~wB`pgO)iL)0kUG9~Ty>jE`trA(m+xH*b6y$#R^w}wH<>#;8>{eEJ`|;CC z752~Hf6Qj*`T6%>50A-TX%7ScCLUH{t)9t_7Z0^_^Db!c*r?DYE^7R%`k>?DhfB`qM+Ub*tFU&QkFX2#u?foBc?C|Jo}RAu_c?H8r}1r0MVoH9 zyQVHYmsSLO_bznY`ue(?>52y5-P`;`^>SX{lf8UY@!o+-Z4JRaJKhS3^k~=nK0Ns8 d`3iZ92EV(Tn+>mSj#f)>++BV`tciia8UVAO@frXC literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/img07.gif b/themes/stargazer/public/images/img07.gif new file mode 100644 index 0000000000000000000000000000000000000000..c11f25e4b0a7f3e17b0aaf568273edf8fe86ae6d GIT binary patch literal 2214 zcmb`C`!gE|0)XR{s3xi?8m|;ZRn>HRxU_R)Ze2x$HMpptNdsJ2=YSfrKPpCwS|QR1Olyb?Out3_f$_jIFJ$x3{;6 ziHVt+*~N<&O-)TbJUn19*!lD4U0q#WTwEL+96UWey}Y~}9Uae|I|qeAot>TC-Q6!- zxZvdE>)QiZT)Y&^N5+U)T@e^;7uA$kv4Rd2$w4mOCV* zNADl_G&scgJUlWwHqKYxP}}# zBPf{9&n@lz5?K@&=o!QlS&Qgs9DD8bO@%1QB%k>P_HD6CL}xSmz3&giM0RHjw=(db zoZ1K~mR}R{t8|G?#Z4OPL!Vo@BD?U@O}EE%)arrORAe`|41h*=aXaFJ*0{$d1$<_}2~* zv&=KH4%OGW$*uY(eTCOY6?c0EJaLPncWuw$e<0oE)4S6Jj$uP}QT;u##ok|cR`@j< zi?hJ|)9%p&y$TWQ{pBAg2KtoZsiw&K=udR@&O9}Jb>dUMc3(L1+Wou10liW(L;5kG zanzudi@5dQ4GkV2>XbXHlS4yBBkx0*NuIews_>bmH(rU3=-2 zj9)8o$%;4r1ccs%trvM!q^`g6#p2hC{d*+quLH+@Md<5usiKmQExf4orbZ%q6AHYv zQ5NQKf8#B}o3QcDKh4JDeax*(o8`A-?{9vHPbO?uBxY@IRwf0V9|qj3xWDyVDweQS z{h()it0r^&lDPKq+%o?-p^VZ9pU4X=kh# zo1C(+Pv7led6RZKc|kk7)TvvSWnEum(`DWB$s}3NQr3=)CM>wT*Sk@XzSk$gl8k%w z-JYF2s#lJDZ$LGdzW+(LMcN-c((LRH0YLt8##AoF|sGf~aX;sf9X3Nxqq{1tj`FqTPP=Hl)t7hQ= zO{Q7QWM0uOJr+FFF6W3_wJT4wGVN*}C_pEC;h3RYEAna6t)qkYbfU8G0R2)2Dnq|n zmC~l)s?FZhi!p@(21#>e27K8BmT8cZX?unp3Nzqvw@Z*=?3X3EVh;BPw0np94A9jh z`KaTgBL&N+{pf%fynm#e3cq@+`WpA>SUsQ8eymx_-apm~3$LE&Mm>`NdI_%m#IQ@- zKRJ{$uNse3f=9+r^gtWj^c;#CZ~(H@5w^Gi?^d-+!YdRSutxr*0G;3g*~V*FaQ z0-cI#f(lsUL3Bk)Cc4Q*j9|hSjEd46UXz=a#r%sw@g@)43x{ z;0JHZ(9K>x5o~0_!MiG6^Z6MpJ39B^JqFz38y>;=zWSgXAJ$@OUB-!}AAF#oTl`ZZ zxCsjfALx;J0oiQsUBf{o1B?wUjNttYRaUW3*lU$+UNT(y+Y}mm9T&k*OHfwN^RPiQ zHa{a*StA7FLYNUev+H@a5)=+DU{7Sxl^^A3+)Z)BWbT5pPUoL&W?nY=)S#>fK=7d; zv3x6w^GhLdiYJx!sh;ZbWch#z9-zWky zj`O90u4*v`mJm=W$eHQ|6*h!Vh|cEB)EQK`PzW(5ygIJ|s>UOth~HOozGC5OLQDzq zHV!#UOqgvpXAxs*oY}TqH7N-~if1C{I;+*>)F@JdfHT)aSGQ)CkP^j60ewN;mcu9A z)p7)b26cNLgq#G5oF9g2C`D1^pB=gL<8Vz!Sqb@`Pvqa}rkKtuJ~=s<`};($hKhl- zriR~003=p);iFp9;kj?8vB1&}bP7rYUDS<-OmviH^F(fk zI%cSKXIUX?!^2t6LPU3%SMWBxZ|K=ErJdzC)TZAZJtuymvx3Ik4EVR6n`BL`VxqRL e)#!Pt(bQ@IZ!4%@&(AES){0Rn{vZ>;jsF6jhn++K literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/images/spacer.gif b/themes/stargazer/public/images/spacer.gif new file mode 100644 index 0000000000000000000000000000000000000000..5bfd67a2d6f72ac3a55cbfcea5866e841d22f5d9 GIT binary patch literal 43 mcmZ?wbhEHbWMp7uXkdT>#h)yUAf^t80Ld^gF}W}@SOWlZ0R#L1 literal 0 HcmV?d00001 diff --git a/themes/stargazer/public/pylons-logo.gif b/themes/stargazer/public/pylons-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..61b2d9ab369e8d161b5c2fd9e484be28d11b2486 GIT binary patch literal 2399 zcmV-l383~zNk%w1VLJgt0M!Ekii(Q!^YeOnbtx$+T3TAy*Vi2#9ipP5=;-K3NJtbD z6eT4k#>U3-%rYS%A)k<085tS2wzh6zD6Nz%JUlzzzB!G1F2bonA0QtxGBGG9Crm^p z8yp)P8yp)N8XFuO92^`P8X6-bA|4(dA0HnlCnqK*CL$stARr(hA0Qte9w;a%BO@au zBqb*(C?q2zBqSs#Cnpsa78n^B7Z(>77#9~878n>95D*X&5)vvZDjORcD=RB+Zf^h9 zF#rGmA^8LW00093EC2ui06PIh000L6z=3c`EE78iP# zn0OZ!j&TG(prN9pq@V}~J%5r;2?PQHiUg~(kan53m<BnYCEvX<`=hwL5I_sL=bWM4J%JH}Gro)>@?PEuf77zjY_qfas{D|ITB zq1augaDFaF?Fo&({hjgvz72@C>7gsDUZWR^*p5^5?s z3Xw#->F5thI-yma3^mY2oDG?a}kf@GE2>J`5O7z%Tq2VN9tgFI6Iz*$;N*iLCXHI+Twz3TY<*pn3 zltC-C5`g1v?hw-d;5(HDVww_glF}}Z%^qJRIoD5HoMC&YZ41_@+dkt9gM^pi|n*}M)VLbcy15MO9E1xSwxU#b{YT!Esjm| z17I)oK-V=tfwN2`-dl<{fQ!;Lv-KEJxhNwgkwMY)8Syb+MkxEY)ru*`xUF<1KFtNI zcl+?!pafn2#N7%4+jl7E3Y~coyXV`)=Apbd`Y19+ zf>l)mMv7#pLMAwoIC(3D?TDAdKq<;!KV;lFJXnDZHco_bDo!k{fWR)1>R3xKVDQq% z1PW3AO-nPm;r6m9q=A{S2LkY51N3LcBEBd92}ooDis*zl{=!71iX-?2pp6LDF&KAr zq`dU_pCk0hY4!*s<2vX%5Ehbu%M-&DDJ8=$dUBH5pyLvVr$SARVv9KBq*M^8!9PN; zl)jW76An3s8g4^@E0kgp-WLTIzN!ee6a_B#_)0~=N|dV$Vx3I+M;IP~m}78eKmy=R z_bF3=O#EMd_;^cQlERgd5@$e=gUxIvjGNs=WiB65rtA^HfUA_H2}KpDJRX6I!;G3x zs8UaRVhW%6L?boDAkPCe!GNEHB5(j0of=NFlMho307X%shOUm3)M)@jBkCommBE7l z0kPyjS=cHOWpF_pvD7##X>YYwvvqb1pM(t*e zrG0II$RfDw4X_#8o60tb=Uua%Byb=bk}4K7zt{EK zKcV#?01NoSjB71#cRL0I##g@Q>=1+B(YYc)_@JFVqS5waVGO&tU>dG%f!pw1t0qgq z(5&w^?kl$jMwo&_7_mE0(&85z*aEFg}*DP+z`w#PvGG5bo_-2a%*TyT!FoaapEI@|fqc+RuAy6jk(SX2tMO@eyR z@?3UGQ_aFi9#A9lS;N-J^-1Q6D-j04pUabpmQfDWR3;W>o6vPVCU zPW3mp(P=r1%N3ewvb_=+1qxXD)|2jm(qNNBRr|UK-{tYIBeBR~8~ee*IPwzS<~%tBB5+Sty*v$M_ZZgatyKk~M?$L%v_lKb4~HgWUP&2DzDlGN>vceagb RV|m*<+V7V2y~z*+06TypDp&vj literal 0 HcmV?d00001 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 @@ +