Commits

Anonymous committed 5298e63 Merge

merge with head

Comments (0)

Files changed (127)

-Copyright (c) 2009 by the Zine Team, see AUTHORS for more details.
+Copyright (c) 2010 by the Zine Team, see AUTHORS for more details.
 
 Some rights reserved.
 
 
     Simple configure script that creates a makefile.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os

external-plugins/README

-This folder contains plugins that were previously part of zine
-but are not longer distributed as part for Zine for different
-reasons.
+This folder contains plugins that were previously part of Zine
+but are not longer distributed as part for Zine for various reasons.

external-plugins/dark_vessel_colorscheme/__init__.py

 
     A dark colorscheme for vessel.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os.path import join, dirname

external-plugins/dark_vessel_colorscheme/metadata.txt

 Author: Armin Ronacher <armin.ronacher@active-4.com>
 Author URL: http://lucumr.pocoo.org/
 License: BSD
-Version: 0.1
+Version: 0.2
 Description: A dark colorscheme for the vessel theme
 Description[de]: Ein dunkles Farbschema für das Vessel-Design
 Depends: vessel_theme

external-plugins/eric_the_fish/__init__.py

     plugin because it uses quite a lot of the internal signaling and
     registration system.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os.path import dirname, join

external-plugins/eric_the_fish/docs/en/index.rst

 this time for documenting plugins.  You can build the docs from source
 using the ``build-documentation`` script distributed with Zine::
 
-	$ ./scripts/build-documentation zine/plugins/eric_the_fish
+    $ ./scripts/build-documentation zine/plugins/eric_the_fish
 
 After that, it shows up in the plugin section of the online help (that's
 what you're looking at right now).

external-plugins/eric_the_fish/fortunes.py

     Erics fortune cookies (ripped and stripped from the ubuntu `fortune`
     fortune data file.)
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

external-plugins/eric_the_fish/metadata.txt

 Author: Armin Ronacher <armin.ronacher@active-4.com>
 Author URL: http://lucumr.pocoo.org/
 License: BSD
-Version: 0.1
+Version: 0.2
 Description: Adds Eric The Fish, an annoying fish to the admin panel. Clicking\
  it shows a random fortune quote. This is a documented example plugin.
 Description[de]: Fügt Erik, einen nervigen Fisch, zum Administrationsbereich hinzu.\

external-plugins/eric_the_fish/shared/fish.js

  * new div for the fish and a second one for the bubble. Then it assigns some
  * classes and registeres a click action for the fish that sends a request.
  *
- * :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+ * :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
  * :license: BSD, see LICENSE for more details.
  */
 

external-plugins/markdown_parser/__init__.py

     TODO: this parser does not support `<intro>` sections and has a
           very bad implementation as it requires multiple parsing steps.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os.path
 import re
 from zine.api import *
 from zine.parsers import BaseParser
-from zine.views.admin import flash, render_admin_response
-from zine.privileges import BLOG_ADMIN, require_privilege
+from zine.views.admin import render_admin_response
+from zine.privileges import BLOG_ADMIN
+from zine.utils.admin import flash, require_admin_privilege
 from zine.utils.zeml import parse_html
 from zine.utils import forms
 try:
                     u'lines above and below to cut the post at that point.'))
 
 
-@require_privilege(BLOG_ADMIN)
+@require_admin_privilege(BLOG_ADMIN)
 def show_markdown_config(req):
     """Show Markdown Parser configuration options."""
     form = ConfigurationForm(initial=dict(

external-plugins/markdown_parser/metadata.txt

 Author: Armin Ronacher <armin.ronacher@active-4.com>
 Author URL: http://lucumr.pocoo.org/
 License: BSD
-Version: 0.1
+Version: 0.2
 Contributors: Kiran Jonnalagadda <jace@pobox.com>
 Description: This plugin allows you to use Markdown for your posts.
 Description[de]: Dieses Plugin erlaubt dir Markdown für die Post-\

scripts/_init_zine.py

 
     Helper to locate zine and the instance folder.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os.path import abspath, join, dirname, pardir, isfile

scripts/_install-posix.py

 
     This script is invoked by the makefile to install Zine on a POSIX system.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/_make-setup-virtualenv.py

 
     Execute this file to regenerate the `setup-virtualenv` script.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os

scripts/add-translation

 
     This script adds a new translation to Zine or a Zine plugin.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os import makedirs

scripts/build-documentation

 
     This command builds the documentation for Zine or a plugin.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/build-event-map

 
     Lists all the events send in a given Zine installation.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/bundle-plugin

 
     The file created can be used to distribute the plugin.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/compile-translations

     We do not use standard MO files because we have to store additional
     information in those files.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import pickle

scripts/create-apache-config

 
     This creates an apache config for static exports.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/create-package

     This script dumps Zine with all files required files into a .tar.gz
     and .zip archive for distribution.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re

scripts/extract-messages

 
     Extract messages into a PO-Template.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os import path, makedirs
     '_': None,
     'gettext': None,
     'ngettext': (1, 2),
+    'l_': None,
     'lazy_gettext': None,
     'lazy_ngettext': (1, 2)
 }

scripts/generate-translit-tab

     You will need a version of transtab which you can get for example
     here: http://www.bitbucket.org/jek/translitcodec/
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os

scripts/new-plugin

 
     This script asks a few questions to create a plugin skeleton
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re
 
     Plugin implementation description goes here.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

scripts/regenerate-post-uids

       Old postings still contain the old uids though, so
       they need to be regenerated.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

scripts/reset-instance

 
     This script resets the development instance.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/run-tests

     This is a wrapper script for running the Zine unittests.
     Run it with the --help option for usage information.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os
 
     This script opens a development server for Zine.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys
 
     This script opens a shell for Zine.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys

scripts/update-translations

 
     Update the translations from the POT.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os import path, listdir, rename
     the wsgiref module installed.  For help con configuration
     have a look at the README file.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

servers/zine.fcgi

 
     For help on configuration have a look at the README file.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

servers/zine.wsgi

     Run Zine in mod_wsgi.  For help on configuration have a look at the
     README file.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

tests/__init__.py

     be patched to remove this incompatibility, the patch is at
     http://tinyurl.com/doctest-patch
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 
         application = get_wsgi_app('/path/to/instance')
 
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 __version__ = '0.2-dev'
 
     Internal core module that survives reloads.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 
     ongoing reloads.  If funky things occur and these do not resolve
     after `timeout` seconds a `RuntimeError` is raised.
     """
-    global _application
+    global _application, _setup_failed
     _setup_failed = False
     _setup_lock.acquire()
     try:
 
 def _unload_zine():
     """Unload all zine libraries."""
-    global _application
+    global _application, _setup_failed
     import sys
 
     _setup_lock.acquire()
                 try:
                     for key, value in module.__dict__.iteritems():
                         setattr(module, key, None)
+                    # clear references
                     value = None
                 except:
                     pass
     core module.  You have been warned.
     """
     # the reloader eats import errors, so make sure that the application
-    # properly before we create our proxy application.
+    # imports properly before we create our proxy application.
     import zine.application
 
     _dispatch_lock = allocate_lock()

zine/_dynamic/__init__.py

     The module is nonpublic but all the important constants are imported
     in some internal utility modules.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """

zine/_dynamic/translit_tab.py

     The map used for zine.utils.text unicode transliteration.  This
     file is automatically generated by the `generate-translit-tab`.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

zine/_ext/__init__.py

     -   pottymouth: used by the text parser to convert arbitary text into
         nice looking HTML as ZEML tree.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """

zine/_ext/pottymouth.py

 
     # The following are simple, context-independent replacement tokens
     TokenMatcher('EMDASH'  , r'(--)'    , replace=unichr(8212)),
-    # No way to reliably distinguish Endash from Hyphen, Dash & Minus, 
+    # No way to reliably distinguish Endash from Hyphen, Dash & Minus,
     # so we don't.  See: http://www.alistapart.com/articles/emen/
 
     TokenMatcher('ELLIPSIS', r'(\.\.\.)', replace=unichr(8230)),
     Replacer(r'(``)', unichr(8220)),
     Replacer(r"('')", unichr(8221)),
 
-    # First we look for inter-word " and ' 
+    # First we look for inter-word " and '
     Replacer(r'(\b"\b)', unichr(34)), # double prime
     Replacer(r"(\b'\b)", unichr(8217)), # apostrophe
-    # Then we look for opening or closing " and ' 
-    Replacer(r'(\b"\B)', unichr(8221)), # close double quote 
+    # Then we look for opening or closing " and '
+    Replacer(r'(\b"\B)', unichr(8221)), # close double quote
     Replacer(r'(\B"\b)', unichr(8220)), # open double quote
     Replacer(r"(\b'\B)", unichr(8217)), # close single quote
     Replacer(r"(\B'\b)", unichr(8216)), # open single quote
 
-    # Then we look for space-padded opening or closing " and ' 
+    # Then we look for space-padded opening or closing " and '
     Replacer(r'(")(\s)', unichr(8221)+r'\2'), # close double quote
-    Replacer(r'(\s)(")', r'\1'+unichr(8220)), # open double quote 
+    Replacer(r'(\s)(")', r'\1'+unichr(8220)), # open double quote
     Replacer(r"(')(\s)", unichr(8217)+r'\2'), # close single quote
     Replacer(r"(\s)(')", r'\1'+unichr(8216)), # open single quote
 
                  all_lists=True,      # disables all lists (<ol> and <ul>)
                  unordered_list=True, # disables all unordered lists (<ul>)
                  ordered_list=True,   # disables all ordered lists (<ol>)
-                 numbered_list=True,  # disables '\d+\.' lists 
+                 numbered_list=True,  # disables '\d+\.' lists
                  blockquote=True,     # disables '>' <blockquote>s
                  bold=True,           # disables *bold*
                  italic=True,         # disables _italics_
             elif n == 'YOUTUBE' and not youtube:                       continue
             elif n == 'EMAIL' and not email:                           continue
             elif n in ('HASH','DASH','NUMBERDOT','ITEMSTAR','BULLET') and not all_lists:
-                continue 
+                continue
             elif n in ('DASH','ITEMSTAR','BULLET') and not unordered_list:
                 continue
             elif n in ('HASH','NUMBERDOT') and not ordered_list:
                     content = m.groups()[0]
                     p += len(content)
 
-                    if tm.replace is not None: 
+                    if tm.replace is not None:
                         unmatched_collection += tm.replace
                         break
 
                 pass
             else:
                 finished.append(top)
-            
+
         return finished
 
 
         """Parse bold and italic and other balanced items"""
         stack = []
         finished = []
-        
+
         last_bold_idx = -1
         last_ital_idx = -1
 
         for b in blocks:
             nb = self._parse_block(b)
             parsed_blocks.append(nb)
-        
+
         return parsed_blocks
 
 
     Module for plugins and core. Star import this to get
     access to all the important helper functions.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 

zine/application.py

     and a couple of helper functions and classes.
 
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import sys
 from os import path, remove, makedirs, walk, environ
 from time import time
-from itertools import izip
-from datetime import datetime, timedelta
-from urlparse import urlparse, urljoin
+from urlparse import urlparse
 from collections import deque
 from inspect import getdoc
 from traceback import format_exception
 
 
 class Theme(object):
-    """Represents a theme and is created automaticall by `add_theme`"""
+    """Represents a theme and is created automatically by `add_theme`."""
     app = None
 
     def __init__(self, name, template_path, metadata=None,
 
     @property
     def description(self):
-        """Return the description of the plugin."""
+        """Return the description of the theme."""
         return self.metadata.get('description', u'')
 
     @property
 
     @property
     def author_info(self):
-        """The author, mail and author URL of the plugin."""
+        """The author, mail and author URL of the theme."""
         from zine.utils.mail import split_email
         return split_email(self.metadata.get('author', u'Nobody')) + \
                (self.metadata.get('author_url'),)
 
     @property
     def author_email(self):
-        """Return the author email address of the plugin."""
+        """Return the author email address of the theme."""
         return self.author_info[1]
 
     @property
     def author_url(self):
-        """Return the URL of the author of the plugin."""
+        """Return the URL of the author of the theme."""
         return self.author_info[2]
 
     @cached_property
 class Zine(object):
     """The central application object.
 
-    Even though the :class:`Zine` class is a regular Python class, you
-    can't create instances by using the regular constructor.  The only
-    documented way to create this class is the :func:`make_zine`
-    function or by using one of the dispatchers created by :func:`make_app`.
+    Even though the :class:`Zine` class is a regular Python class, you can't
+    create instances by using the regular constructor.  The only documented way
+    to create this class is the :func:`zine._core.setup` function or by using
+    one of the dispatchers created by :func:`zine._core.get_wsgi_app`.
     """
 
     _setup_only = []
         return f
 
     def __init__(self, instance_folder):
-        # this check ensures that only make_app can create Zine instances
+        # this check ensures that only setup() can create Zine instances
         if get_application() is not self:
             raise TypeError('cannot create %r instances. use the '
-                            'make_zine factory function.' %
+                            'zine._core.setup() factory function.' %
                             self.__class__.__name__)
         self.instance_folder = path.abspath(instance_folder)
 
 
     @setuponly
     def add_importer(self, importer):
-        """Register an importer.  For more informations about importers
+        """Register an importer.  For more information about importers
         see the :mod:`zine.importers`.
         """
         importer = importer(self)
         request = get_request()
         javascript = [
             'Zine.ROOT_URL = %s' % dump_json(base_url),
-            'Zine.BLOG_URL = %s' % dump_json(base_url + self.cfg['blog_url_prefix'])
+            'Zine.BLOG_URL = %s' % dump_json(base_url +
+                                             self.cfg['blog_url_prefix'])
         ]
         if request is None or request.user.is_manager:
             javascript.append('Zine.ADMIN_URL = %s' %
-                              dump_json(base_url + self.cfg['admin_url_prefix']))
+                              dump_json(base_url +
+                                        self.cfg['admin_url_prefix']))
         result.append(u'<script type="text/javascript">%s;</script>' %
                       '; '.join(javascript))
 
         for callback in iter_listeners('after-request-setup'):
             result = callback(request)
             if result is not None:
-                return result(environ, start_response)
+                return result
 
         # normal request dispatching
         try:
         returned.
 
         A separate thread is spawned so that the internal request does not
-        caused troubles for the current one in terms of persistent database
+        cause troubles for the current one in terms of persistent database
         objects.
 
         This is for example used in the `open_url` method to allow access to
 # import here because of circular dependencies
 from zine import i18n
 from zine.utils import log
+from zine.utils.net import NetException
 from zine.utils.http import make_external_url
     a binding to memcached.
 
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os
 
 from zine.utils import local
 
-try:
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
-
 
 def get_cache(app):
     """Return the cache for the application.  This is called during the
             response = None
             if use_cache:
                 cache_key = key + request.path.encode('utf-8')
-                response = request.app.cache.get(key)
+                response = request.app.cache.get(cache_key)
 
             if response is None:
                 response = f(request, *args, **kwargs)
     changes the application is reloaded automatically.
 
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os
 
 _dev_mode = environment.MODE == 'development'
 
+l_ = lazy_gettext
+
 #: variables the zine core uses
 DEFAULT_VARS = {
     # core system settings
-    'database_uri':             TextField(default=u'', help_text=lazy_gettext(
+    'database_uri':             TextField(default=u'', help_text=l_(
         u'The database URI.  For more information about database settings '
         u'consult the Zine help.')),
-    'force_https':              BooleanField(default=False, help_text=lazy_gettext(
+    'force_https':              BooleanField(default=False, help_text=l_(
         u'If a request to an http URL comes in, Zine will redirect to the same '
-        u'URL on https if this is savely possible.  This requires a working '
-        u'SSL setup or otherwise Zine will become unresponsive.')),
-    'database_debug':           BooleanField(default=False, help_text=lazy_gettext(
-        u'If enabled the database will collect the SQL statements and add them '
-        u'to the bottom of the page for easier debugging')),
-    'blog_title':               TextField(default=lazy_gettext(u'My Zine Blog')),
-    'blog_tagline':             TextField(default=lazy_gettext(u'just another Zine blog')),
-    'blog_url':                 TextField(default=u'', help_text=lazy_gettext(
+        u'URL on https if this is safely possible.  This requires a working '
+        u'SSL setup, otherwise Zine will become unresponsive.')),
+    'database_debug':           BooleanField(default=False, help_text=l_(
+        u'If enabled, the database will collect all SQL statements and add '
+        u'them to the bottom of the page for easier debugging.')),
+    'blog_title':               TextField(default=l_(u'My Zine Blog')),
+    'blog_tagline':             TextField(default=l_(u'just another Zine blog')),
+    'blog_url':                 TextField(default=u'', help_text=l_(
         u'The base URL of the blog.  This has to be set to a full canonical URL '
-        u'(including http or https).  If not set the application will behave '
+        u'(including http or https).  If not set, the application will behave '
         u'confusingly.  Remember to change this value if you move your blog '
         u'to a new location.')),
-    'blog_email':               TextField(default=u'', help_text=lazy_gettext(
+    'blog_email':               TextField(default=u'', help_text=l_(
         u'The email address given here is used by the notification system to send '
-        u'mails from.  Also plugins that send mails will use this address as '
-        u'sender address.'), validators=[is_valid_email()]),
+        u'emails from.  Also plugins that send mails will use this address as '
+        u'the sender address.'), validators=[is_valid_email()]),
     'timezone':                 ChoiceField(choices=sorted(list_timezones()),
-        default=u'UTC', help_text=lazy_gettext(
+        default=u'UTC', help_text=l_(
         u'The timezone of the blog.  All times and dates in the user interface '
         u'and on the website will be shown in this timezone.  It\'s save to '
-        u'change the timezone after posts were created because the information '
+        u'change the timezone after posts are created because the information '
         u'in the database is stored as UTC.')),
-    'primary_author':           TextField(default=u'', help_text=lazy_gettext(
+    'primary_author':           TextField(default=u'', help_text=l_(
         u'If this blog is written primarily by one author, some themes can ' \
         u'skip the author\'s name on posts unless written by a guest.')),
-    'maintenance_mode':         BooleanField(default=False, help_text=lazy_gettext(
-        u'If set to true the blog enables the maintainance mode.')),
+    'maintenance_mode':         BooleanField(default=False, help_text=l_(
+        u'If set to true, the blog enables the maintainance mode.')),
     'session_cookie_name':      TextField(default=u'zine_session',
-        help_text=lazy_gettext(u'If there are multiple zine installations on '
-        u'the same host the cookie name should be set to something different '
+        help_text=l_(u'If there are multiple Zine installations on '
+        u'the same host, the cookie name should be set to something different '
         u'for each blog.')),
     'theme':                    TextField(default=u'default'),
-    'secret_key':               TextField(default=u'', help_text=lazy_gettext(
-        u'The secret key is used for vairous security related tasks in the '
-        u'system.  For example the cookie is signed with this value.')),
+    'secret_key':               TextField(default=u'', help_text=l_(
+        u'The secret key is used for various security related tasks in the '
+        u'system.  For example, the cookie is signed with this value.')),
     'language':                 ChoiceField(choices=list_languages(False),
                                             default=u'en'),
 
-    'iid':                      TextField(default=u'', help_text=lazy_gettext(
+    'iid':                      TextField(default=u'', help_text=l_(
         u'The iid uniquely identifies the Zine instance.  Currently this '
         u'value is unused, but once set you should not modify it.')),
 
     # log and development settings
     'log_file':                 TextField(default=u'zine.log'),
-    'log_level':                ChoiceField(choices=[(k, lazy_gettext(k)) for k, v
+    'log_level':                ChoiceField(choices=[(k, l_(k)) for k, v
                                                 in sorted(log.LEVELS.items(),
                                                           key=lambda x: x[1])],
                                             default=u'warning'),
     'log_email_only':           BooleanField(default=_dev_mode,
-        help_text=lazy_gettext(u'During development this is helpful to '
+        help_text=l_(u'During development activating this is helpful to '
         u'log emails into a mail.log file in your instance folder instead '
         u'of delivering them to your MTA.')),
     'passthrough_errors':       BooleanField(default=_dev_mode,
-        help_text=lazy_gettext(u'If this is set to true, errors in Zine '
-        u'are not catched so that debuggers can catch it instead.  This is '
+        help_text=l_(u'If this is set to true, errors in Zine '
+        u'are not caught so that debuggers can catch it instead.  This is '
         u'useful for plugin and core development.')),
 
     # url settings
                                           validators=[is_valid_url_prefix()]),
     'post_url_format':          TextField(default=u'%year%/%month%/%day%/%slug%',
                                           validators=[is_valid_url_format()],
-                                          help_text=lazy_gettext(
+                                          help_text=l_(
         u'Use %year%, %month%, %day%, %hour%, %minute% and %second%. '
         u'Changes here will only affect new posts.')),
-    'ascii_slugs':              BooleanField(default=True, help_text=lazy_gettext(
+    'ascii_slugs':              BooleanField(default=True, help_text=l_(
         u'Automatically generated slugs are limited to ASCII')),
     'fixed_url_date_digits':    BooleanField(default=False,
-                                     help_text=lazy_gettext(u'Dates are zero '
+                                     help_text=l_(u'Dates are zero '
                                      u'padded like 2009/04/22 instead of '
                                      u'2009/4/22')),
 
     'enable_eager_caching':     BooleanField(default=False),
     'cache_timeout':            IntegerField(default=300, min_value=10),
     'cache_system':             ChoiceField(choices=[
-        (u'null', lazy_gettext(u'No Cache')),
-        (u'simple', lazy_gettext(u'Simple Cache')),
-        (u'memcached', lazy_gettext(u'memcached')),
-        (u'filesystem', lazy_gettext(u'Filesystem'))
+        (u'null', l_(u'No Cache')),
+        (u'simple', l_(u'Simple Cache')),
+        (u'memcached', l_(u'memcached')),
+        (u'filesystem', l_(u'Filesystem'))
     ], default=u'null'),
     'memcached_servers':        CommaSeparated(TextField(
                                                     validators=[is_netaddr()]),
     # comments and pingback
     'comments_enabled':         BooleanField(default=True),
     'moderate_comments':        ChoiceField(choices=[
-        (0, lazy_gettext(u'Automatically approve all comments')),
-        (1, lazy_gettext(u'An administrator must always approve the comment')),
-        (2, lazy_gettext(u'Automatically approve comments by known comment authors'))
+        (0, l_(u'Automatically approve all comments')),
+        (1, l_(u'An administrator must always approve the comment')),
+        (2, l_(u'Automatically approve comments by known comment authors'))
                                             ], default=1),
+    'comments_open_for':        IntegerField(default=0, help_text=l_(
+        u'The number of days commenting is possible.  If set to zero, comments '
+        u'will be open forever.')),
     'pings_enabled':            BooleanField(default=True),
-    'plaintext_parser_nolinks': BooleanField(default=False, help_text=lazy_gettext(
-        u'If set to true, the plaintext parser will not create links automatically.')),
+    'plaintext_parser_nolinks': BooleanField(default=False, help_text=l_(
+        u'If set to true, the plaintext parser will not create links '
+        u'automatically.')),
 
     # post view
-    'posts_per_page':           IntegerField(default=10, help_text=lazy_gettext(
+    'posts_per_page':           IntegerField(default=10, help_text=l_(
         u'The number of posts that are shown on a page.  This value might not be '
         u'honored by some themes and is probably only used for the index page.')),
     'use_flat_comments':        BooleanField(default=False),
     'smtp_use_tls':             BooleanField(default=False),
 
     # network settings
-    'default_network_timeout':  IntegerField(default=5, help_text=lazy_gettext(
+    'default_network_timeout':  IntegerField(default=5, help_text=l_(
         u'This timeout is used by default for all network related operations. '
         u'The default should be fine for most environments but if you have a '
-        u'very bad network connection during development you should increase it.')),
+        u'very bad network connection during development you should increase '
+        u'it.')),
 
     # plugin settings
     'plugin_guard':             BooleanField(default=not _dev_mode),
     'plugins':                  CommaSeparated(TextField(), default=list),
     'plugin_searchpath':        CommaSeparated(TextField(), default=list,
-        help_text=lazy_gettext(u'It\'s possible to one or more comma '
-        u'separated paths here that are searched for plugins.  If the '
+        help_text=l_(u'It\'s possible to put one or more comma '
+        u'separated paths here that are searched for plugins.  If a path '
         u'is not absolute, it\'s considered relative to the instance '
         u'folder.')),
 
     #admin settings
     'dashboard_reddit':         BooleanField(default=True, help_text=
-        lazy_gettext(u'Set this to true if you want to see the most recent '
-        u'entries on the Zine reddit on your dashbaord.'))
+        l_(u'Set this to true if you want to see the most recent '
+        u'entries on the Zine reddit on your dashboard.'))
 }
 
 HIDDEN_KEYS = set(('iid', 'secret_key', 'blogger_auth_token',
     """Try to convert a value from string or fall back to the default."""
     try:
         return field(value)
-    except ValidationError, e:
+    except ValidationError:
         return field.get_default()
 
 
     zine.database
     ~~~~~~~~~~~~~
 
-    This module is a rather complex layer on top of SQLAlchemy 0.4.
-    Basically you will never use the `zine.database` module except you
-    are a core developer, but always the high level
-    :mod:`~zine.database.db` module which you can import from the
-    :mod:`zine.api` module.
+    This module is a rather complex layer on top of SQLAlchemy.
 
+    Basically you will never use the `zine.database` module except if you
+    are a core developer, but always the high level :mod:`~zine.database.db`
+    module which you can import from the :mod:`zine.api` module.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re
 import os
 import sys
 import time
-import urlparse
 from os import path
-from datetime import datetime, timedelta
 from types import ModuleType
 from copy import deepcopy
 
 from sqlalchemy.orm.interfaces import AttributeExtension
 from sqlalchemy.exc import ArgumentError
 from sqlalchemy.ext.declarative import declarative_base
-from sqlalchemy.util import to_list
 from sqlalchemy.engine.url import make_url, URL
-from sqlalchemy.types import MutableType, TypeDecorator
+from sqlalchemy.types import TypeDecorator
 from sqlalchemy.ext.associationproxy import association_proxy
 
 from werkzeug import url_decode
     def process_bind_param(self, value, dialect):
         if value is None:
             return
-        from zine.utils.zeml import dump_parser_data, RootElement
+        from zine.utils.zeml import dump_parser_data
         return dump_parser_data(value)
 
     def process_result_value(self, value, dialect):
                              local_manager.get_ident)
 
 
+def mapper(cls, *args, **kwargs):
+    """Attaches a query and auto registers."""
+    if not hasattr(cls, 'query'):
+        cls.query = session.query_property(Query)
+    old_init = getattr(cls, '__init__', None)
+    def register_init(self, *args, **kwargs):
+        old_init(self, *args, **kwargs)
+        session.add(self)
+    cls.__init__ = register_init
+    return orm.mapper(cls, *args, **kwargs)
+
+
 # configure a declarative base.  This is unused in the code but makes it easier
 # for plugins to work with the database.
 class ModelBase(object):
     """Internal baseclass for `Model`."""
-Model = declarative_base(name='Model', cls=ModelBase, mapper=session.mapper)
+Model = declarative_base(name='Model', cls=ModelBase, mapper=mapper)
 ModelBase.query = session.query_property(Query)
 
 
 db.create_engine = create_engine
 db.session = session
 db.ZEMLParserData = ZEMLParserData
-db.mapper = session.mapper
+db.mapper = mapper
 db.association_proxy = association_proxy
 db.attribute_loaded = attribute_loaded
 db.AttributeExtension = AttributeExtension

zine/docs/__init__.py

 
     This is separate from the sphinx powered developer documentation.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re

zine/docs/builder.py

     The documentation building system.  This is only used by the
     documentation building script.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re

zine/docs/en/parsers.rst

 
     This is another paragraph.  Paragraphs are usually separated by
     a wider gap in the theme.
-    
+
     -   This is an unordered list.
     -   Of multiple items.
 
 
     This is another paragraph.  Paragraphs are usually separated by
     a wider gap in the theme.
-    
+
     * This is an unordered list.
     * Of multiple items.
 

zine/environment.py

                 templates/core              core templates
                 i18n/                       translations
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from os.path import realpath, dirname, join, pardir, isdir
 
     The form classes the zine core uses.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from copy import copy
 from zine.i18n import _, lazy_gettext, list_languages
 from zine.application import get_application, get_request, emit_event
 from zine.config import DEFAULT_VARS
-from zine.database import db, posts, comments, notification_subscriptions
+from zine.database import db, posts
 from zine.models import User, Group, Comment, Post, Category, Tag, \
      NotificationSubscription, STATUS_DRAFT, STATUS_PUBLISHED, \
      STATUS_PROTECTED, STATUS_PRIVATE, \
 from zine.utils import forms, log, dump_json
 from zine.utils.http import redirect_to
 from zine.utils.validators import ValidationError, is_valid_email, \
-     is_valid_url, is_valid_slug, is_netaddr, is_not_whitespace_only
+     is_valid_url, is_valid_slug, is_not_whitespace_only
 from zine.utils.redirects import register_redirect, change_url_prefix
 
 
         message=lazy_gettext(u'You have to enter a valid URL or omit the field.')
     )])
     body = forms.TextField(lazy_gettext(u'Text'), min_length=2, max_length=6000,
-                           messages=dict(
+                           required=True, messages=dict(
         too_short=lazy_gettext(u'Your comment is too short.'),
         too_long=lazy_gettext(u'Your comment is too long.'),
         required=lazy_gettext(u'You have to enter a comment.')
     def context_validate(self, data):
         if not self.post.comments_enabled:
             raise ValidationError(_('Post is closed for commenting.'))
+        if self.post.comments_closed:
+            raise ValidationError(_('Commenting is no longer possible.'))
 
     def make_comment(self):
         """A handy helper to create a comment from the validated form."""
         self.comment.status = COMMENT_BLOCKED_USER
         self.comment.bocked_msg = msg
 
+
 class MarkCommentForm(_CommentBoundForm):
     """Form used to block comments."""
 
         if self.user.posts.count() is 0:
             data['action'] = None
         if data['action'] == 'reassign' and not data['reassign_to']:
-            # XXX: Bad wording
-            raise ValidationError(_('You have to select the user that '
-                                    'gets the posts assigned.'))
+            raise ValidationError(_('You have to select a user to reassign '
+                                    'the posts to.'))
 
     def delete_user(self):
         """Deletes the user."""
 class DeleteAccountForm(_UserBoundForm):
     """Used for a user to delete a his own account."""
 
+    password = forms.TextField(
+        lazy_gettext(u"Your password is required to delete your account:"),
+        required=True, widget=forms.PasswordInput,
+        messages = dict(required=lazy_gettext(u'Your password is required!'))
+    )
+
     def __init__(self, user, initial=None):
         _UserBoundForm.__init__(self, user, forms.fill_dict(initial,
             action='delete'
         ))
 
+    def validate_password(self, value):
+        if not self.user.check_password(value):
+            raise ValidationError(_(u'Invalid password'))
+
     def delete_user(self):
         """Deletes the user's account."""
         # find all the comments by this author and make them comments that
     moderate_comments = config_field('moderate_comments',
                                      lazy_gettext(u'Comment Moderation'),
                                      widget=forms.RadioButtonGroup)
+    comments_open_for = config_field('comments_open_for',
+        label=lazy_gettext(u'Comments Open Period'))
     pings_enabled = config_field('pings_enabled',
         lazy_gettext(u'Pingbacks enabled'),
         help_text=lazy_gettext(u'enable pingbacks per default'))
                 parent = newparent
             else:
                 parent = None
-    # One could probably optimize this by tracking the amount of deleted
-    # comments
+    # XXX: one could probably optimize this by tracking the amount
+    # of deleted comments
     comment.post.sync_comment_count()
 
 

zine/i18n/__init__.py

 
     New languages are added with `add-translation`.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os
 from babel import Locale, dates, UnknownLocaleError
 from babel.support import Translations as TranslationsBase
 from pytz import timezone, UTC
-from werkzeug.exceptions import NotFound
 
 import zine
 from zine.environment import LOCALE_PATH, LOCALE_DOMAIN, \

zine/importers/__init__.py

     API as well as some core importers we implement as part of the software
     and not as plugin.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import os
     """
     if isinstance(app, basestring):
         from zine import setup
-        app = make_zine(app)
+        app = setup(app)
 
     blog = load_import_dump(app, id)
     callback(blog)
     #: and internal addressing.
     name = None
 
+    #: an explanation of what this importer can import.
+    description = None
+
     @property
     def title(self):
         return self.name.title()

zine/importers/feed.py

     This importer can import web feeds.  Currently it is limited to ATOM
     plus optional Zine extensions.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from pickle import loads
 
     def __init__(self, tree):
         Parser.__init__(self, tree)
-        self.global_author = None 
+        self.global_author = None
 
         # use for the category fallback handling if no extension
         # takes over the handling.
 
     def parse(self):
         # atom allows the author to be defined for the whole feed
-        # before the entries.  Capture it here. 
+        # before the entries.  Capture it here.
         self.global_author = self.tree.find(atom.author)
-        
+
         for entry in self.tree.findall(atom.entry):
             post = self.parse_post(entry)
             if post is not None:
 
         author = entry.find(atom.author)
         if author is None:
-            author = self.global_author 
+            author = self.global_author
         email = author.findtext(atom.email)
         username = author.findtext(atom.name)
         uri = author.findtext(atom.uri)
 class FeedImporter(Importer):
     name = 'feed'
     title = lazy_gettext(u'Feed Importer')
+    description = lazy_gettext(u'Handles ATOM feeds with optional extensions '
+                               u'such as those exported by Zine itself. '
+                               u'Plugins can add further extensions to be '
+                               u'recognized by this importer.')
 
     def configure(self, request):
         form = FeedImportForm()
                 try:
                     feed = open_url(form.data['download_url']).stream
                 except Exception, e:
-                    error = _(u'Error downloading from URL: %s') % e
-            elif not feed:
+                    log.exception(_('Error downloading feed'))
+                    flash(_(u'Error downloading from URL: %s') % e, 'error')
+            if not feed:
                 return redirect_to('import/feed')
 
             try:
                               _parser_data(element.findtext(zine.parser_data)))
             comments[int(element.attrib['id'])] = comment
             parent = element.findtext(zine.parent)
-            if parent is not None:
+            if parent:
                 unresolved_parents[comment] = int(parent)
 
         for comment, parent_id in unresolved_parents.iteritems():

zine/importers/wordpress.py

 
     Implements an importer for WordPress extended RSS feeds.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 import re
 from zine.importers import Importer, Blog, Tag, Category, Author, Post, Comment
 from zine.i18n import lazy_gettext, _
 from zine.utils import log
-from zine.utils.validators import is_valid_url
 from zine.utils.admin import flash
 from zine.utils.xml import Namespace, html_entities, escape
 from zine.utils.zeml import parse_html, inject_implicit_paragraphs
 class WordPressImporter(Importer):
     name = 'wordpress'
     title = 'WordPress'
+    description = lazy_gettext(u'Handles import of WordPress "extended RSS" '
+                               u' feeds.')
 
     def configure(self, request):
         form = WordPressImportForm()
                 try:
                     dump = open_url(form.data['download_url']).stream
                 except Exception, e:
-                    error = _(u'Error downloading from URL: %s') % e
-            elif not dump:
+                    log.exception(_('Error downloading feed'))
+                    flash(_(u'Error downloading from URL: %s') % e, 'error')
+            if not dump:
                 return redirect_to('import/wordpress')
 
             try:
 
     The core models and query helper functions.
 
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
     :license: BSD, see LICENSE for more details.
 """
 from math import log
 from zine.utils.pagination import Pagination
 from zine.utils.crypto import gen_pwhash, check_pwhash
 from zine.utils.http import make_external_url
-from zine.privileges import Privilege, _Privilege, privilege_attribute, \
+from zine.privileges import _Privilege, privilege_attribute, \
      add_admin_privilege, MODERATE_COMMENTS, ENTER_ADMIN_PANEL, BLOG_ADMIN, \
      VIEW_DRAFTS, VIEW_PROTECTED, MODERATE_OWN_ENTRIES, MODERATE_OWN_PAGES
 from zine.application import get_application, get_request, url_for
 
     def set_auto_slug(self):
         """Generate a slug for this post."""
-        cfg = get_application().cfg
+        #cfg = get_application().cfg
         slug = gen_slug(self.title)
         if not slug:
             slug = to_blog_timezone(self.pub_date).strftime('%H%M')
             uid = build_tag_uri(app, self.pub_date, content_type, self.slug)
         self.uid = uid
 
+    @property
+    def comments_closed(self):
+        """True if commenting is no longer possible."""
+        app = get_application()
+        open_for = app.cfg['comments_open_for']
+        if open_for == 0:
+            return False
+        return self.pub_date + timedelta(days=open_for) < datetime.utcnow()
+
 
 class SummarizedPost(_PostBase):
     """Like a regular post but without text and parser data."""
         request = get_request()
         if user is None:
             user = request.user
-        if user.has_privilege(MODERATE_OWN_ENTRIES | MODERATE_OWN_PAGES):
-            # Comment belongs to a post the user is the author. It's visible.
-            return self.post.author is user
+        if self.post.author is user and \
+           user.has_privilege(MODERATE_OWN_ENTRIES | MODERATE_OWN_PAGES):
+            return True
         elif user.has_privilege(MODERATE_COMMENTS):
             # User is able to manage comments. It's visible.
             return True

zine/notifications.py

-# -*- coding: utf-8 -*-
-"""
-    zine.notifications
-    ~~~~~~~~~~~~~~~~~~
-
-    This module implements an extensible notification system.  Plugins can
-    provide different kinds of notification systems (like email, jabber etc.)
-
-    Each user can subscribe to different kinds of events.  The general design
-    is inspired by Growl.
-
-    :copyright: (c) 2009 by the Zine Team, see AUTHORS for more details.
-    :license: BSD, see LICENSE for more details.
-"""
-from datetime import datetime
-from urlparse import urlsplit
-
-from werkzeug import url_unquote
-
-from zine.models import NotificationSubscription, User
-from zine.application import get_application, get_request, render_template
-from zine.privileges import BLOG_ADMIN, ENTER_ACCOUNT_PANEL, MODERATE_COMMENTS,\
-     MODERATE_OWN_PAGES, MODERATE_OWN_ENTRIES
-from zine.utils.zeml import parse_zeml, escape
-from zine.utils.mail import send_email
-from zine.i18n import lazy_gettext, _
-
-
-__all__ = ['DEFAULT_NOTIFICATION_TYPES', 'NotificationType']
-
-DEFAULT_NOTIFICATION_TYPES = {}
-
-
-def send_notification(type, message, user=Ellipsis):
-    """Convenience function.  Get the application object and deliver the
-    notification to it's NotificationManager.
-
-    The message must be a valid ZEML formatted message.  The following
-    top-level elements are available for marking up the message:
-
-    title
-        The title of the notification.  Some systems may only transmit this
-        part of the message.
-
-    summary
-        An optional quick summary.  If the text is short enough it can be
-        omitted and the system will try to transmit the longtext in that
-        case.  The upper limit for the summary should be around 100 chars.
-
-    details
-        If given this may either contain a paragraph with textual information
-        or an ordered or unordered list of text or links.  The general markup
-        rules apply.
-
-    longtext
-        The full text of this notification.  May contain some formattings.
-
-    actions
-        If given this may contain an unordered list of action links.  These
-        links may be transmitted together with the notification.
-
-    Additionally if there is an associated page with the notification,
-    somewhere should be a link element with a "selflink" class.  This can be
-    embedded in the longtext or actions (but any other element too).
-
-    Example markup::
-
-        <title>New comment on "Foo bar baz"</title>
-        <summary>Mr. Miracle wrote a new comment: "This is awesome."</summary>
-        <details>
-          <ul>
-            <li><a href="http://miracle.invalid/">Mr. Miracle</a>
-            <li><a href="mailto:mr@miracle.invalid">E-Mail</a>
-          </ul>
-        </details>
-        <longtext>
-          <p>This is awesome.  Keep it up!
-          <p>Love your work
-        </longtext>
-        <actions>
-          <ul>
-            <li><a href="http://.../link" class="selflink">all comments</a>
-            <li><a href="http://.../?action=delete">delete it</a>
-            <li><a href="http://.../?action=approve">approve it</a>
-          </ul>
-        </actions>
-
-    Example plaintext rendering (e-mail)::
-
-        Subject: New comment on "Foo bar baz"
-
-        Mr. Miracle             http://miracle.invalid/
-        E-Mail                  mr@mircale.invalid
-
-        > This is awesome.   Keep it up!
-        > Love your work.
-
-        Actions:
-          - delete it           http://.../?action=delete
-          - approve it          http://.../?action=approve
-
-    Example IM notification rendering (jabber)::
-
-        New comment on "Foo bar baz."  Mr. Miracle wrote anew comment:
-        "This is awesome".  http://.../link
-    """
-    get_application().notification_manager.send(
-        Notification(type, message, user)
-    )
-
-
-def send_notification_template(type, template_name, user=Ellipsis, **context):
-    """Like `send_notification` but renders a template instead."""
-    notification = render_template(template_name, **context)
-    send_notification(type, notification, user)
-
-
-class NotificationType(object):
-    """There are different kinds of notifications. E.g. you want to
-    send a special type of notification after a comment is saved.
-    """
-
-    def __init__(self, name, description, privileges):
-        self.name = name
-        self.description = description
-        self.privileges = privileges
-
-    def __repr__(self):
-        return '<%s %r>' % (self.__class__.__name__, self.name)
-
-
-class Notification(object):
-    """A notification that can be sent to a user. It contains a message.
-    The message is a zeml construct.
-    """
-
-    def __init__(self, id, message, user=Ellipsis):
-        self.message = parse_zeml(message, 'system')
-        self.id = id
-        self.sent_date = datetime.utcnow()
-        if user is Ellipsis:
-            self.user = get_request().user
-        else:
-            self.user = user
-
-    @property
-    def self_link(self):
-        link = self.message.query('a[class~=selflink]').first
-        if link is not None:
-            return link.attributes.get('href')
-
-    title = property(lambda x: x.message.query('/title').first)
-    details = property(lambda x: x.message.query('/details').first)
-    actions = property(lambda x: x.message.query('/actions').first)
-    summary = property(lambda x: x.message.query('/summary').first)
-    longtext = property(lambda x: x.message.query('/longtext').first)
-
-
-class NotificationSystem(object):
-    """Use this as a base class for specific notification systems such as
-    `JabberNotificationSystem` or `EmailNotificationSystem`.
-
-    The class must implement a method `send` that receives a notification
-    object and a user object as parameter and then sends the message via
-    the specific system.  The plugin is itself responsible for extracting the
-    information necessary to send the message from the user object.  (Like
-    extracting the email address).
-    """
-
-    def __init__(self, app):
-        self.app = app
-
-    #: subclasses have to overrides this as class attributes.
-    name = None
-    key = None
-
-    def send(self, user, notification):
-        raise NotImplementedError()
-
-
-class EMailNotificationSystem(NotificationSystem):
-    """Sends notifications to user via E-Mail."""
-
-    key = 'email'
-    name = lazy_gettext(u'E-Mail')
-
-    def send(self, user, notification):
-        title = u'[%s] %s' % (
-            self.app.cfg['blog_title'],
-            notification.title.to_text()
-        )
-        text = self.mail_from_notification(notification)
-        send_email(title, text, [user.email])
-
-    def unquote_link(self, link):
-        """Unquotes some kinds of links.  For example mailto:foo links are
-        stripped and properly unquoted because the mails we write are in
-        plain text and nobody is interested in URLs there.
-        """
-        scheme, netloc, path = urlsplit(link)[:3]
-        if scheme == 'mailto':
-            return url_unquote(path)
-        return link
-
-    def collect_list_details(self, container):
-        """Returns the information collected from a single detail list item."""
-        for item in container.children:
-            if len(item.children) == 1 and item.children[0].name == 'a':
-                link = item.children[0]
-                href = link.attributes.get('href')
-                yield dict(text=link.to_text(simple=True),
-                           link=self.unquote_link(href), is_textual=False)
-            else:
-                yield dict(text=item.to_text(multiline=False),
-                           link=None, is_textual=True)
-
-
-    def find_details(self, container):
-        # no container given, nothing can be found
-        if container is None or not container.children:
-            return []
-
-        result = []
-        for child in container.children:
-            if child.name in ('ul', 'ol'):
-                result.extend(self.collect_list_details(child))
-            elif child.name == 'p':
-                result.extend(dict(text=child.to_text(),
-                                   link=None, is_textual=True))
-        return result
-
-    def find_actions(self, container):
-        if not container:
-            return []
-        ul = container.query('/ul').first
-        if not ul:
-            return []
-        return list(self.collect_list_details(ul))
-
-    def mail_from_notification(self, message):
-        title = message.title.to_text()
-        details = self.find_details(message.details)
-        longtext = message.longtext.to_text(collect_urls=True,
-                                            initial_indent=2)
-        actions = self.find_actions(message.actions)
-        return render_template('notifications/email.txt', title=title,
-                               details=details, longtext=longtext,
-                               actions=actions)
-
-
-class NotificationManager(object):
-    """The NotificationManager is informed about new notifications by the
-    send_notification function. It then decides to which notification
-    plugins the notification is handed over by looking up a database table
-    in the form:
-
-        user_id  | notification_system | notification id
-        ---------+---------------------+--------------------------
-        1        | jabber              | NEW_COMMENT
-        1        | email               | ZINE_UPGRADE_AVAILABLE
-        1        | sms                 | SERVER_EXPLODED
-
-    The NotificationManager also assures that only users interested in
-    a particular type of notifications receive a message.
-    """
-
-    def __init__(self):
-        self.systems = {}
-        self.notification_types = DEFAULT_NOTIFICATION_TYPES.copy()
-
-    def send(self, notification):
-        # given the type of the notification, check what users want that
-        # notification; via what system and call the according
-        # notification system in order to finally deliver the message
-        subscriptions = NotificationSubscription.query.filter_by(
-            notification_id=notification.id.name
-        )
-        if notification.user:
-            subscriptions = subscriptions.filter(
-                NotificationSubscription.user!=notification.user
-            )
-
-        for subscription in subscriptions.all():
-            system = self.systems.get(subscription.notification_system)
-            if system is not None:
-                system.send(subscription.user, notification)
-
-    def types(self, user=None):
-        if not user:
-            user = get_request().user
-        for notification in self.notification_types.itervalues():
-            if user.has_privilege(notification.privileges):
-                yield notification
-
-    def add_notification_type(self, notification):
-        self.notification_types[type.name] = type
-
-
-def _register(name, description, privileges=ENTER_ACCOUNT_PANEL):
-    """Register a new builtin type of notifications."""
-    nottype = NotificationType(name, description, privileges)
-    DEFAULT_NOTIFICATION_TYPES[name] = nottype
-    globals()[name] = nottype
-    __all__.append(name)
-
-
-_register('NEW_COMMENT',
-          lazy_gettext(u'When a new comment is received.'))
-_register('COMMENT_REQUIRES_MODERATION',
-          lazy_gettext(u'When a comment requires moderation.'),
-          (MODERATE_OWN_PAGES | MODERATE_OWN_ENTRIES | MODERATE_COMMENTS))
-_register('SECURITY_ALERT',
-          lazy_gettext(u'When Zine found an urgent security alarm.'),
-          BLOG_ADMIN)
-_register('ZINE_ERROR', lazy_gettext(u'When Zine throws errors.'), BLOG_ADMIN)
-
-
-DEFAULT_NOTIFICATION_SYSTEMS = [EMailNotificationSystem]
-del _register
+# -*- coding: utf-8 -*-
+"""
+    zine.notifications
+    ~~~~~~~~~~~~~~~~~~
+
+    This module implements an extensible notification system.  Plugins can
+    provide different kinds of notification systems (like email, jabber etc.)
+
+    Each user can subscribe to different kinds of events.  The general design
+    is inspired by Growl.
+
+    :copyright: (c) 2010 by the Zine Team, see AUTHORS for more details.
+    :license: BSD, see LICENSE for more details.
+"""
+from datetime import datetime
+from urlparse import urlsplit
+
+from werkzeug import url_unquote
+
+from zine.models import NotificationSubscription
+from zine.application import get_application, get_request, render_template
+from zine.privileges import BLOG_ADMIN, ENTER_ACCOUNT_PANEL, MODERATE_COMMENTS,\
+     MODERATE_OWN_PAGES, MODERATE_OWN_ENTRIES
+from zine.utils.zeml import parse_zeml
+from zine.utils.mail import send_email
+from zine.i18n import lazy_gettext
+
+
+__all__ = ['DEFAULT_NOTIFICATION_TYPES', 'NotificationType']
+
+DEFAULT_NOTIFICATION_TYPES = {}
+
+
+def send_notification(type, message, user=Ellipsis):
+    """Convenience function.  Get the application object and deliver the
+    notification to it's NotificationManager.
+
+    The message must be a valid ZEML formatted message.  The following
+    top-level elements are available for marking up the message:
+
+    title
+        The title of the notification.  Some systems may only transmit this
+        part of the message.
+
+    summary
+        An optional quick summary.  If the text is short enough it can be
+        omitted and the system will try to transmit the longtext in that
+        case.  The upper limit for the summary should be around 100 chars.
+
+    details
+        If given this may either contain a paragraph with textual information
+        or an ordered or unordered list of text or links.  The general markup
+        rules apply.
+
+    longtext
+        The full text of this notification.  May contain some formattings.
+
+    actions
+        If given this may contain an unordered list of action links.  These
+        links may be transmitted together with the notification.
+
+    Additionally if there is an associated page with the notification,
+    somewhere should be a link element with a "selflink" class.  This can be
+    embedded in the longtext or actions (but any other element too).
+
+    Example markup::
+
+        <title>New comment on "Foo bar baz"</title>
+        <summary>Mr. Miracle wrote a new comment: "This is awesome."</summary>
+        <details>
+          <ul>
+            <li><a href="http://miracle.invalid/">Mr. Miracle</a>
+            <li><a href="mailto:mr@miracle.invalid">E-Mail</a>
+          </ul>
+        </details>
+        <longtext>
+          <p>This is awesome.  Keep it up!
+          <p>Love your work
+        </longtext>
+        <actions>
+          <ul>
+            <li><a href="http://.../link" class="selflink">all comments</a>
+            <li><a href="http://.../?action=delete">delete it</a>
+            <li><a href="http://.../?action=approve">approve it</a>
+          </ul>
+        </actions>
+
+    Example plaintext rendering (e-mail)::
+
+        Subject: New comment on "Foo bar baz"
+
+        Mr. Miracle             http://miracle.invalid/
+        E-Mail                  mr@mircale.invalid
+
+        > This is awesome.   Keep it up!
+        > Love your work.