← Back to team overview

widelands-dev team mailing list archive

[Merge] lp:~widelands-dev/widelands-website/django1_8 into lp:widelands-website

 

kaputtnik has proposed merging lp:~widelands-dev/widelands-website/django1_8 into lp:widelands-website.

Commit message:
Upgrade to Django 1.8 

Included formerly third party apps: django-messages, django-ratings, sphinxdoc, threadedcomments, tracking

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1088032 in Widelands Website: "Home page results in "ImportError No module named feeds" if trying to use Django 1.4.2"
  https://bugs.launchpad.net/widelands-website/+bug/1088032
  Bug #1473023 in Widelands Website: "Upgrade to recaptach v2"
  https://bugs.launchpad.net/widelands-website/+bug/1473023
  Bug #1552425 in Widelands Website: "Upgrade to django 1.8"
  https://bugs.launchpad.net/widelands-website/+bug/1552425

For more details, see:
https://code.launchpad.net/~widelands-dev/widelands-website/django1_8/+merge/298561

Upgrade the website code to use Django 1.8

There are three text conflicts when merging which are caused by mixed indentations, IMHO. I will resolve them when the real merge into trunk is done and before uploading trunk to launchpad.

After merging with trunk, i want to make another test on alpha.widelands.org


-- 
The attached diff has been truncated due to its size.
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands-website/django1_8 into lp:widelands-website.
=== modified file '.bzrignore'
--- .bzrignore	2013-06-17 05:46:56 +0000
+++ .bzrignore	2016-06-28 17:58:37 +0000
@@ -4,10 +4,9 @@
 /.bzr-repo
 
 # Uploaded images.
-/media/forum/img/en
-/media/news
-/media/wlhelp/
-/media/wlimages
-/media/wlmaps/
-/media/wlprofile/avatars
-/media/wlscreens/
+media/news/*
+media/wlhelp/*
+media/wlmaps/*
+media/wlprofile/avatars/*
+media/wlscreens/*
+media/wlimages/*

=== modified file 'README.txt'
--- README.txt	2015-09-20 12:24:47 +0000
+++ README.txt	2016-06-28 17:58:37 +0000
@@ -9,11 +9,11 @@
 the dependencies for the website. Go and install them all.
 
 Example:
-On Ubuntu, installing all required tools and dependencies in two commands::
+On Ubuntu, installing all required tools and dependencies in two commands:
 
    $ sudo apt-get install python-dev python-virtualenv python-pip mercurial bzr subversion git-core sqlite3
    $ sudo apt-get build-dep python-numpy
-
+   
 Setting up the local environment
 --------------------------------
 
@@ -23,17 +23,17 @@
 
 This will make sure that your virtual environment is not tainted with python
 packages from your global site packages. Very important!
-Now, we create and activate our environment:: 
+Now, we create and activate our environment. This installation depends on python2.7, so you must may use a special virtualenv-command: 
 
-   $ virtualenv --no-site-packages wlwebsite
-   $ cd wlwebsite
+   $ virtualenv wl_django1_8
+   $ cd wl_django1_8
    $ source bin/activate
 
 Next, we download the website source code::
 
    $ mkdir code
    $ cd code
-   $ bzr branch lp:widelands-website widelands
+   $ bzr branch lp:~widelands-dev/widelands-website/django1_8 widelands
    $ cd widelands
 
 All fine and good. Now we have to install all the third party modules the
@@ -63,31 +63,17 @@
    $ ln -s local_urls.py.sample local_urls.py
    $ ln -s local_settings.py.sample local_settings.py
    
-There has to be some corrections to get into the admin pages:
-
-Either copy the folders "media" and "templates"
-
-   from: ~/wlwebsite/django/contrib/admin
-   to:   ~/wlwebsite/lib/python2.7/site-packages/django/contrib/admin/
-   
-or create symlinks:
-
-   $ ln -s ~/wlwebsite/django/contrib/admin/templates/ ~/wlwebsite/lib/python2.7/site-packages/django/contrib/admin/templates
-   $ ln -s ~/wlwebsite/django/contrib/admin/media/ ~/wlwebsite/lib/python2.7/site-packages/django/contrib/admin/media
-
 Setting up the database
 ^^^^^^^^^^^^^^^^^^^^^^^
 
-Now, let's try if everything works out::
-
-   $ ./manage.py syncdb
-
-You will need to enter a superuser name and account.
-After setting up the database, pybb and djangoratings will not be synced.
-To migrate these, run::
+Now creating the tables in the database:
 
    $ ./manage.py migrate
 
+Create a superuser:
+
+   $ ./manage.py createsuperuser
+
 Now, let's run the page::
 
    $ ./manage.py runserver
@@ -99,8 +85,12 @@
 ^^^^^^^^^^^^^^^^^^^^^^^
 
 Go to http://localhost:8000/admin. Log in with your super user and go to the
-Sites Admin. Change your site name from example.com to localhost. Now,
-everything should work out.
+following Tables:
+
+- Site/Sites: Change your site name from example.com to localhost:8000. 
+- Wlprofile/Profiles: Add yourself as a user
+
+Now everything should work.
 
 Accessing the website from other machines
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -113,7 +103,7 @@
 
    $ ./manage.py runserver 169.254.1.0:8000
 
-See also http://docs.djangoproject.com/en/dev/ref/django-admin/#runserver-port-or-address-port
+See also https://docs.djangoproject.com/en/dev/ref/django-admin/#examples-of-using-different-ports-and-addresses
 for further details. 
 
 Setting up the online help / encyclopedia

=== removed file 'atomformat.py'
--- atomformat.py	2010-01-01 21:35:23 +0000
+++ atomformat.py	1970-01-01 00:00:00 +0000
@@ -1,542 +0,0 @@
-# 
-# django-atompub by James Tauber <http://jtauber.com/>
-# http://code.google.com/p/django-atompub/
-# An implementation of the Atom format and protocol for Django
-# 
-# For instructions on how to use this module to generate Atom feeds,
-# see http://code.google.com/p/django-atompub/wiki/UserGuide
-# 
-# 
-# Copyright (c) 2007, James Tauber
-# 
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-# 
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-# 
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-# 
-
-from xml.sax.saxutils import XMLGenerator
-from datetime import datetime
-
-
-GENERATOR_TEXT = 'django-atompub'
-GENERATOR_ATTR = {
-    'uri': 'http://code.google.com/p/django-atompub/',
-    'version': 'r33'
-}
-
-
-
-## based on django.utils.xmlutils.SimplerXMLGenerator
-class SimplerXMLGenerator(XMLGenerator):
-    def addQuickElement(self, name, contents=None, attrs=None):
-        "Convenience method for adding an element with no children"
-        if attrs is None: attrs = {}
-        self.startElement(name, attrs)
-        if contents is not None:
-            self.characters(contents)
-        self.endElement(name)
-
-
-
-## based on django.utils.feedgenerator.rfc3339_date
-def rfc3339_date(date):
-    return date.strftime('%Y-%m-%dT%H:%M:%SZ')
-
-
-
-## based on django.utils.feedgenerator.get_tag_uri
-def get_tag_uri(url, date):
-    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id";
-    tag = re.sub('^http://', '', url)
-    if date is not None:
-        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
-    tag = re.sub('#', '/', tag)
-    return 'tag:' + tag
-
-
-
-## based on django.contrib.syndication.feeds.Feed
-class Feed(object):
-    
-    
-    VALIDATE = True
-    
-    
-    def __init__(self, slug, feed_url):
-        # @@@ slug and feed_url are not used yet
-        pass
-    
-    
-    def __get_dynamic_attr(self, attname, obj, default=None):
-        try:
-            attr = getattr(self, attname)
-        except AttributeError:
-            return default
-        if callable(attr):
-            # Check func_code.co_argcount rather than try/excepting the
-            # function and catching the TypeError, because something inside
-            # the function may raise the TypeError. This technique is more
-            # accurate.
-            if hasattr(attr, 'func_code'):
-                argcount = attr.func_code.co_argcount
-            else:
-                argcount = attr.__call__.func_code.co_argcount
-            if argcount == 2: # one argument is 'self'
-                return attr(obj)
-            else:
-                return attr()
-        return attr
-    
-    
-    def get_feed(self, extra_params=None):
-        
-        if extra_params:
-            try:
-                obj = self.get_object(extra_params.split('/'))
-            except (AttributeError, LookupError):
-                raise LookupError('Feed does not exist')
-        else:
-            obj = None
-        
-        feed = AtomFeed(
-            atom_id = self.__get_dynamic_attr('feed_id', obj),
-            title = self.__get_dynamic_attr('feed_title', obj),
-            updated = self.__get_dynamic_attr('feed_updated', obj),
-            icon = self.__get_dynamic_attr('feed_icon', obj),
-            logo = self.__get_dynamic_attr('feed_logo', obj),
-            rights = self.__get_dynamic_attr('feed_rights', obj),
-            subtitle = self.__get_dynamic_attr('feed_subtitle', obj),
-            authors = self.__get_dynamic_attr('feed_authors', obj, default=[]),
-            categories = self.__get_dynamic_attr('feed_categories', obj, default=[]),
-            contributors = self.__get_dynamic_attr('feed_contributors', obj, default=[]),
-            links = self.__get_dynamic_attr('feed_links', obj, default=[]),
-            extra_attrs = self.__get_dynamic_attr('feed_extra_attrs', obj),
-            hide_generator = self.__get_dynamic_attr('hide_generator', obj, default=False)
-        )
-        
-        items = self.__get_dynamic_attr('items', obj)
-        if items is None:
-            raise LookupError('Feed has no items field')
-        
-        for item in items:
-            feed.add_item(
-                atom_id = self.__get_dynamic_attr('item_id', item), 
-                title = self.__get_dynamic_attr('item_title', item),
-                updated = self.__get_dynamic_attr('item_updated', item),
-                content = self.__get_dynamic_attr('item_content', item),
-                published = self.__get_dynamic_attr('item_published', item),
-                rights = self.__get_dynamic_attr('item_rights', item),
-                source = self.__get_dynamic_attr('item_source', item),
-                summary = self.__get_dynamic_attr('item_summary', item),
-                authors = self.__get_dynamic_attr('item_authors', item, default=[]),
-                categories = self.__get_dynamic_attr('item_categories', item, default=[]),
-                contributors = self.__get_dynamic_attr('item_contributors', item, default=[]),
-                links = self.__get_dynamic_attr('item_links', item, default=[]),
-                extra_attrs = self.__get_dynamic_attr('item_extra_attrs', None, default={}),
-            )
-        
-        if self.VALIDATE:
-            feed.validate()
-        return feed
-
-
-
-class ValidationError(Exception):
-    pass
-
-
-
-## based on django.utils.feedgenerator.SyndicationFeed and django.utils.feedgenerator.Atom1Feed
-class AtomFeed(object):
-    
-    
-    mime_type = 'application/atom+xml'
-    ns = u'http://www.w3.org/2005/Atom'
-    
-    
-    def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
-        authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
-        if atom_id is None:
-            raise LookupError('Feed has no feed_id field')
-        if title is None:
-            raise LookupError('Feed has no feed_title field')
-        # if updated == None, we'll calculate it
-        self.feed = {
-            'id': atom_id,
-            'title': title,
-            'updated': updated,
-            'icon': icon,
-            'logo': logo,
-            'rights': rights,
-            'subtitle': subtitle,
-            'authors': authors,
-            'categories': categories,
-            'contributors': contributors,
-            'links': links,
-            'extra_attrs': extra_attrs,
-            'hide_generator': hide_generator,
-        }
-        self.items = []
-    
-    
-    def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
-        authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
-        if atom_id is None:
-            raise LookupError('Feed has no item_id method')
-        if title is None:
-            raise LookupError('Feed has no item_title method')
-        if updated is None:
-            raise LookupError('Feed has no item_updated method')
-        self.items.append({
-            'id': atom_id,
-            'title': title,
-            'updated': updated,
-            'content': content,
-            'published': published,
-            'rights': rights,
-            'source': source,
-            'summary': summary,
-            'authors': authors,
-            'categories': categories,
-            'contributors': contributors,
-            'links': links,
-            'extra_attrs': extra_attrs,
-        })
-    
-    
-    def latest_updated(self):
-        """
-        Returns the latest item's updated or the current time if there are no items.
-        """
-        updates = [item['updated'] for item in self.items]
-        if len(updates) > 0:
-            updates.sort()
-            return updates[-1]
-        else:
-            return datetime.now() # @@@ really we should allow a feed to define its "start" for this case
-    
-    
-    def write_text_construct(self, handler, element_name, data):
-        if isinstance(data, tuple):
-            text_type, text = data
-            if text_type == 'xhtml':
-                handler.startElement(element_name, {'type': text_type})
-                handler._write(text) # write unescaped -- it had better be well-formed XML
-                handler.endElement(element_name)
-            else:
-                handler.addQuickElement(element_name, text, {'type': text_type})
-        else:
-            handler.addQuickElement(element_name, data)
-    
-    
-    def write_person_construct(self, handler, element_name, person):
-        handler.startElement(element_name, {})
-        handler.addQuickElement(u'name', person['name'])
-        if 'uri' in person:
-            handler.addQuickElement(u'uri', person['uri'])
-        if 'email' in person:
-            handler.addQuickElement(u'email', person['email'])
-        handler.endElement(element_name)
-    
-    
-    def write_link_construct(self, handler, link):
-        if 'length' in link:
-            link['length'] = str(link['length'])
-        handler.addQuickElement(u'link', None, link)
-    
-    
-    def write_category_construct(self, handler, category):
-        handler.addQuickElement(u'category', None, category)
-    
-    
-    def write_source(self, handler, data):
-        handler.startElement(u'source', {})
-        if data.get('id'):
-            handler.addQuickElement(u'id', data['id'])
-        if data.get('title'):
-            self.write_text_construct(handler, u'title', data['title'])
-        if data.get('subtitle'):
-            self.write_text_construct(handler, u'subtitle', data['subtitle'])
-        if data.get('icon'):
-            handler.addQuickElement(u'icon', data['icon'])
-        if data.get('logo'):
-            handler.addQuickElement(u'logo', data['logo'])
-        if data.get('updated'):
-            handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
-        for category in data.get('categories', []):
-            self.write_category_construct(handler, category)
-        for link in data.get('links', []):
-            self.write_link_construct(handler, link)
-        for author in data.get('authors', []):
-            self.write_person_construct(handler, u'author', author)
-        for contributor in data.get('contributors', []):
-            self.write_person_construct(handler, u'contributor', contributor)
-        if data.get('rights'):
-            self.write_text_construct(handler, u'rights', data['rights'])
-        handler.endElement(u'source')
-    
-    
-    def write_content(self, handler, data):
-        if isinstance(data, tuple):
-            content_dict, text = data
-            if content_dict.get('type') == 'xhtml':
-                handler.startElement(u'content', content_dict)
-                handler._write(text) # write unescaped -- it had better be well-formed XML
-                handler.endElement(u'content')
-            else:
-                handler.addQuickElement(u'content', text, content_dict)
-        else:
-            handler.addQuickElement(u'content', data)
-    
-    
-    def write(self, outfile, encoding):
-        handler = SimplerXMLGenerator(outfile, encoding)
-        handler.startDocument()
-        feed_attrs = {u'xmlns': self.ns}
-        if self.feed.get('extra_attrs'):
-            feed_attrs.update(self.feed['extra_attrs'])
-        handler.startElement(u'feed', feed_attrs)
-        handler.addQuickElement(u'id', self.feed['id'])
-        self.write_text_construct(handler, u'title', self.feed['title'])
-        if self.feed.get('subtitle'):
-            self.write_text_construct(handler, u'subtitle', self.feed['subtitle'])
-        if self.feed.get('icon'):
-            handler.addQuickElement(u'icon', self.feed['icon'])
-        if self.feed.get('logo'):
-            handler.addQuickElement(u'logo', self.feed['logo'])
-        if self.feed['updated']:
-            handler.addQuickElement(u'updated', rfc3339_date(self.feed['updated']))
-        else:
-            handler.addQuickElement(u'updated', rfc3339_date(self.latest_updated()))
-        for category in self.feed['categories']:
-            self.write_category_construct(handler, category)
-        for link in self.feed['links']:
-            self.write_link_construct(handler, link)
-        for author in self.feed['authors']:
-            self.write_person_construct(handler, u'author', author)
-        for contributor in self.feed['contributors']:
-            self.write_person_construct(handler, u'contributor', contributor)
-        if self.feed.get('rights'):
-            self.write_text_construct(handler, u'rights', self.feed['rights'])
-        if not self.feed.get('hide_generator'):
-            handler.addQuickElement(u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
-        
-        self.write_items(handler)
-        
-        handler.endElement(u'feed')
-    
-    
-    def write_items(self, handler):
-        for item in self.items:
-            entry_attrs = item.get('extra_attrs', {})
-            handler.startElement(u'entry', entry_attrs)
-            
-            handler.addQuickElement(u'id', item['id'])
-            self.write_text_construct(handler, u'title', item['title'])
-            handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
-            if item.get('published'):
-                handler.addQuickElement(u'published', rfc3339_date(item['published']))
-            if item.get('rights'):
-                self.write_text_construct(handler, u'rights', item['rights'])
-            if item.get('source'):
-                self.write_source(handler, item['source'])
-            
-            for author in item['authors']:
-                self.write_person_construct(handler, u'author', author)
-            for contributor in item['contributors']:
-                self.write_person_construct(handler, u'contributor', contributor)
-            for category in item['categories']:
-                self.write_category_construct(handler, category)
-            for link in item['links']:
-                self.write_link_construct(handler, link)
-            if item.get('summary'):
-                self.write_text_construct(handler, u'summary', item['summary'])
-            if item.get('content'):
-                self.write_content(handler, item['content'])
-            
-            handler.endElement(u'entry')
-    
-    
-    def validate(self):
-        
-        def validate_text_construct(obj):
-            if isinstance(obj, tuple):
-                if obj[0] not in ['text', 'html', 'xhtml']:
-                    return False
-            # @@@ no validation is done that 'html' text constructs are valid HTML
-            # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
-            
-            return True
-        
-        if not validate_text_construct(self.feed['title']):
-            raise ValidationError('feed title has invalid type')
-        if self.feed.get('subtitle'):
-            if not validate_text_construct(self.feed['subtitle']):
-                raise ValidationError('feed subtitle has invalid type')
-        if self.feed.get('rights'):
-            if not validate_text_construct(self.feed['rights']):
-                raise ValidationError('feed rights has invalid type')
-        
-        alternate_links = {}
-        for link in self.feed.get('links'):
-            if link.get('rel') == 'alternate' or link.get('rel') == None:
-                key = (link.get('type'), link.get('hreflang'))
-                if key in alternate_links:
-                    raise ValidationError('alternate links must have unique type/hreflang')
-                alternate_links[key] = link
-        
-        if self.feed.get('authors'):
-            feed_author = True
-        else:
-            feed_author = False
-        
-        for item in self.items:
-            if not feed_author and not item.get('authors'):
-                if item.get('source') and item['source'].get('authors'):
-                    pass
-                else:
-                    raise ValidationError('if no feed author, all entries must have author (possibly in source)')
-            
-            if not validate_text_construct(item['title']):
-                raise ValidationError('entry title has invalid type')
-            if item.get('rights'):
-                if not validate_text_construct(item['rights']):
-                    raise ValidationError('entry rights has invalid type')
-            if item.get('summary'):
-                if not validate_text_construct(item['summary']):
-                    raise ValidationError('entry summary has invalid type')
-            source = item.get('source')
-            if source:
-                if source.get('title'):
-                    if not validate_text_construct(source['title']):
-                        raise ValidationError('source title has invalid type')
-                if source.get('subtitle'):
-                    if not validate_text_construct(source['subtitle']):
-                        raise ValidationError('source subtitle has invalid type')
-                if source.get('rights'):
-                    if not validate_text_construct(source['rights']):
-                        raise ValidationError('source rights has invalid type')
-            
-            alternate_links = {}
-            for link in item.get('links'):
-                if link.get('rel') == 'alternate' or link.get('rel') == None:
-                    key = (link.get('type'), link.get('hreflang'))
-                    if key in alternate_links:
-                        raise ValidationError('alternate links must have unique type/hreflang')
-                    alternate_links[key] = link
-            
-            if not item.get('content'):
-                if not alternate_links:
-                    raise ValidationError('if no content, entry must have alternate link')
-            
-            if item.get('content') and isinstance(item.get('content'), tuple):
-                content_type = item.get('content')[0].get('type')
-                if item.get('content')[0].get('src'):
-                    if item.get('content')[1]:
-                        raise ValidationError('content with src should be empty')
-                    if not item.get('summary'):
-                        raise ValidationError('content with src requires a summary too')
-                    if content_type in ['text', 'html', 'xhtml']:
-                        raise ValidationError('content with src cannot have type of text, html or xhtml')
-                if content_type:
-                    if '/' in content_type and \
-                        not content_type.startswith('text/') and \
-                        not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
-                        not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
-                        # @@@ check content is Base64
-                        if not item.get('summary'):
-                            raise ValidationError('content in Base64 requires a summary too')
-                    if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
-                        raise ValidationError('content type does not appear to be valid')
-                    
-                    # @@@ no validation is done that 'html' text constructs are valid HTML
-                    # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
-                    
-                    return
-        
-        return
-
-
-
-class LegacySyndicationFeed(AtomFeed):
-    """
-    Provides an SyndicationFeed-compatible interface in its __init__ and
-    add_item but is really a new AtomFeed object.
-    """
-    
-    def __init__(self, title, link, description, language=None, author_email=None,
-            author_name=None, author_link=None, subtitle=None, categories=[],
-            feed_url=None, feed_copyright=None):
-        
-        atom_id = link
-        title = title
-        updated = None # will be calculated
-        rights = feed_copyright
-        subtitle = subtitle
-        author_dict = {'name': author_name}
-        if author_link:
-            author_dict['uri'] = author_uri
-        if author_email:
-            author_dict['email'] = author_email
-        authors = [author_dict]
-        if categories:
-            categories = [{'term': term} for term in categories]
-        links = [{'rel': 'alternate', 'href': link}]
-        if feed_url:
-            links.append({'rel': 'self', 'href': feed_url})
-        if language:
-            extra_attrs = {'xml:lang': language}
-        else:
-            extra_attrs = {}
-        
-        # description ignored (as with Atom1Feed)
-        
-        AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
-                authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
-    
-    
-    def add_item(self, title, link, description, author_email=None,
-            author_name=None, author_link=None, pubdate=None, comments=None,
-            unique_id=None, enclosure=None, categories=[], item_copyright=None):
-        
-        if unique_id:
-            atom_id = unique_id
-        else:
-            atom_id = get_tag_uri(link, pubdate)
-        title = title
-        updated = pubdate
-        if item_copyright:
-            rights = item_copyright
-        else:
-            rights = None
-        if description:
-            summary = 'html', description
-        else:
-            summary = None
-        author_dict = {'name': author_name}
-        if author_link:
-            author_dict['uri'] = author_uri
-        if author_email:
-            author_dict['email'] = author_email
-        authors = [author_dict]
-        categories = [{'term': term} for term in categories]
-        links = [{'rel': 'alternate', 'href': link}]
-        if enclosure:
-            links.append({'rel': 'enclosure', 'href': enclosure.url, 'length': enclosure.length, 'type': enclosure.mime_type})
-        
-        AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
-                authors=authors, categories=categories, links=links)

=== added directory 'djangoratings'
=== added file 'djangoratings/LICENSE'
--- djangoratings/LICENSE	1970-01-01 00:00:00 +0000
+++ djangoratings/LICENSE	2016-06-28 17:58:37 +0000
@@ -0,0 +1,22 @@
+Copyright (c) 2009, David Cramer <dcramer@xxxxxxxxx>
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this 
+list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright notice, 
+this list of conditions and the following disclaimer in the documentation 
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

=== added file 'djangoratings/__init__.py'
--- djangoratings/__init__.py	1970-01-01 00:00:00 +0000
+++ djangoratings/__init__.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,46 @@
+import os.path
+import warnings
+
+__version__ = (0, 3, 7)
+
+def _get_git_revision(path):
+    revision_file = os.path.join(path, 'refs', 'heads', 'master')
+    if not os.path.exists(revision_file):
+        return None
+    fh = open(revision_file, 'r')
+    try:
+        return fh.read()
+    finally:
+        fh.close()
+
+def get_revision():
+    """
+    :returns: Revision number of this branch/checkout, if available. None if
+        no revision number can be determined.
+    """
+    package_dir = os.path.dirname(__file__)
+    checkout_dir = os.path.normpath(os.path.join(package_dir, '..'))
+    path = os.path.join(checkout_dir, '.git')
+    if os.path.exists(path):
+        return _get_git_revision(path)
+    return None
+
+__build__ = get_revision()
+
+def lazy_object(location):
+    def inner(*args, **kwargs):
+        parts = location.rsplit('.', 1)
+        warnings.warn('`djangoratings.%s` is deprecated. Please use `%s` instead.' % (parts[1], location), DeprecationWarning)
+        try:
+            imp = __import__(parts[0], globals(), locals(), [parts[1]], -1)
+        except:
+            imp = __import__(parts[0], globals(), locals(), [parts[1]])
+        func = getattr(imp, parts[1])
+        if callable(func):
+            return func(*args, **kwargs)
+        return func
+    return inner
+
+RatingField = lazy_object('djangoratings.fields.RatingField')
+AnonymousRatingField = lazy_object('djangoratings.fields.AnonymousRatingField')
+Rating = lazy_object('djangoratings.fields.Rating')
\ No newline at end of file

=== added file 'djangoratings/admin.py'
--- djangoratings/admin.py	1970-01-01 00:00:00 +0000
+++ djangoratings/admin.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,15 @@
+from django.contrib import admin
+from models import Vote, Score
+
+class VoteAdmin(admin.ModelAdmin):
+    list_display = ('content_object', 'user', 'ip_address', 'cookie', 'score', 'date_changed')
+    list_filter = ('score', 'content_type', 'date_changed')
+    search_fields = ('ip_address',)
+    raw_id_fields = ('user',)
+
+class ScoreAdmin(admin.ModelAdmin):
+    list_display = ('content_object', 'score', 'votes')
+    list_filter = ('content_type',)
+
+admin.site.register(Vote, VoteAdmin)
+admin.site.register(Score, ScoreAdmin)

=== added file 'djangoratings/default_settings.py'
--- djangoratings/default_settings.py	1970-01-01 00:00:00 +0000
+++ djangoratings/default_settings.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,5 @@
+from django.conf import settings
+
+# Used to limit the number of unique IPs that can vote on a single object+field.
+#   useful if you're getting rating spam by users registering multiple accounts
+RATINGS_VOTES_PER_IP = 3
\ No newline at end of file

=== added file 'djangoratings/exceptions.py'
--- djangoratings/exceptions.py	1970-01-01 00:00:00 +0000
+++ djangoratings/exceptions.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,5 @@
+class InvalidRating(ValueError): pass
+class AuthRequired(TypeError): pass
+class CannotChangeVote(Exception): pass
+class CannotDeleteVote(Exception): pass
+class IPLimitReached(Exception): pass
\ No newline at end of file

=== added file 'djangoratings/fields.py'
--- djangoratings/fields.py	1970-01-01 00:00:00 +0000
+++ djangoratings/fields.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,394 @@
+from django.db.models import IntegerField, PositiveIntegerField
+from django.conf import settings
+
+import forms
+import itertools
+from datetime import datetime
+
+from models import Vote, Score
+from default_settings import RATINGS_VOTES_PER_IP
+from exceptions import *
+
+if 'django.contrib.contenttypes' not in settings.INSTALLED_APPS:
+    raise ImportError("djangoratings requires django.contrib.contenttypes in your INSTALLED_APPS")
+
+from django.contrib.contenttypes.models import ContentType
+
+__all__ = ('Rating', 'RatingField', 'AnonymousRatingField')
+
+try:
+    from hashlib import md5
+except ImportError:
+    from md5 import new as md5
+
+try:
+    from django.utils.timezone import now
+except ImportError:
+    now = datetime.now
+
+def md5_hexdigest(value):
+    return md5(value).hexdigest()
+
+class Rating(object):
+    def __init__(self, score, votes):
+        self.score = score
+        self.votes = votes
+
+class RatingManager(object):
+    def __init__(self, instance, field):
+        self.content_type = None
+        self.instance = instance
+        self.field = field
+        
+        self.votes_field_name = "%s_votes" % (self.field.name,)
+        self.score_field_name = "%s_score" % (self.field.name,)
+    
+    def get_percent(self):
+        """get_percent()
+        
+        Returns the weighted percentage of the score from min-max values"""
+        if not (self.votes and self.score):
+            return 0
+        return 100 * (self.get_rating() / self.field.range)
+    
+    def get_real_percent(self):
+        """get_real_percent()
+        
+        Returns the unmodified percentage of the score based on a 0-point scale."""
+        if not (self.votes and self.score):
+            return 0
+        return 100 * (self.get_real_rating() / self.field.range)
+    
+    def get_ratings(self):
+        """get_ratings()
+        
+        Returns a Vote QuerySet for this rating field."""
+        return Vote.objects.filter(content_type=self.get_content_type(), object_id=self.instance.pk, key=self.field.key)
+        
+    def get_rating(self):
+        """get_rating()
+        
+        Returns the weighted average rating."""
+        if not (self.votes and self.score):
+            return 0
+        return float(self.score)/(self.votes+self.field.weight)
+    
+    def get_opinion_percent(self):
+        """get_opinion_percent()
+        
+        Returns a neutral-based percentage."""
+        return (self.get_percent()+100)/2
+
+    def get_real_rating(self):
+        """get_rating()
+        
+        Returns the unmodified average rating."""
+        if not (self.votes and self.score):
+            return 0
+        return float(self.score)/self.votes
+    
+    def get_rating_for_user(self, user, ip_address=None, cookies={}):
+        """get_rating_for_user(user, ip_address=None, cookie=None)
+        
+        Returns the rating for a user or anonymous IP."""
+        kwargs = dict(
+            content_type    = self.get_content_type(),
+            object_id       = self.instance.pk,
+            key             = self.field.key,
+        )
+
+        if not (user and user.is_authenticated()):
+            if not ip_address:
+                raise ValueError('``user`` or ``ip_address`` must be present.')
+            kwargs['user__isnull'] = True
+            kwargs['ip_address'] = ip_address
+        else:
+            kwargs['user'] = user
+        
+        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
+        if use_cookies:
+            # TODO: move 'vote-%d.%d.%s' to settings or something
+            cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
+            cookie = cookies.get(cookie_name)
+            if cookie:    
+                kwargs['cookie'] = cookie
+            else:
+                kwargs['cookie__isnull'] = True
+            
+        try:
+            rating = Vote.objects.get(**kwargs)
+            return rating.score
+        except Vote.MultipleObjectsReturned:
+            pass
+        except Vote.DoesNotExist:
+            pass
+        return
+    
+    def get_iterable_range(self):
+        return range(1, self.field.range) #started from 1, because 0 is equal to delete
+        
+    def add(self, score, user, ip_address, cookies={}, commit=True):
+        """add(score, user, ip_address)
+        
+        Used to add a rating to an object."""
+        try:
+            score = int(score)
+        except (ValueError, TypeError):
+            raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
+        
+        delete = (score == 0)
+        if delete and not self.field.allow_delete:
+            raise CannotDeleteVote("you are not allowed to delete votes for %s" % (self.field.name,))
+            # ... you're also can't delete your vote if you haven't permissions to change it. I leave this case for CannotChangeVote
+        
+        if score < 0 or score > self.field.range:
+            raise InvalidRating("%s is not a valid choice for %s" % (score, self.field.name))
+
+        is_anonymous = (user is None or not user.is_authenticated())
+        if is_anonymous and not self.field.allow_anonymous:
+            raise AuthRequired("user must be a user, not '%r'" % (user,))
+        
+        if is_anonymous:
+            user = None
+        
+        defaults = dict(
+            score = score,
+            ip_address = ip_address,
+        )
+        
+        kwargs = dict(
+            content_type    = self.get_content_type(),
+            object_id       = self.instance.pk,
+            key             = self.field.key,
+            user            = user,
+        )
+        if not user:
+            kwargs['ip_address'] = ip_address
+        
+        use_cookies = (self.field.allow_anonymous and self.field.use_cookies)
+        if use_cookies:
+            defaults['cookie'] = now().strftime('%Y%m%d%H%M%S%f') # -> md5_hexdigest?
+            # TODO: move 'vote-%d.%d.%s' to settings or something
+            cookie_name = 'vote-%d.%d.%s' % (kwargs['content_type'].pk, kwargs['object_id'], kwargs['key'][:6],) # -> md5_hexdigest?
+            cookie = cookies.get(cookie_name) # try to get existent cookie value
+            if not cookie:
+                kwargs['cookie__isnull'] = True
+            kwargs['cookie'] = cookie
+
+        try:
+            rating, created = Vote.objects.get(**kwargs), False
+        except Vote.DoesNotExist:
+            if delete:
+                raise CannotDeleteVote("attempt to find and delete your vote for %s is failed" % (self.field.name,))
+            if getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
+                num_votes = Vote.objects.filter(
+                    content_type=kwargs['content_type'],
+                    object_id=kwargs['object_id'],
+                    key=kwargs['key'],
+                    ip_address=ip_address,
+                ).count()
+                if num_votes >= getattr(settings, 'RATINGS_VOTES_PER_IP', RATINGS_VOTES_PER_IP):
+                    raise IPLimitReached()
+            kwargs.update(defaults)
+            if use_cookies:
+                # record with specified cookie was not found ...
+                cookie = defaults['cookie'] # ... thus we need to replace old cookie (if presented) with new one
+                kwargs.pop('cookie__isnull', '') # ... and remove 'cookie__isnull' (if presented) from .create()'s **kwargs
+            rating, created = Vote.objects.create(**kwargs), True
+            
+        has_changed = False
+        if not created:
+            if self.field.can_change_vote:
+                has_changed = True
+                self.score -= rating.score
+                # you can delete your vote only if you have permission to change your vote
+                if not delete:
+                    rating.score = score
+                    rating.save()
+                else:
+                    self.votes -= 1
+                    rating.delete()
+            else:
+                raise CannotChangeVote()
+        else:
+            has_changed = True
+            self.votes += 1
+        if has_changed:
+            if not delete:
+                self.score += rating.score
+            if commit:
+                self.instance.save()
+            #setattr(self.instance, self.field.name, Rating(score=self.score, votes=self.votes))
+            
+            defaults = dict(
+                score   = self.score,
+                votes   = self.votes,
+            )
+            
+            kwargs = dict(
+                content_type    = self.get_content_type(),
+                object_id       = self.instance.pk,
+                key             = self.field.key,
+            )
+            
+            try:
+                score, created = Score.objects.get(**kwargs), False
+            except Score.DoesNotExist:
+                kwargs.update(defaults)
+                score, created = Score.objects.create(**kwargs), True
+            
+            if not created:
+                score.__dict__.update(defaults)
+                score.save()
+        
+        # return value
+        adds = {}
+        if use_cookies:
+            adds['cookie_name'] = cookie_name
+            adds['cookie'] = cookie
+        if delete:
+            adds['deleted'] = True
+        return adds
+
+    def delete(self, user, ip_address, cookies={}, commit=True):
+        return self.add(0, user, ip_address, cookies, commit)
+    
+    def _get_votes(self, default=None):
+        return getattr(self.instance, self.votes_field_name, default)
+    
+    def _set_votes(self, value):
+        return setattr(self.instance, self.votes_field_name, value)
+        
+    votes = property(_get_votes, _set_votes)
+
+    def _get_score(self, default=None):
+        return getattr(self.instance, self.score_field_name, default)
+    
+    def _set_score(self, value):
+        return setattr(self.instance, self.score_field_name, value)
+        
+    score = property(_get_score, _set_score)
+
+    def get_content_type(self):
+        if self.content_type is None:
+            self.content_type = ContentType.objects.get_for_model(self.instance)
+        return self.content_type
+    
+    def _update(self, commit=False):
+        """Forces an update of this rating (useful for when Vote objects are removed)."""
+        votes = Vote.objects.filter(
+            content_type    = self.get_content_type(),
+            object_id       = self.instance.pk,
+            key             = self.field.key,
+        )
+        obj_score = sum([v.score for v in votes])
+        obj_votes = len(votes)
+
+        score, created = Score.objects.get_or_create(
+            content_type    = self.get_content_type(),
+            object_id       = self.instance.pk,
+            key             = self.field.key,
+            defaults        = dict(
+                score       = obj_score,
+                votes       = obj_votes,
+            )
+        )
+        if not created:
+            score.score = obj_score
+            score.votes = obj_votes
+            score.save()
+        self.score = obj_score
+        self.votes = obj_votes
+        if commit:
+            self.instance.save()
+
+class RatingCreator(object):
+    def __init__(self, field):
+        self.field = field
+        self.votes_field_name = "%s_votes" % (self.field.name,)
+        self.score_field_name = "%s_score" % (self.field.name,)
+
+    def __get__(self, instance, type=None):
+        if instance is None:
+            return self.field
+            #raise AttributeError('Can only be accessed via an instance.')
+        return RatingManager(instance, self.field)
+
+    def __set__(self, instance, value):
+        if isinstance(value, Rating):
+            setattr(instance, self.votes_field_name, value.votes)
+            setattr(instance, self.score_field_name, value.score)
+        else:
+            raise TypeError("%s value must be a Rating instance, not '%r'" % (self.field.name, value))
+
+class RatingField(IntegerField):
+    """
+    A rating field contributes two columns to the model instead of the standard single column.
+    """
+    def __init__(self, *args, **kwargs):
+        if 'choices' in kwargs:
+            raise TypeError("%s invalid attribute 'choices'" % (self.__class__.__name__,))
+        self.can_change_vote = kwargs.pop('can_change_vote', False)
+        self.weight = kwargs.pop('weight', 0)
+        self.range = kwargs.pop('range', 2)
+        self.allow_anonymous = kwargs.pop('allow_anonymous', False)
+        self.use_cookies = kwargs.pop('use_cookies', False)
+        self.allow_delete = kwargs.pop('allow_delete', False)
+        kwargs['editable'] = False
+        kwargs['default'] = 0
+        kwargs['blank'] = True
+        super(RatingField, self).__init__(*args, **kwargs)
+    
+    def contribute_to_class(self, cls, name):
+        self.name = name
+
+        # Votes tally field
+        self.votes_field = PositiveIntegerField(
+            editable=False, default=0, blank=True)
+        cls.add_to_class("%s_votes" % (self.name,), self.votes_field)
+
+        # Score sum field
+        self.score_field = IntegerField(
+            editable=False, default=0, blank=True)
+        cls.add_to_class("%s_score" % (self.name,), self.score_field)
+
+        self.key = md5_hexdigest(self.name)
+
+        field = RatingCreator(self)
+
+        if not hasattr(cls, '_djangoratings'):
+            cls._djangoratings = []
+        cls._djangoratings.append(self)
+
+        setattr(cls, name, field)
+
+    def get_db_prep_save(self, value):
+        # XXX: what happens here?
+        pass
+
+    def get_db_prep_lookup(self, lookup_type, value):
+        # TODO: hack in support for __score and __votes
+        # TODO: order_by on this field should use the weighted algorithm
+        raise NotImplementedError(self.get_db_prep_lookup)
+        # if lookup_type in ('score', 'votes'):
+        #     lookup_type = 
+        #     return self.score_field.get_db_prep_lookup()
+        if lookup_type == 'exact':
+            return [self.get_db_prep_save(value)]
+        elif lookup_type == 'in':
+            return [self.get_db_prep_save(v) for v in value]
+        else:
+            return super(RatingField, self).get_db_prep_lookup(lookup_type, value)
+
+    def formfield(self, **kwargs):
+        defaults = {'form_class': forms.RatingField}
+        defaults.update(kwargs)
+        return super(RatingField, self).formfield(**defaults)
+
+    # TODO: flatten_data method
+
+
+class AnonymousRatingField(RatingField):
+    def __init__(self, *args, **kwargs):
+        kwargs['allow_anonymous'] = True
+        super(AnonymousRatingField, self).__init__(*args, **kwargs)

=== added file 'djangoratings/forms.py'
--- djangoratings/forms.py	1970-01-01 00:00:00 +0000
+++ djangoratings/forms.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,6 @@
+from django import forms
+
+__all__ = ('RatingField',)
+
+class RatingField(forms.ChoiceField):
+    pass
\ No newline at end of file

=== added directory 'djangoratings/management'
=== added file 'djangoratings/management/__init__.py'
=== added directory 'djangoratings/management/commands'
=== added file 'djangoratings/management/commands/__init__.py'
=== added file 'djangoratings/management/commands/update_recommendations.py'
--- djangoratings/management/commands/update_recommendations.py	1970-01-01 00:00:00 +0000
+++ djangoratings/management/commands/update_recommendations.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,7 @@
+from django.core.management.base import NoArgsCommand, CommandError
+
+from djangoratings.models import SimilarUser
+
+class Command(NoArgsCommand):
+    def handle_noargs(self, **options):
+        SimilarUser.objects.update_recommendations()
\ No newline at end of file

=== added file 'djangoratings/managers.py'
--- djangoratings/managers.py	1970-01-01 00:00:00 +0000
+++ djangoratings/managers.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,114 @@
+from django.db.models import Manager
+from django.db.models.query import QuerySet
+
+from django.contrib.contenttypes.models import ContentType
+import itertools
+
+class VoteQuerySet(QuerySet):
+    def delete(self, *args, **kwargs):
+        """Handles updating the related `votes` and `score` fields attached to the model."""
+        # XXX: circular import
+        from fields import RatingField
+
+        qs = self.distinct().values_list('content_type', 'object_id').order_by('content_type')
+    
+        to_update = []
+        for content_type, objects in itertools.groupby(qs, key=lambda x: x[0]):
+            model_class = ContentType.objects.get(pk=content_type).model_class()
+            if model_class:
+                to_update.extend(list(model_class.objects.filter(pk__in=list(objects)[0])))
+        
+        retval = super(VoteQuerySet, self).delete(*args, **kwargs)
+        
+        # TODO: this could be improved
+        for obj in to_update:
+            for field in getattr(obj, '_djangoratings', []):
+                getattr(obj, field.name)._update(commit=False)
+            obj.save()
+        
+        return retval
+        
+class VoteManager(Manager):
+    def get_query_set(self):
+        return VoteQuerySet(self.model)
+
+    def get_for_user_in_bulk(self, objects, user):
+        objects = list(objects)
+        if len(objects) > 0:
+            ctype = ContentType.objects.get_for_model(objects[0])
+            votes = list(self.filter(content_type__pk=ctype.id,
+                                     object_id__in=[obj._get_pk_val() \
+                                                    for obj in objects],
+                                     user__pk=user.id))
+            vote_dict = dict([(vote.object_id, vote) for vote in votes])
+        else:
+            vote_dict = {}
+        return vote_dict
+
+class SimilarUserManager(Manager):
+    def get_recommendations(self, user, model_class, min_score=1):
+        from djangoratings.models import Vote, IgnoredObject
+        
+        content_type = ContentType.objects.get_for_model(model_class)
+        
+        params = dict(
+            v=Vote._meta.db_table,
+            sm=self.model._meta.db_table,
+            m=model_class._meta.db_table,
+            io=IgnoredObject._meta.db_table,
+        )
+        
+        objects = model_class._default_manager.extra(
+            tables=[params['v']],
+            where=[
+                '%(v)s.object_id = %(m)s.id and %(v)s.content_type_id = %%s' % params,
+                '%(v)s.user_id IN (select to_user_id from %(sm)s where from_user_id = %%s and exclude = 0)' % params,
+                '%(v)s.score >= %%s' % params,
+                # Exclude already rated maps
+                '%(v)s.object_id NOT IN (select object_id from %(v)s where content_type_id = %(v)s.content_type_id and user_id = %%s)' % params,
+                # IgnoredObject exclusions
+                '%(v)s.object_id NOT IN (select object_id from %(io)s where content_type_id = %(v)s.content_type_id and user_id = %%s)' % params,
+            ],
+            params=[content_type.id, user.id, min_score, user.id, user.id]
+        ).distinct()
+
+        # objects = model_class._default_manager.filter(pk__in=content_type.votes.extra(
+        #     where=['user_id IN (select to_user_id from %s where from_user_id = %d and exclude = 0)' % (self.model._meta.db_table, user.pk)],
+        # ).filter(score__gte=min_score).exclude(
+        #     object_id__in=IgnoredObject.objects.filter(content_type=content_type, user=user).values_list('object_id', flat=True),
+        # ).exclude(
+        #     object_id__in=Vote.objects.filter(content_type=content_type, user=user).values_list('object_id', flat=True)
+        # ).distinct().values_list('object_id', flat=True))
+        
+        return objects
+    
+    def update_recommendations(self):
+        # TODO: this is mysql only atm
+        # TODO: this doesnt handle scores that have multiple values (e.g. 10 points, 5 stars)
+        # due to it calling an agreement as score = score. We need to loop each rating instance
+        # and express the condition based on the range.
+        from djangoratings.models import Vote
+        from django.db import connection
+        cursor = connection.cursor()
+        cursor.execute('begin')
+        cursor.execute('truncate table %s' % (self.model._meta.db_table,))
+        cursor.execute("""insert into %(t1)s
+          (to_user_id, from_user_id, agrees, disagrees, exclude)
+          select v1.user_id, v2.user_id,
+                 sum(if(v2.score = v1.score, 1, 0)) as agrees,
+                 sum(if(v2.score != v1.score, 1, 0)) as disagrees, 0
+            from %(t2)s as v1
+              inner join %(t2)s as v2
+                on v1.user_id != v2.user_id
+                and v1.object_id = v2.object_id
+                and v1.content_type_id = v2.content_type_id
+            where v1.user_id is not null
+              and v2.user_id is not null
+            group by v1.user_id, v2.user_id
+            having agrees / (disagrees + 0.0001) > 3
+          on duplicate key update agrees = values(agrees), disagrees = values(disagrees);""" % dict(
+            t1=self.model._meta.db_table,
+            t2=Vote._meta.db_table,
+        ))
+        cursor.execute('commit')
+        cursor.close()
\ No newline at end of file

=== added directory 'djangoratings/migrations'
=== added file 'djangoratings/migrations/0001_initial.py'
--- djangoratings/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ djangoratings/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import django.utils.timezone
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='IgnoredObject',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField()),
+                ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Score',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField()),
+                ('key', models.CharField(max_length=32)),
+                ('score', models.IntegerField()),
+                ('votes', models.PositiveIntegerField()),
+                ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+            ],
+        ),
+        migrations.CreateModel(
+            name='SimilarUser',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('agrees', models.PositiveIntegerField(default=0)),
+                ('disagrees', models.PositiveIntegerField(default=0)),
+                ('exclude', models.BooleanField(default=False)),
+                ('from_user', models.ForeignKey(related_name='similar_users', to=settings.AUTH_USER_MODEL)),
+                ('to_user', models.ForeignKey(related_name='similar_users_from', to=settings.AUTH_USER_MODEL)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Vote',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField()),
+                ('key', models.CharField(max_length=32)),
+                ('score', models.IntegerField()),
+                ('ip_address', models.GenericIPAddressField()),
+                ('cookie', models.CharField(max_length=32, null=True, blank=True)),
+                ('date_added', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+                ('date_changed', models.DateTimeField(default=django.utils.timezone.now, editable=False)),
+                ('content_type', models.ForeignKey(related_name='votes', to='contenttypes.ContentType')),
+                ('user', models.ForeignKey(related_name='votes', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
+            ],
+        ),
+        migrations.AlterUniqueTogether(
+            name='vote',
+            unique_together=set([('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='similaruser',
+            unique_together=set([('from_user', 'to_user')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='score',
+            unique_together=set([('content_type', 'object_id', 'key')]),
+        ),
+        migrations.AlterUniqueTogether(
+            name='ignoredobject',
+            unique_together=set([('content_type', 'object_id')]),
+        ),
+    ]

=== added file 'djangoratings/migrations/__init__.py'
=== added file 'djangoratings/models.py'
--- djangoratings/models.py	1970-01-01 00:00:00 +0000
+++ djangoratings/models.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,93 @@
+from datetime import datetime
+
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.auth.models import User
+
+try:
+    from django.utils.timezone import now
+except ImportError:
+    now = datetime.now
+
+from managers import VoteManager, SimilarUserManager
+
+class Vote(models.Model):
+    content_type    = models.ForeignKey(ContentType, related_name="votes")
+    object_id       = models.PositiveIntegerField()
+    key             = models.CharField(max_length=32)
+    score           = models.IntegerField()
+    user            = models.ForeignKey(User, blank=True, null=True, related_name="votes")
+    ip_address      = models.GenericIPAddressField()
+    cookie          = models.CharField(max_length=32, blank=True, null=True)
+    date_added      = models.DateTimeField(default=now, editable=False)
+    date_changed    = models.DateTimeField(default=now, editable=False)
+
+    objects         = VoteManager()
+
+    content_object  = GenericForeignKey()
+
+    class Meta:
+        unique_together = (('content_type', 'object_id', 'key', 'user', 'ip_address', 'cookie'))
+
+    def __unicode__(self):
+        return u"%s voted %s on %s" % (self.user_display, self.score, self.content_object)
+
+    def save(self, *args, **kwargs):
+        self.date_changed = now()
+        super(Vote, self).save(*args, **kwargs)
+
+    def user_display(self):
+        if self.user:
+            return "%s (%s)" % (self.user.username, self.ip_address)
+        return self.ip_address
+    user_display = property(user_display)
+
+    def partial_ip_address(self):
+        ip = self.ip_address.split('.')
+        ip[-1] = 'xxx'
+        return '.'.join(ip)
+    partial_ip_address = property(partial_ip_address)
+
+class Score(models.Model):
+    content_type    = models.ForeignKey(ContentType)
+    object_id       = models.PositiveIntegerField()
+    key             = models.CharField(max_length=32)
+    score           = models.IntegerField()
+    votes           = models.PositiveIntegerField()
+    
+    content_object  = GenericForeignKey()
+
+    class Meta:
+        unique_together = (('content_type', 'object_id', 'key'),)
+
+    def __unicode__(self):
+        return u"%s scored %s with %s votes" % (self.content_object, self.score, self.votes)
+
+class SimilarUser(models.Model):
+    from_user       = models.ForeignKey(User, related_name="similar_users")
+    to_user         = models.ForeignKey(User, related_name="similar_users_from")
+    agrees          = models.PositiveIntegerField(default=0)
+    disagrees       = models.PositiveIntegerField(default=0)
+    exclude         = models.BooleanField(default=False)
+    
+    objects         = SimilarUserManager()
+    
+    class Meta:
+        unique_together = (('from_user', 'to_user'),)
+
+    def __unicode__(self):
+        print u"%s %s similar to %s" % (self.from_user, self.exclude and 'is not' or 'is', self.to_user)
+
+class IgnoredObject(models.Model):
+    user            = models.ForeignKey(User)
+    content_type    = models.ForeignKey(ContentType)
+    object_id       = models.PositiveIntegerField()
+    
+    content_object  = GenericForeignKey()
+    
+    class Meta:
+        unique_together = (('content_type', 'object_id'),)
+    
+    def __unicode__(self):
+        return self.content_object

=== added file 'djangoratings/runtests.py'
--- djangoratings/runtests.py	1970-01-01 00:00:00 +0000
+++ djangoratings/runtests.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+import sys
+
+from os.path import dirname, abspath
+
+from django.conf import settings
+
+if not settings.configured:
+    settings.configure(
+        DATABASE_ENGINE='sqlite3',
+        INSTALLED_APPS=[
+            'django.contrib.auth',
+            'django.contrib.contenttypes',
+            'djangoratings',
+        ]
+    )
+
+from django.test.simple import run_tests
+
+
+def runtests(*test_args):
+    if not test_args:
+        test_args = ['djangoratings']
+    parent = dirname(abspath(__file__))
+    sys.path.insert(0, parent)
+    failures = run_tests(test_args, verbosity=1, interactive=True)
+    sys.exit(failures)
+
+
+if __name__ == '__main__':
+    runtests(*sys.argv[1:])
\ No newline at end of file

=== added directory 'djangoratings/templatetags'
=== added file 'djangoratings/templatetags/__init__.py'
=== added file 'djangoratings/templatetags/ratings.py'
--- djangoratings/templatetags/ratings.py	1970-01-01 00:00:00 +0000
+++ djangoratings/templatetags/ratings.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,89 @@
+"""
+Template tags for Django
+"""
+# TODO: add in Jinja tags if Coffin is available
+
+from django import template
+from django.contrib.contenttypes.models import ContentType
+from django.db.models import ObjectDoesNotExist
+
+from djangoratings.models import Vote
+
+register = template.Library()
+
+class RatingByRequestNode(template.Node):
+    def __init__(self, request, obj, context_var):
+        self.request = request
+        self.obj, self.field_name = obj.split('.')
+        self.context_var = context_var
+    
+    def render(self, context):
+        try:
+            request = template.resolve_variable(self.request, context)
+            obj = template.resolve_variable(self.obj, context)
+            field = getattr(obj, self.field_name)
+        except (template.VariableDoesNotExist, AttributeError):
+            return ''
+        try:
+            vote = field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES)
+            context[self.context_var] = vote
+        except ObjectDoesNotExist:
+            context[self.context_var] = 0
+        return ''
+
+def do_rating_by_request(parser, token):
+    """
+    Retrieves the ``Vote`` cast by a user on a particular object and
+    stores it in a context variable. If the user has not voted, the
+    context variable will be 0.
+    
+    Example usage::
+    
+        {% rating_by_request request on instance as vote %}
+    """
+    
+    bits = token.contents.split()
+    if len(bits) != 6:
+        raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0])
+    if bits[2] != 'on':
+        raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0])
+    if bits[4] != 'as':
+        raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0])
+    return RatingByRequestNode(bits[1], bits[3], bits[5])
+register.tag('rating_by_request', do_rating_by_request)
+
+class RatingByUserNode(RatingByRequestNode):
+    def render(self, context):
+        try:
+            user = template.resolve_variable(self.request, context)
+            obj = template.resolve_variable(self.obj, context)
+            field = getattr(obj, self.field_name)
+        except template.VariableDoesNotExist:
+            return ''
+        try:
+            vote = field.get_rating_for_user(user)
+            context[self.context_var] = vote
+        except ObjectDoesNotExist:
+            context[self.context_var] = 0
+        return ''
+
+def do_rating_by_user(parser, token):
+    """
+    Retrieves the ``Vote`` cast by a user on a particular object and
+    stores it in a context variable. If the user has not voted, the
+    context variable will be 0.
+    
+    Example usage::
+    
+        {% rating_by_user user on instance as vote %}
+    """
+    
+    bits = token.contents.split()
+    if len(bits) != 6:
+        raise template.TemplateSyntaxError("'%s' tag takes exactly five arguments" % bits[0])
+    if bits[2] != 'on':
+        raise template.TemplateSyntaxError("second argument to '%s' tag must be 'on'" % bits[0])
+    if bits[4] != 'as':
+        raise template.TemplateSyntaxError("fourth argument to '%s' tag must be 'as'" % bits[0])
+    return RatingByUserNode(bits[1], bits[3], bits[5])
+register.tag('rating_by_user', do_rating_by_user)

=== added file 'djangoratings/tests.py'
--- djangoratings/tests.py	1970-01-01 00:00:00 +0000
+++ djangoratings/tests.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,149 @@
+import unittest
+import random
+
+from django.db import models
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+from django.conf import settings
+
+from exceptions import *
+from models import Vote, SimilarUser, IgnoredObject
+from fields import AnonymousRatingField, RatingField
+
+settings.RATINGS_VOTES_PER_IP = 1
+
+class RatingTestModel(models.Model):
+    rating = AnonymousRatingField(range=2, can_change_vote=True)
+    rating2 = RatingField(range=2, can_change_vote=False)
+    
+    def __unicode__(self):
+        return unicode(self.pk)
+
+class RatingTestCase(unittest.TestCase):
+    def testRatings(self):
+        instance = RatingTestModel.objects.create()
+        
+        # Test adding votes
+        instance.rating.add(score=1, user=None, ip_address='127.0.0.1')
+        self.assertEquals(instance.rating.score, 1)
+        self.assertEquals(instance.rating.votes, 1)
+
+        # Test adding votes
+        instance.rating.add(score=2, user=None, ip_address='127.0.0.2')
+        self.assertEquals(instance.rating.score, 3)
+        self.assertEquals(instance.rating.votes, 2)
+
+        # Test changing of votes
+        instance.rating.add(score=2, user=None, ip_address='127.0.0.1')
+        self.assertEquals(instance.rating.score, 4)
+        self.assertEquals(instance.rating.votes, 2)
+        
+        # Test users
+        user = User.objects.create(username=str(random.randint(0, 100000000)))
+        user2 = User.objects.create(username=str(random.randint(0, 100000000)))
+        
+        instance.rating.add(score=2, user=user, ip_address='127.0.0.3')
+        self.assertEquals(instance.rating.score, 6)
+        self.assertEquals(instance.rating.votes, 3)
+        
+        instance.rating2.add(score=2, user=user, ip_address='127.0.0.3')
+        self.assertEquals(instance.rating2.score, 2)
+        self.assertEquals(instance.rating2.votes, 1)
+        
+        self.assertRaises(IPLimitReached, instance.rating2.add, score=2, user=user2, ip_address='127.0.0.3')
+
+        # Test deletion hooks
+        Vote.objects.filter(ip_address='127.0.0.3').delete()
+        
+        instance = RatingTestModel.objects.get(pk=instance.pk)
+
+        self.assertEquals(instance.rating.score, 4)
+        self.assertEquals(instance.rating.votes, 2)
+        self.assertEquals(instance.rating2.score, 0)
+        self.assertEquals(instance.rating2.votes, 0)
+
+class RecommendationsTestCase(unittest.TestCase):
+    def setUp(self):
+        self.instance = RatingTestModel.objects.create()
+        self.instance2 = RatingTestModel.objects.create()
+        self.instance3 = RatingTestModel.objects.create()
+        self.instance4 = RatingTestModel.objects.create()
+        self.instance5 = RatingTestModel.objects.create()
+        
+        # Test users
+        self.user = User.objects.create(username=str(random.randint(0, 100000000)))
+        self.user2 = User.objects.create(username=str(random.randint(0, 100000000)))
+    
+    def testExclusions(self):
+        Vote.objects.all().delete()
+
+        self.instance.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance2.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance3.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance4.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance5.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+
+        # we should only need to call this once
+        SimilarUser.objects.update_recommendations()
+
+        self.assertEquals(SimilarUser.objects.count(), 2)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 4)
+        
+        ct = ContentType.objects.get_for_model(RatingTestModel)
+        
+        IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance2.pk)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 3)
+
+        IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance3.pk)
+        IgnoredObject.objects.create(user=self.user2, content_type=ct, object_id=self.instance4.pk)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 1)
+        self.assertEquals(recs, [self.instance5])
+        
+        self.instance5.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 0)
+    
+    def testSimilarUsers(self):
+        Vote.objects.all().delete()
+
+        self.instance.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance2.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance3.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance4.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance5.rating.add(score=1, user=self.user, ip_address='127.0.0.1')
+        self.instance.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+        self.instance2.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+        self.instance3.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+        
+        SimilarUser.objects.update_recommendations()
+
+        self.assertEquals(SimilarUser.objects.count(), 2)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 2)
+        
+        self.instance4.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+
+        SimilarUser.objects.update_recommendations()
+
+        self.assertEquals(SimilarUser.objects.count(), 2)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 1)
+        self.assertEquals(recs, [self.instance5])
+        
+        self.instance5.rating.add(score=1, user=self.user2, ip_address='127.0.0.2')
+
+        SimilarUser.objects.update_recommendations()
+
+        self.assertEquals(SimilarUser.objects.count(), 2)
+
+        recs = list(SimilarUser.objects.get_recommendations(self.user2, RatingTestModel))
+        self.assertEquals(len(recs), 0)
\ No newline at end of file

=== added file 'djangoratings/views.py'
--- djangoratings/views.py	1970-01-01 00:00:00 +0000
+++ djangoratings/views.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,124 @@
+from django.contrib.contenttypes.models import ContentType
+from django.core.exceptions import ObjectDoesNotExist
+from django.http import HttpResponse, Http404
+
+from exceptions import *
+from django.conf import settings
+from default_settings import RATINGS_VOTES_PER_IP
+
+class AddRatingView(object):
+    def __call__(self, request, content_type_id, object_id, field_name, score):
+        """__call__(request, content_type_id, object_id, field_name, score)
+        
+        Adds a vote to the specified model field."""
+        
+        try:
+            instance = self.get_instance(content_type_id, object_id)
+        except ObjectDoesNotExist:
+            raise Http404('Object does not exist')
+        
+        context = self.get_context(request)
+        context['instance'] = instance
+        
+        try:
+            field = getattr(instance, field_name)
+        except AttributeError:
+            return self.invalid_field_response(request, context)
+        
+        context.update({
+            'field': field,
+            'score': score,
+        })
+        
+        had_voted = bool(field.get_rating_for_user(request.user, request.META['REMOTE_ADDR'], request.COOKIES))
+        
+        context['had_voted'] = had_voted
+                    
+        try:
+            adds = field.add(score, request.user, request.META.get('REMOTE_ADDR'), request.COOKIES)
+        except IPLimitReached:
+            return self.too_many_votes_from_ip_response(request, context)
+        except AuthRequired:
+            return self.authentication_required_response(request, context)
+        except InvalidRating:
+            return self.invalid_rating_response(request, context)
+        except CannotChangeVote:
+            return self.cannot_change_vote_response(request, context)
+        except CannotDeleteVote:
+            return self.cannot_delete_vote_response(request, context)
+        if had_voted:
+            return self.rating_changed_response(request, context, adds)
+        return self.rating_added_response(request, context, adds)
+    
+    def get_context(self, request, context={}):
+        return context
+    
+    def render_to_response(self, template, context, request):
+        raise NotImplementedError
+
+    def too_many_votes_from_ip_response(self, request, context):
+        response = HttpResponse('Too many votes from this IP address for this object.')
+        return response
+
+    def rating_changed_response(self, request, context, adds={}):
+        response = HttpResponse('Vote changed.')
+        if 'cookie' in adds:
+            cookie_name, cookie = adds['cookie_name'], adds['cookie']
+            if 'deleted' in adds:
+                response.delete_cookie(cookie_name)
+            else:
+                response.set_cookie(cookie_name, cookie, 31536000, path='/') # TODO: move cookie max_age to settings
+        return response
+    
+    def rating_added_response(self, request, context, adds={}):
+        response = HttpResponse('Vote recorded.')
+        if 'cookie' in adds:
+            cookie_name, cookie = adds['cookie_name'], adds['cookie']
+            if 'deleted' in adds:
+                response.delete_cookie(cookie_name)
+            else:
+                response.set_cookie(cookie_name, cookie, 31536000, path='/') # TODO: move cookie max_age to settings
+        return response
+
+    def authentication_required_response(self, request, context):
+        response = HttpResponse('You must be logged in to vote.')
+        response.status_code = 403
+        return response
+    
+    def cannot_change_vote_response(self, request, context):
+        response = HttpResponse('You have already voted.')
+        response.status_code = 403
+        return response
+    
+    def cannot_delete_vote_response(self, request, context):
+        response = HttpResponse('You can\'t delete this vote.')
+        response.status_code = 403
+        return response
+    
+    def invalid_field_response(self, request, context):
+        response = HttpResponse('Invalid field name.')
+        response.status_code = 403
+        return response
+    
+    def invalid_rating_response(self, request, context):
+        response = HttpResponse('Invalid rating value.')
+        response.status_code = 403
+        return response
+        
+    def get_instance(self, content_type_id, object_id):
+        return ContentType.objects.get(pk=content_type_id)\
+            .get_object_for_this_type(pk=object_id)
+
+
+class AddRatingFromModel(AddRatingView):
+    def __call__(self, request, model, app_label, object_id, field_name, score):
+        """__call__(request, model, app_label, object_id, field_name, score)
+        
+        Adds a vote to the specified model field."""
+        try:
+            content_type = ContentType.objects.get(model=model, app_label=app_label)
+        except ContentType.DoesNotExist:
+            raise Http404('Invalid `model` or `app_label`.')
+        
+        return super(AddRatingFromModel, self).__call__(request, content_type.id,
+                                                        object_id, field_name, score)

=== modified file 'local_settings.py.sample'
--- local_settings.py.sample	2012-07-11 17:28:26 +0000
+++ local_settings.py.sample	2016-06-28 17:58:37 +0000
@@ -2,7 +2,6 @@
 bd = "/some/path"
 bd = os.getcwd() # Better make this a static path
 
-TEMPLATE_DIRS = bd + '/templates'
 STATIC_MEDIA_PATH = bd + '/media'
 MEDIA_ROOT = bd + '/media/'
 WIDELANDS_SVN_DIR = "/path/to/widelands/trunk/"
@@ -19,4 +18,9 @@
 }
 
 
+# The following are just dummy values, but needed defined
+NORECAPTCHA_SITE_KEY = 'dummy'
+NORECAPTCHA_SECRET_KEY = 'dummy'
 
+# The logo used for mainpage
+LOGO_FILE = 'Logo_alpha.png'

=== modified file 'local_urls.py.sample'
--- local_urls.py.sample	2010-01-02 11:29:19 +0000
+++ local_urls.py.sample	2016-06-28 17:58:37 +0000
@@ -1,8 +1,8 @@
-from django.conf.urls.defaults import *
+from django.conf.urls import *
 from django.conf import settings
 
 
-local_urlpatterns = patterns('',
+local_urlpatterns = [
    url(r'^wlmedia/(?P<path>.*)$',
        'django.views.static.serve',
        {'document_root': settings.STATIC_MEDIA_PATH},
@@ -11,4 +11,4 @@
        'django.views.static.serve',
        {'document_root': settings.STATIC_MEDIA_PATH},
        name='static_media_pybb'),
-)
+]

=== modified file 'mainpage/context_processors.py'
--- mainpage/context_processors.py	2013-06-30 12:53:49 +0000
+++ mainpage/context_processors.py	2016-06-28 17:58:37 +0000
@@ -1,4 +1,6 @@
 from django.conf import settings
 
 def settings_for_templates(request):
-    return {'USE_GOOGLE_ANALYTICS': settings.USE_GOOGLE_ANALYTICS}
+    context = {'USE_GOOGLE_ANALYTICS': settings.USE_GOOGLE_ANALYTICS,
+               'LOGO_FILE': settings.LOGO_FILE}
+    return context

=== modified file 'mainpage/forms.py'
--- mainpage/forms.py	2015-08-24 19:55:58 +0000
+++ mainpage/forms.py	2016-06-28 17:58:37 +0000
@@ -1,14 +1,16 @@
 #!/usr/bin/env python -tt
 # encoding: utf-8
 
+from django import forms
 from registration.forms import RegistrationForm
-from wlrecaptcha.forms import RecaptchaForm, \
-    RecaptchaFieldPlaceholder, RecaptchaWidget
-from django import forms
-
-class RegistrationWithCaptchaForm(RegistrationForm,RecaptchaForm):
-    captcha = RecaptchaFieldPlaceholder(widget=RecaptchaWidget(theme="white"),
-                                label="Are you human?")
+from nocaptcha_recaptcha.fields import NoReCaptchaField
+from wlprofile.models import Profile as wlprofile
+
+# Overwritten form to include a captcha
+class RegistrationWithCaptchaForm(RegistrationForm):
+    captcha = NoReCaptchaField()
+
+    
 
 class ContactForm(forms.Form):
     surname = forms.CharField(max_length=80)

=== modified file 'mainpage/templatetags/wl_markdown.py'
--- mainpage/templatetags/wl_markdown.py	2015-04-01 20:01:41 +0000
+++ mainpage/templatetags/wl_markdown.py	2016-06-28 17:58:37 +0000
@@ -13,6 +13,7 @@
 from django.conf import settings
 from django.utils.encoding import smart_str, force_unicode
 from django.utils.safestring import mark_safe
+from settings import BLEACH_ALLOWED_TAGS, BLEACH_ALLOWED_ATTRIBUTES
 
 # Try to get a not so fully broken markdown module
 import markdown
@@ -20,14 +21,15 @@
     raise ImportError, "Markdown library to old!"
 from markdown import markdown
 import re
+import bleach
 
-from BeautifulSoup import BeautifulSoup
+from BeautifulSoup import BeautifulSoup, NavigableString
 
 # If we can import a Wiki module with Articles, we
 # will check for internal wikipages links in all internal
 # links starting with /wiki/
 try:
-    from widelands.wiki.models import Article, ChangeSet
+    from wiki.models import Article, ChangeSet
     check_for_missing_wikipages = True
 except ImportError:
     check_for_missing_wikipages = False
@@ -72,13 +74,13 @@
     """
     for before,after in SMILEY_PREESCAPING:
         text = text.replace(before,after)
-
     return text
 
 
 revisions_re = [
     re.compile( "bzr:r(\d+)" ),
 ]
+
 def _insert_revision( text ):
     for r in revisions_re:
         text = r.sub( lambda m: """<a href="%s">r%s</a>""" % (
@@ -171,15 +173,17 @@
     # Do Preescaping for markdown, so that some things stay intact
     # This is currently only needed for this smiley ">:-)"
     value = _insert_smiley_preescaping( value )
-
     custom = keyw.pop('custom', True)
-    # nvalue = markdown(value, extras = [ "footnotes"], *args, **keyw)
-    nvalue = smart_str(markdown(value, extensions=["extra","toc"], *args, **keyw))
+    html = smart_str(markdown(value, extensions=["extra","toc"], *args, **keyw))
+
+    # Sanitize posts from potencial untrusted users (Forum/Wiki/Maps)
+    if 'bleachit' in args:
+        html = mark_safe(bleach.clean(html, tags=BLEACH_ALLOWED_TAGS, attributes=BLEACH_ALLOWED_ATTRIBUTES))
 
     # Since we only want to do replacements outside of tags (in general) and not between
     # <a> and </a> we partition our site accordingly
     # BeautifoulSoup does all the heavy lifting
-    soup = BeautifulSoup(nvalue)
+    soup = BeautifulSoup(html)
     if len(soup.contents) == 0:
         # well, empty soup. Return it
         return unicode(soup)
@@ -205,13 +209,13 @@
 
                 rv = pattern.sub(replacement, rv)
             text.replaceWith(rv)
-
+ 
     # This call slows the whole function down...
     # unicode->reparsing.
     # The function goes from .5 ms to 1.5ms on my system
     # Well, for our site with it's little traffic it's maybe not so important...
-    soup = BeautifulSoup(unicode(soup)) # What a waste of cycles :(
-
+    # What a waste of cycles :(
+    soup = BeautifulSoup(unicode(soup))
     # We have to go over this to classify links
     for tag in soup.findAll("a"):
         rv = _classify_link(tag)
@@ -230,13 +234,12 @@
 
 
 @register.filter
-def wl_markdown(value, arg=''):
-    """
-    My own markup filter, wrapping the markup2 library, which is less bugged.
-    """
-    if arg != '':
-        return mark_safe(force_unicode(do_wl_markdown(value,safe_mode=arg)))
+def wl_markdown(content, arg=''):
+    """
+    A Filter which decides when to 'bleach' the content.
+    """
+    if arg == 'bleachit':
+        return mark_safe(do_wl_markdown(content, 'bleachit'))
     else:
-        return mark_safe(force_unicode(do_wl_markdown(value,)))
-wl_markdown.is_safe = True
+        return mark_safe(do_wl_markdown(content))
 

=== modified file 'mainpage/urls.py'
--- mainpage/urls.py	2009-02-19 17:01:00 +0000
+++ mainpage/urls.py	2016-06-28 17:58:37 +0000
@@ -1,4 +1,4 @@
-from django.conf.urls.defaults import *
+from django.conf.urls import *
 from widelands.mainpage import views
 
 urlpatterns = patterns('',

=== modified file 'mainpage/views.py'
--- mainpage/views.py	2016-02-03 08:21:08 +0000
+++ mainpage/views.py	2016-06-28 17:58:37 +0000
@@ -20,7 +20,6 @@
 
 def legal_notice(request):
     """The legal notice page to fullfill law."""
-
     if request.method == 'POST':
         form = ContactForm(request.POST)
         if form.is_valid():
@@ -32,10 +31,11 @@
                                 form.cleaned_data['inquiry']])
             sender = 'legal_note@xxxxxxxxxxxxx'
 
-            ## get email addresses which are in a tuple of ('name','email'),
+            ## get email addresses which are in form of ('name','email'),
             recipients = []
             for recipient in INQUIRY_RECIPIENTS:
                 recipients.append(recipient[1])
+                print('recipeients: ', recipients)
 
             send_mail(subject, message, sender,
                 recipients, fail_silently=False)
@@ -43,41 +43,38 @@
 
     else:
         form = ContactForm() # An unbound form
-
+    
     return render(request, 'mainpage/legal_notice.html', {
         'form': form,
+        'inquiry_recipients': INQUIRY_RECIPIENTS,
         })
 
 def legal_notice_thanks(request):
     return render(request, 'mainpage/legal_notice_thanks.html')
 
-
-from forms import RegistrationWithCaptchaForm
-from registration.backends.default import DefaultBackend
-
-
-def register(request):
-    """Overwritten view from registration to include a captcha.
-
-    We only need this because the remote IP addr must be passed to the
-    form; the registration doesn't do this
-
-    """
-    remote_ip = request.META['REMOTE_ADDR']
-    if request.method == 'POST':
-        form = RegistrationWithCaptchaForm(remote_ip, data=request.POST,
-                                           files=request.FILES)
-        if form.is_valid():
-            new_user = DefaultBackend().register(request, **form.cleaned_data)
-            return HttpResponseRedirect(reverse('registration_complete'))
-    else:
-        form = RegistrationWithCaptchaForm(remote_ip)
-
-    return render_to_response('registration/registration_form.html',
-                              {'registration_form': form},
-                              context_instance=RequestContext(request))
-
-
+from wlprofile.models import Profile
+from registration.backends.hmac.views import RegistrationView
+from django.contrib.auth.models import User
+
+class OwnRegistrationView(RegistrationView):
+    """
+    Overwriting the default function to save also the extended User model (wlprofile)
+    """
+    def create_inactive_user(self, form):
+        """
+        Additionally save the custom enxtended user data.
+        """
+        new_user = form.save(commit=False)
+        new_user.is_active = False
+        new_user.save()
+        reg_user = User.objects.get(username=new_user)
+        ext_profile = Profile(user=reg_user)
+        ext_profile.save()
+
+        self.send_activation_email(new_user)
+
+        return new_user
+    
 def developers(request):
     """This reads out some json files in the SVN directory, and returns it as a
     wl_markdown_object.

=== modified file 'manage.py'
--- manage.py	2009-02-19 17:01:00 +0000
+++ manage.py	2016-06-28 17:58:37 +0000
@@ -1,11 +1,10 @@
 #!/usr/bin/env python
-from django.core.management import execute_manager
-try:
-    import settings # Assumed to be in the same directory.
-except ImportError:
-    import sys
-    sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
-    sys.exit(1)
+import os
+import sys
 
 if __name__ == "__main__":
-    execute_manager(settings)
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+
+    from django.core.management import execute_from_command_line
+
+    execute_from_command_line(sys.argv)

=== modified file 'media/css/base.css'
--- media/css/base.css	2016-02-09 18:05:18 +0000
+++ media/css/base.css	2016-06-28 17:58:37 +0000
@@ -163,6 +163,9 @@
 	text-align: center;
 }
 
+.right  {
+	text-align: right;
+}
 .middle {
 	vertical-align: middle;
 }

=== added file 'media/img/Logo_alpha.png'
Binary files media/img/Logo_alpha.png	1970-01-01 00:00:00 +0000 and media/img/Logo_alpha.png	2016-06-28 17:58:37 +0000 differ
=== added directory 'media/wlimages'
=== added directory 'media/wlprofile/avatars'
=== modified file 'news/admin.py'
--- news/admin.py	2009-02-21 18:24:02 +0000
+++ news/admin.py	2016-06-28 17:58:37 +0000
@@ -1,5 +1,5 @@
 from django.contrib import admin
-from widelands.news.models import *
+from news.models import *
 
 
 class CategoryAdmin(admin.ModelAdmin):

=== modified file 'news/feeds.py'
--- news/feeds.py	2011-08-24 13:09:49 +0000
+++ news/feeds.py	2016-06-28 17:58:37 +0000
@@ -1,27 +1,26 @@
-from django.contrib.syndication.feeds import FeedDoesNotExist
+from django.contrib.syndication.views import Feed, FeedDoesNotExist
 from django.core.exceptions import ObjectDoesNotExist
-from django.contrib.sites.models import Site
-from django.contrib.syndication.feeds import Feed
 from django.core.urlresolvers import reverse
-from widelands.news.models import Post, Category
-
-
+from news.models import Post, Category
+
+# Validated through http://validator.w3.org/feed/
 class NewsPostsFeed(Feed):
-    title = 'Widelands news posts feed'
+    # RSS Feed
+    title = 'Widelands news feed'
     description = 'The news section from the widelands.org homepage'
-    title_template = 'feeds/posts_title.html'
-    description_template = 'feeds/posts_description.html'
+    title_template = 'news/feeds/posts_title.html'
+    description_template = 'news/feeds/posts_description.html'
+
+    def items(self):
+        return Post.objects.published()[:10]
 
     def link(self):
         return reverse('news_index')
 
-    def items(self):
-        return Post.objects.published()[:10]
-
-    def item_pubdate(self, obj):
-        return obj.publish
-
-
+    def item_pubdate(self, item):
+        return item.publish
+
+# Currently not used / not checked for compatibility for django 1.8
 class NewsPostsByCategory(Feed):
     title = 'Widelands.org posts category feed'
 
@@ -30,13 +29,13 @@
             raise ObjectDoesNotExist
         return Category.objects.get(slug__exact=bits[0])
 
-    def link(self, obj):
-        if not obj:
+    def link(self, item):
+        if not item:
             raise FeedDoesNotExist
-        return obj.get_absolute_url()
-
-    def description(self, obj):
-        return "Posts recently categorized as %s" % obj.title
-
-    def items(self, obj):
-        return obj.post_set.published()[:10]
+        return item.get_absolute_url()
+
+    def description(self, item):
+        return "Posts recently categorized as %s" % item.title
+
+    def items(self, item):
+        return item.post_set.published()[:10]

=== modified file 'news/managers.py'
--- news/managers.py	2009-02-21 18:24:02 +0000
+++ news/managers.py	2016-06-28 17:58:37 +0000
@@ -6,4 +6,4 @@
     """Returns published posts that are not in the future."""
     
     def published(self):
-        return self.get_query_set().filter(status__gte=2, publish__lte=datetime.datetime.now())
\ No newline at end of file
+        return self.get_queryset().filter(status__gte=2, publish__lte=datetime.datetime.now())
\ No newline at end of file

=== added directory 'news/migrations'
=== added file 'news/migrations/0001_initial.py'
--- news/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ news/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+import news.models
+import tagging.fields
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Category',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('title', models.CharField(max_length=100, verbose_name='title')),
+                ('slug', models.SlugField(unique=True, verbose_name='slug')),
+                ('image', models.ImageField(upload_to=news.models.get_upload_name)),
+            ],
+            options={
+                'ordering': ('title',),
+                'db_table': 'news_categories',
+                'verbose_name': 'category',
+                'verbose_name_plural': 'categories',
+            },
+        ),
+        migrations.CreateModel(
+            name='Post',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('title', models.CharField(max_length=200, verbose_name='title')),
+                ('slug', models.SlugField(verbose_name='slug', unique_for_date=b'publish')),
+                ('body', models.TextField(verbose_name='body')),
+                ('tease', models.TextField(verbose_name='tease', blank=True)),
+                ('status', models.IntegerField(default=2, verbose_name='status', choices=[(1, 'Draft'), (2, 'Public')])),
+                ('allow_comments', models.BooleanField(default=True, verbose_name='allow comments')),
+                ('publish', models.DateTimeField(verbose_name='publish')),
+                ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')),
+                ('modified', models.DateTimeField(auto_now=True, verbose_name='modified')),
+                ('tags', tagging.fields.TagField(max_length=255, blank=True)),
+                ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL, null=True)),
+                ('categories', models.ManyToManyField(to='news.Category', blank=True)),
+            ],
+            options={
+                'ordering': ('-publish',),
+                'db_table': 'news_posts',
+                'verbose_name': 'post',
+                'verbose_name_plural': 'posts',
+                'get_latest_by': 'publish',
+            },
+        ),
+    ]

=== added file 'news/migrations/__init__.py'
=== modified file 'news/models.py'
--- news/models.py	2011-08-25 13:42:22 +0000
+++ news/models.py	2016-06-28 17:58:37 +0000
@@ -3,7 +3,7 @@
 from django.db.models import permalink
 from django.contrib.auth.models import User
 from tagging.fields import TagField
-from widelands.news.managers import PublicManager
+from news.managers import PublicManager
 from django.core.urlresolvers import reverse
 
 import settings
@@ -51,7 +51,7 @@
     )
     title           = models.CharField(_('title'), max_length=200)
     slug            = models.SlugField(_('slug'), unique_for_date='publish')
-    author          = models.ForeignKey(User, blank=True, null=True)
+    author          = models.ForeignKey(User, null=True)
     body            = models.TextField(_('body'))
     tease           = models.TextField(_('tease'), blank=True)
     status          = models.IntegerField(_('status'), choices=STATUS_CHOICES, default=2)
@@ -114,7 +114,7 @@
         return ('news_detail', None, {
             'slug': self.slug,
             'year': self.publish.year,
-            'month': self.publish.strftime('%m'),
+            'month': self.publish.strftime('%b'),
             'day': self.publish.day,
         })
     

=== removed file 'news/templatetags/news.py'
--- news/templatetags/news.py	2010-06-14 18:12:31 +0000
+++ news/templatetags/news.py	1970-01-01 00:00:00 +0000
@@ -1,136 +0,0 @@
-from django.template.loader import render_to_string
-from django import template
-from django.conf import settings
-from django.db import models
-
-import re
-
-Post = models.get_model('news', 'post')
-Category = models.get_model('news', 'category')
-
-register = template.Library()
-
-
-class LatestPosts(template.Node):
-    def __init__(self, limit, var_name):
-        self.limit = limit
-        self.var_name = var_name
-
-    def render(self, context):
-        posts = Post.objects.published()[:int(self.limit)]
-        if posts and (int(self.limit) == 1):
-            context[self.var_name] = posts[0]
-        else:
-            context[self.var_name] = posts
-        return ''
-
-@register.tag
-def get_latest_posts(parser, token):
-    """
-    Gets any number of latest posts and stores them in a varable.
-
-    Syntax::
-
-        {% get_latest_posts [limit] as [var_name] %}
-
-    Example usage::
-
-        {% get_latest_posts 10 as latest_post_list %}
-    """
-    try:
-        tag_name, arg = token.contents.split(None, 1)
-    except ValueError:
-        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
-    m = re.search(r'(.*?) as (\w+)', arg)
-    if not m:
-        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
-    format_string, var_name = m.groups()
-    return LatestPosts(format_string, var_name)
-
-class NewsYears(template.Node):
-    def __init__(self, var_name):
-        self.var_name = var_name
-
-    def render(self, context):
-        years = Post.objects.all().dates('publish', 'year')
-        context[self.var_name] = years
-        return ''
-
-@register.tag
-def get_news_years(parser, token):
-    """
-    Gets any number of latest posts and stores them in a varable.
-
-    Syntax::
-
-    {% get_latest_posts [limit] as [var_name] %}
-
-    Example usage::
-
-    {% get_latest_posts 10 as latest_post_list %}
-    """
-    try:
-        tag_name, arg = token.contents.split(None, 1)
-    except ValueError:
-        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
-    m = re.search(r'as (\w+)', arg)
-    if not m:
-        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
-    (var_name, ) = m.groups()
-    return NewsYears(var_name)
-
-class NewsCategories(template.Node):
-    def __init__(self, var_name):
-        self.var_name = var_name
-
-    def render(self, context):
-        categories = Category.objects.all()
-        context[self.var_name] = categories
-        return ''
-
-@register.tag
-def get_news_categories(parser, token):
-    """
-    Gets all news categories.
-
-    Syntax::
-
-        {% get_news_categories as [var_name] %}
-
-    Example usage::
-
-        {% get_news_categories as category_list %}
-    """
-    try:
-        tag_name, arg = token.contents.split(None, 1)
-    except ValueError:
-        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
-    m = re.search(r'as (\w+)', arg)
-    if not m:
-        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
-    var_name = m.groups()[0]
-    return NewsCategories(var_name)
-
-
-@register.filter
-def get_links(value):
-    """
-    Extracts links from a ``Post`` body and returns a list.
-
-    Template Syntax::
-
-        {{ post.body|markdown:"safe"|get_links }}
-
-    """
-    try:
-        try:
-            from BeautifulSoup import BeautifulSoup
-        except ImportError:
-            from beautifulsoup import BeautifulSoup
-        soup = BeautifulSoup(value)
-        return soup.findAll('a')
-    except ImportError:
-        if settings.DEBUG:
-            raise template.TemplateSyntaxError, "Error in 'get_links' filter: BeautifulSoup isn't installed."
-    return value
-

=== added file 'news/templatetags/news_extras.py'
--- news/templatetags/news_extras.py	1970-01-01 00:00:00 +0000
+++ news/templatetags/news_extras.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,133 @@
+from django.template.loader import render_to_string
+from django import template
+from django.conf import settings
+from django.apps import apps
+
+import re
+
+Post = apps.get_model('news', 'post')
+Category = apps.get_model('news', 'category')
+
+register = template.Library()
+
+
+class LatestPosts(template.Node):
+    def __init__(self, limit, var_name):
+        self.limit = limit
+        self.var_name = var_name
+
+    def render(self, context):
+        posts = Post.objects.published()[:int(self.limit)]
+        if posts and (int(self.limit) == 1):
+            context[self.var_name] = posts[0]
+        else:
+            context[self.var_name] = posts
+        return ''
+
+@register.tag
+def get_latest_posts(parser, token):
+    """
+    Gets any number of latest posts and stores them in a varable.
+
+    Syntax::
+
+        {% get_latest_posts [limit] as [var_name] %}
+
+    Example usage::
+
+        {% get_latest_posts 10 as latest_post_list %}
+    """
+    try:
+        tag_name, arg = token.contents.split(None, 1)
+    except ValueError:
+        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
+    m = re.search(r'(.*?) as (\w+)', arg)
+    if not m:
+        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
+    format_string, var_name = m.groups()
+    return LatestPosts(format_string, var_name)
+
+class NewsYears(template.Node):
+    def __init__(self, var_name):
+        self.var_name = var_name
+
+    def render(self, context):
+        years = Post.objects.all().dates('publish', 'year')
+        context[self.var_name] = years
+        return ''
+
+@register.tag
+def get_news_years(parser, token):
+    """
+    Gets any number of latest posts and stores them in a varable.
+
+    Syntax::
+
+    {% get_latest_posts [limit] as [var_name] %}
+
+    Example usage::
+
+    {% get_latest_posts 10 as latest_post_list %}
+    """
+    try:
+        tag_name, arg = token.contents.split(None, 1)
+    except ValueError:
+        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
+    m = re.search(r'as (\w+)', arg)
+    if not m:
+        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
+    (var_name, ) = m.groups()
+    return NewsYears(var_name)
+
+class NewsCategories(template.Node):
+    def __init__(self, var_name):
+        self.var_name = var_name
+
+    def render(self, context):
+        categories = Category.objects.all()
+        context[self.var_name] = categories
+        return ''
+
+@register.tag
+def get_news_categories(parser, token):
+    """
+    Gets all news categories.
+
+    Syntax::
+
+        {% get_news_categories as [var_name] %}
+
+    Example usage::
+
+        {% get_news_categories as category_list %}
+    """
+    try:
+        tag_name, arg = token.contents.split(None, 1)
+    except ValueError:
+        raise template.TemplateSyntaxError, "%s tag requires arguments" % token.contents.split()[0]
+    m = re.search(r'as (\w+)', arg)
+    if not m:
+        raise template.TemplateSyntaxError, "%s tag had invalid arguments" % tag_name
+    var_name = m.groups()[0]
+    return NewsCategories(var_name)
+
+
+@register.filter
+def get_links(value):
+    """
+    Extracts links from a ``Post`` body and returns a list.
+
+    Template Syntax::
+
+        {{ post.body|markdown:"safe"|get_links }}
+
+    """
+    try:
+        from BeautifulSoup import BeautifulSoup
+        soup = BeautifulSoup(value)
+        return soup.findAll('a')
+    except ImportError:
+        if settings.DEBUG:
+            raise template.TemplateSyntaxError, "Error in 'get_links' filter: BeautifulSoup isn't installed."
+    return value
+

=== modified file 'news/urls.py'
--- news/urls.py	2010-06-14 18:12:31 +0000
+++ news/urls.py	2016-06-28 17:58:37 +0000
@@ -1,37 +1,42 @@
-from django.conf.urls.defaults import *
-from widelands.news import views as news_views
-
-
-urlpatterns = patterns('',
-    url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/(?P<slug>[-\w]+)/$',
-        view=news_views.post_detail,
+from django.conf.urls import *
+from news.models import Post
+from django.views.generic.dates import DateDetailView, YearArchiveView, MonthArchiveView
+from django.views.generic import ListView
+
+urlpatterns = [ 
+     url(r'^(?P<year>[0-9]{4})/(?P<month>[-\w]+)/(?P<day>[0-9]+)/(?P<slug>[-\w]+)/$',
+        DateDetailView.as_view(model=Post, date_field="publish", template_name="news/post_detail.html"),
         name='news_detail'),
-    
-     url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$',
-         view=news_views.post_archive_day,
-         name='news_archive_day'),
-    
-     url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/$',
-         view=news_views.post_archive_month,
-         name='news_archive_month'),
-    
-     url(r'^(?P<year>\d{4})/$',
-         view=news_views.post_archive_year,
-         name='news_archive_year'),
-
-    # url(r'^categories/(?P<slug>[-\w]+)/$',
-    #     view=news_views.category_detail,
-    #     name='news_category_detail'),
-    #
-    # url (r'^categories/$',
-    #     view=news_views.category_list,
-    #     name='news_category_list'),
-
-    url(r'^page/(?P<page>\w)/$',
-        view=news_views.post_list,
-        name='news_index_paginated'),
-
-    url(r'^$',
-        view=news_views.post_list,
-        name='news_index'),
-)
+
+    #  url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/(?P<day>\d{1,2})/$',
+    #      view=news_views.post_archive_day,
+    #      name='news_archive_day'),
+    # 
+    #  url(r'^(?P<year>\d{4})/(?P<month>\d{1,2})/$',
+    #      view=news_views.post_archive_month,
+    #      name='news_archive_month'),
+    # 
+      url(r'^(?P<year>\d{4})/(?P<month>[-\w]+)/$',
+          MonthArchiveView.as_view(model=Post, date_field="publish"),
+          name='news_archive_month'),
+
+      url(r'^(?P<year>\d{4})/$',
+          YearArchiveView.as_view(model=Post, make_object_list=True, date_field="publish", template_name="post_archive_year.html"),
+          name='news_archive_year'),
+    # 
+    # # url(r'^categories/(?P<slug>[-\w]+)/$',
+    # #     view=news_views.category_detail,
+    # #     name='news_category_detail'),
+    # #
+    # # url (r'^categories/$',
+    # #     view=news_views.category_list,
+    # #     name='news_category_list'),
+    # 
+    # url(r'^page/(?P<page>\w)/$',
+    #     ListView.as_view(model=Post, template_name="news/post_list.html"),
+    #     name='news_index_paginated'),
+     
+     url(r'^$',
+         ListView.as_view(model=Post, template_name="news/post_list.html"),
+         name='news_index'),
+]

=== removed file 'news/views.py'
--- news/views.py	2010-06-14 18:12:31 +0000
+++ news/views.py	1970-01-01 00:00:00 +0000
@@ -1,162 +0,0 @@
-from django.shortcuts import render_to_response, get_object_or_404
-from django.template import RequestContext
-from django.http import Http404
-from django.views.generic import date_based, list_detail
-from django.db.models import Q
-from widelands.news.models import *
-
-import datetime
-import re
-
-
-def post_list(request, page=0, **kwargs):
-    return list_detail.object_list(
-        request,
-        queryset = Post.objects.published(),
-        page = page,
-        **kwargs
-    )
-post_list.__doc__ = list_detail.object_list.__doc__
-
-
-def post_archive_year(request, year, **kwargs):
-    return date_based.archive_year(
-        request,
-        year = year,
-        date_field = 'publish',
-        queryset = Post.objects.published(),
-        make_object_list = True,
-        **kwargs
-    )
-post_archive_year.__doc__ = date_based.archive_year.__doc__
-
-
-def post_archive_month(request, year, month, **kwargs):
-    return date_based.archive_month(
-        request,
-        year = year,
-        month = month,
-        month_format = "%m",
-        date_field = 'publish',
-        queryset = Post.objects.published(),
-        **kwargs
-    )
-post_archive_month.__doc__ = date_based.archive_month.__doc__
-
-
-def post_archive_day(request, year, month, day, **kwargs):
-    return date_based.archive_day(
-        request,
-        year = year,
-        month = month,
-        month_format = "%m",
-        day = day,
-        date_field = 'publish',
-        queryset = Post.objects.published(),
-        **kwargs
-    )
-post_archive_day.__doc__ = date_based.archive_day.__doc__
-
-
-def post_detail(request, slug, year, month, day, **kwargs):
-    return date_based.object_detail(
-        request,
-        year = year,
-        month = month,
-        month_format = "%m",
-        day = day,
-        date_field = 'publish',
-        slug = slug,
-        queryset = Post.objects.published(),
-        **kwargs
-    )
-post_detail.__doc__ = date_based.object_detail.__doc__
-
-
-def category_list(request, template_name = 'news/category_list.html', **kwargs):
-    """
-    Category list
-
-    Template: ``news/category_list.html``
-    Context:
-        object_list
-            List of categories.
-    """
-    return list_detail.object_list(
-        request,
-        queryset = Category.objects.all(),
-        template_name = template_name,
-        **kwargs
-    )
-
-def category_detail(request, slug, template_name = 'news/category_detail.html', **kwargs):
-    """
-    Category detail
-
-    Template: ``news/category_detail.html``
-    Context:
-        object_list
-            List of posts specific to the given category.
-        category
-            Given category.
-    """
-    category = get_object_or_404(Category, slug__iexact=slug)
-
-    return list_detail.object_list(
-        request,
-        queryset = category.post_set.published(),
-        extra_context = {'category': category},
-        template_name = template_name,
-        **kwargs
-    )
-
-
-# Stop Words courtesy of http://www.dcs.gla.ac.uk/idom/ir_resources/linguistic_utils/stop_words
-STOP_WORDS = r"""\b(a|about|above|across|after|afterwards|again|against|all|almost|alone|along|already|also|
-although|always|am|among|amongst|amoungst|amount|an|and|another|any|anyhow|anyone|anything|anyway|anywhere|are|
-around|as|at|back|be|became|because|become|becomes|becoming|been|before|beforehand|behind|being|below|beside|
-besides|between|beyond|bill|both|bottom|but|by|call|can|cannot|cant|co|computer|con|could|couldnt|cry|de|describe|
-detail|do|done|down|due|during|each|eg|eight|either|eleven|else|elsewhere|empty|enough|etc|even|ever|every|everyone|
-everything|everywhere|except|few|fifteen|fify|fill|find|fire|first|five|for|former|formerly|forty|found|four|from|
-front|full|further|get|give|go|had|has|hasnt|have|he|hence|her|here|hereafter|hereby|herein|hereupon|hers|herself|
-him|himself|his|how|however|hundred|i|ie|if|in|inc|indeed|interest|into|is|it|its|itself|keep|last|latter|latterly|
-least|less|ltd|made|many|may|me|meanwhile|might|mill|mine|more|moreover|most|mostly|move|much|must|my|myself|name|
-namely|neither|never|nevertheless|next|nine|no|nobody|none|noone|nor|not|nothing|now|nowhere|of|off|often|on|once|
-one|only|onto|or|other|others|otherwise|our|ours|ourselves|out|over|own|part|per|perhaps|please|put|rather|re|same|
-see|seem|seemed|seeming|seems|serious|several|she|should|show|side|since|sincere|six|sixty|so|some|somehow|someone|
-something|sometime|sometimes|somewhere|still|such|system|take|ten|than|that|the|their|them|themselves|then|thence|
-there|thereafter|thereby|therefore|therein|thereupon|these|they|thick|thin|third|this|those|though|three|through|
-throughout|thru|thus|to|together|too|top|toward|towards|twelve|twenty|two|un|under|until|up|upon|us|very|via|was|
-we|well|were|what|whatever|when|whence|whenever|where|whereafter|whereas|whereby|wherein|whereupon|wherever|whether|
-which|while|whither|who|whoever|whole|whom|whose|why|will|with|within|without|would|yet|you|your|yours|yourself|
-yourselves)\b"""
-
-
-def search(request, template_name='news/post_search.html'):
-    """
-    Search for news posts.
-
-    This template will allow you to setup a simple search form that will try to return results based on
-    given search strings. The queries will be put through a stop words filter to remove words like
-    'the', 'a', or 'have' to help imporve the result set.
-
-    Template: ``news/post_search.html``
-    Context:
-        object_list
-            List of news posts that match given search term(s).
-        search_term
-            Given search term.
-    """
-    context = {}
-    if request.GET:
-        stop_word_list = re.compile(STOP_WORDS, re.IGNORECASE)
-        search_term = '%s' % request.GET['q']
-        cleaned_search_term = stop_word_list.sub('', search_term)
-        cleaned_search_term = cleaned_search_term.strip()
-        if len(cleaned_search_term) != 0:
-            post_list = Post.objects.published().filter(Q(body__icontains=cleaned_search_term) | Q(tags__icontains=cleaned_search_term) | Q(categories__title__icontains=cleaned_search_term))
-            context = {'object_list': post_list, 'search_term':search_term}
-        else:
-            message = 'Search term was too vague. Please try again.'
-            context = {'message':message}
-    return render_to_response(template_name, context, context_instance=RequestContext(request))

=== added file 'news/views.py.delete'
--- news/views.py.delete	1970-01-01 00:00:00 +0000
+++ news/views.py.delete	2016-06-28 17:58:37 +0000
@@ -0,0 +1,164 @@
+from django.shortcuts import render_to_response, get_object_or_404
+from django.template import RequestContext
+from django.http import Http404
+#from django.views.generic import date_based, list_detail
+from django.db.models import Q
+from news.models import *
+from django.views import generic
+from django.views.generic.dates import DateDetailView
+
+import datetime
+import re
+
+
+    
+def post_list(request, page=0, **kwargs):
+    return list_detail.object_list(
+        request,
+        queryset = Post.objects.published(),
+        page = page,
+        **kwargs
+    )
+#post_list.__doc__ = list_detail.object_list.__doc__
+
+def post_archive_year(request, year, **kwargs):
+    return date_based.archive_year(
+        request,
+        year = year,
+        date_field = 'publish',
+        queryset = Post.objects.published(),
+        make_object_list = True,
+        **kwargs
+    )
+#post_archive_year.__doc__ = date_based.archive_year.__doc__
+
+
+def post_archive_month(request, year, month, **kwargs):
+    return date_based.archive_month(
+        request,
+        year = year,
+        month = month,
+        month_format = "%m",
+        date_field = 'publish',
+        queryset = Post.objects.published(),
+        **kwargs
+    )
+#post_archive_month.__doc__ = date_based.archive_month.__doc__
+
+
+def post_archive_day(request, year, month, day, **kwargs):
+    return date_based.archive_day(
+        request,
+        year = year,
+        month = month,
+        month_format = "%m",
+        day = day,
+        date_field = 'publish',
+        queryset = Post.objects.published(),
+        **kwargs
+    )
+#post_archive_day.__doc__ = date_based.archive_day.__doc__
+
+
+def post_detail(request, slug, year, month, day, **kwargs):
+    return date_based.object_detail(
+        request,
+        year = year,
+        month = month,
+        month_format = "%m",
+        day = day,
+        date_field = 'publish',
+        slug = slug,
+        queryset = Post.objects.published(),
+        **kwargs
+    )
+#post_detail.__doc__ = date_based.object_detail.__doc__
+
+
+def category_list(request, template_name = 'news/category_list.html', **kwargs):
+    """
+    Category list
+
+    Template: ``news/category_list.html``
+    Context:
+        object_list
+            List of categories.
+    """
+    return list_detail.object_list(
+        request,
+        queryset = Category.objects.all(),
+        template_name = template_name,
+        **kwargs
+    )
+
+def category_detail(request, slug, template_name = 'news/category_detail.html', **kwargs):
+    """
+    Category detail
+
+    Template: ``news/category_detail.html``
+    Context:
+        object_list
+            List of posts specific to the given category.
+        category
+            Given category.
+    """
+    category = get_object_or_404(Category, slug__iexact=slug)
+
+    return list_detail.object_list(
+        request,
+        queryset = category.post_set.published(),
+        extra_context = {'category': category},
+        template_name = template_name,
+        **kwargs
+    )
+
+
+# Stop Words courtesy of http://www.dcs.gla.ac.uk/idom/ir_resources/linguistic_utils/stop_words
+STOP_WORDS = r"""\b(a|about|above|across|after|afterwards|again|against|all|almost|alone|along|already|also|
+although|always|am|among|amongst|amoungst|amount|an|and|another|any|anyhow|anyone|anything|anyway|anywhere|are|
+around|as|at|back|be|became|because|become|becomes|becoming|been|before|beforehand|behind|being|below|beside|
+besides|between|beyond|bill|both|bottom|but|by|call|can|cannot|cant|co|computer|con|could|couldnt|cry|de|describe|
+detail|do|done|down|due|during|each|eg|eight|either|eleven|else|elsewhere|empty|enough|etc|even|ever|every|everyone|
+everything|everywhere|except|few|fifteen|fify|fill|find|fire|first|five|for|former|formerly|forty|found|four|from|
+front|full|further|get|give|go|had|has|hasnt|have|he|hence|her|here|hereafter|hereby|herein|hereupon|hers|herself|
+him|himself|his|how|however|hundred|i|ie|if|in|inc|indeed|interest|into|is|it|its|itself|keep|last|latter|latterly|
+least|less|ltd|made|many|may|me|meanwhile|might|mill|mine|more|moreover|most|mostly|move|much|must|my|myself|name|
+namely|neither|never|nevertheless|next|nine|no|nobody|none|noone|nor|not|nothing|now|nowhere|of|off|often|on|once|
+one|only|onto|or|other|others|otherwise|our|ours|ourselves|out|over|own|part|per|perhaps|please|put|rather|re|same|
+see|seem|seemed|seeming|seems|serious|several|she|should|show|side|since|sincere|six|sixty|so|some|somehow|someone|
+something|sometime|sometimes|somewhere|still|such|system|take|ten|than|that|the|their|them|themselves|then|thence|
+there|thereafter|thereby|therefore|therein|thereupon|these|they|thick|thin|third|this|those|though|three|through|
+throughout|thru|thus|to|together|too|top|toward|towards|twelve|twenty|two|un|under|until|up|upon|us|very|via|was|
+we|well|were|what|whatever|when|whence|whenever|where|whereafter|whereas|whereby|wherein|whereupon|wherever|whether|
+which|while|whither|who|whoever|whole|whom|whose|why|will|with|within|without|would|yet|you|your|yours|yourself|
+yourselves)\b"""
+
+
+def search(request, template_name='news/post_search.html'):
+    """
+    Search for news posts.
+
+    This template will allow you to setup a simple search form that will try to return results based on
+    given search strings. The queries will be put through a stop words filter to remove words like
+    'the', 'a', or 'have' to help imporve the result set.
+
+    Template: ``news/post_search.html``
+    Context:
+        object_list
+            List of news posts that match given search term(s).
+        search_term
+            Given search term.
+    """
+    context = {}
+    if request.GET:
+        stop_word_list = re.compile(STOP_WORDS, re.IGNORECASE)
+        search_term = '%s' % request.GET['q']
+        cleaned_search_term = stop_word_list.sub('', search_term)
+        cleaned_search_term = cleaned_search_term.strip()
+        if len(cleaned_search_term) != 0:
+            post_list = Post.objects.published().filter(Q(body__icontains=cleaned_search_term) | Q(tags__icontains=cleaned_search_term) | Q(categories__title__icontains=cleaned_search_term))
+            context = {'object_list': post_list, 'search_term':search_term}
+        else:
+            message = 'Search term was too vague. Please try again.'
+            context = {'message':message}
+    return render_to_response(template_name, context, context_instance=RequestContext(request))

=== added directory 'notification'
=== added file 'notification/AUTHORS'
--- notification/AUTHORS	1970-01-01 00:00:00 +0000
+++ notification/AUTHORS	2016-06-28 17:58:37 +0000
@@ -0,0 +1,13 @@
+
+The PRIMARY AUTHORS are:
+
+	* James Tauber
+	* Brian Rosner
+	* Jannis Leidel
+
+ADDITIONAL CONTRIBUTORS include:
+
+	* Eduardo Padoan
+	* Fabian Neumann
+	* Juanjo Conti
+	* Michael Trier

=== added file 'notification/LICENSE'
--- notification/LICENSE	1970-01-01 00:00:00 +0000
+++ notification/LICENSE	2016-06-28 17:58:37 +0000
@@ -0,0 +1,22 @@
+Copyright (c) 2008 James Tauber and contributors
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file

=== added file 'notification/README'
--- notification/README	1970-01-01 00:00:00 +0000
+++ notification/README	2016-06-28 17:58:37 +0000
@@ -0,0 +1,19 @@
+This is the old version (0.1.4) of the notification app by James Tauber.
+I have included it as a widelands app because the new version is 
+incompatible with our old data.
+
+See the file LICENSE for Copyright notice.
+
+Original Description:
+=====================
+
+Many sites need to notify users when certain events have occurred and to allow
+configurable options as to how those notifications are to be received.
+
+The project aims to provide a Django app for this sort of functionality. This
+includes:
+
+ * submission of notification messages by other apps
+ * notification messages on signing in
+ * notification messages via email (configurable by user)
+ * notification messages via feed

=== added file 'notification/__init__.py'
--- notification/__init__.py	1970-01-01 00:00:00 +0000
+++ notification/__init__.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,9 @@
+VERSION = (0, 1, 4, "final")
+
+def get_version():
+    if VERSION[3] != "final":
+        return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3])
+    else:
+        return "%s.%s.%s" % (VERSION[0], VERSION[1], VERSION[2])
+
+__version__ = get_version()
\ No newline at end of file

=== added file 'notification/admin.py'
--- notification/admin.py	1970-01-01 00:00:00 +0000
+++ notification/admin.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,17 @@
+from django.contrib import admin
+from notification.models import NoticeType, NoticeSetting, Notice, ObservedItem
+
+class NoticeTypeAdmin(admin.ModelAdmin):
+    list_display = ('label', 'display', 'description', 'default')
+
+class NoticeSettingAdmin(admin.ModelAdmin):
+    list_display = ('id', 'user', 'notice_type', 'medium', 'send')
+
+class NoticeAdmin(admin.ModelAdmin):
+    list_display = ('message', 'user', 'notice_type', 'added', 'unseen', 'archived')
+
+
+admin.site.register(NoticeType, NoticeTypeAdmin)
+admin.site.register(NoticeSetting, NoticeSettingAdmin)
+admin.site.register(Notice, NoticeAdmin)
+admin.site.register(ObservedItem)

=== added file 'notification/atomformat.py'
--- notification/atomformat.py	1970-01-01 00:00:00 +0000
+++ notification/atomformat.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,542 @@
+# 
+# django-atompub by James Tauber <http://jtauber.com/>
+# http://code.google.com/p/django-atompub/
+# An implementation of the Atom format and protocol for Django
+# 
+# For instructions on how to use this module to generate Atom feeds,
+# see http://code.google.com/p/django-atompub/wiki/UserGuide
+# 
+# 
+# Copyright (c) 2007, James Tauber
+# 
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+# 
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+# 
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+# 
+
+from xml.sax.saxutils import XMLGenerator
+from datetime import datetime
+
+
+GENERATOR_TEXT = 'django-atompub'
+GENERATOR_ATTR = {
+    'uri': 'http://code.google.com/p/django-atompub/',
+    'version': 'r33'
+}
+
+
+
+## based on django.utils.xmlutils.SimplerXMLGenerator
+class SimplerXMLGenerator(XMLGenerator):
+    def addQuickElement(self, name, contents=None, attrs=None):
+        "Convenience method for adding an element with no children"
+        if attrs is None: attrs = {}
+        self.startElement(name, attrs)
+        if contents is not None:
+            self.characters(contents)
+        self.endElement(name)
+
+
+
+## based on django.utils.feedgenerator.rfc3339_date
+def rfc3339_date(date):
+    return date.strftime('%Y-%m-%dT%H:%M:%SZ')
+
+
+
+## based on django.utils.feedgenerator.get_tag_uri
+def get_tag_uri(url, date):
+    "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id";
+    tag = re.sub('^http://', '', url)
+    if date is not None:
+        tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
+    tag = re.sub('#', '/', tag)
+    return 'tag:' + tag
+
+
+
+## based on django.contrib.syndication.feeds.Feed
+class Feed(object):
+    
+    
+    VALIDATE = True
+    
+    
+    def __init__(self, slug, feed_url):
+        # @@@ slug and feed_url are not used yet
+        pass
+    
+    
+    def __get_dynamic_attr(self, attname, obj, default=None):
+        try:
+            attr = getattr(self, attname)
+        except AttributeError:
+            return default
+        if callable(attr):
+            # Check func_code.co_argcount rather than try/excepting the
+            # function and catching the TypeError, because something inside
+            # the function may raise the TypeError. This technique is more
+            # accurate.
+            if hasattr(attr, 'func_code'):
+                argcount = attr.func_code.co_argcount
+            else:
+                argcount = attr.__call__.func_code.co_argcount
+            if argcount == 2: # one argument is 'self'
+                return attr(obj)
+            else:
+                return attr()
+        return attr
+    
+    
+    def get_feed(self, extra_params=None):
+        
+        if extra_params:
+            try:
+                obj = self.get_object(extra_params.split('/'))
+            except (AttributeError, LookupError):
+                raise LookupError('Feed does not exist')
+        else:
+            obj = None
+        
+        feed = AtomFeed(
+            atom_id = self.__get_dynamic_attr('feed_id', obj),
+            title = self.__get_dynamic_attr('feed_title', obj),
+            updated = self.__get_dynamic_attr('feed_updated', obj),
+            icon = self.__get_dynamic_attr('feed_icon', obj),
+            logo = self.__get_dynamic_attr('feed_logo', obj),
+            rights = self.__get_dynamic_attr('feed_rights', obj),
+            subtitle = self.__get_dynamic_attr('feed_subtitle', obj),
+            authors = self.__get_dynamic_attr('feed_authors', obj, default=[]),
+            categories = self.__get_dynamic_attr('feed_categories', obj, default=[]),
+            contributors = self.__get_dynamic_attr('feed_contributors', obj, default=[]),
+            links = self.__get_dynamic_attr('feed_links', obj, default=[]),
+            extra_attrs = self.__get_dynamic_attr('feed_extra_attrs', obj),
+            hide_generator = self.__get_dynamic_attr('hide_generator', obj, default=False)
+        )
+        
+        items = self.__get_dynamic_attr('items', obj)
+        if items is None:
+            raise LookupError('Feed has no items field')
+        
+        for item in items:
+            feed.add_item(
+                atom_id = self.__get_dynamic_attr('item_id', item), 
+                title = self.__get_dynamic_attr('item_title', item),
+                updated = self.__get_dynamic_attr('item_updated', item),
+                content = self.__get_dynamic_attr('item_content', item),
+                published = self.__get_dynamic_attr('item_published', item),
+                rights = self.__get_dynamic_attr('item_rights', item),
+                source = self.__get_dynamic_attr('item_source', item),
+                summary = self.__get_dynamic_attr('item_summary', item),
+                authors = self.__get_dynamic_attr('item_authors', item, default=[]),
+                categories = self.__get_dynamic_attr('item_categories', item, default=[]),
+                contributors = self.__get_dynamic_attr('item_contributors', item, default=[]),
+                links = self.__get_dynamic_attr('item_links', item, default=[]),
+                extra_attrs = self.__get_dynamic_attr('item_extra_attrs', None, default={}),
+            )
+        
+        if self.VALIDATE:
+            feed.validate()
+        return feed
+
+
+
+class ValidationError(Exception):
+    pass
+
+
+
+## based on django.utils.feedgenerator.SyndicationFeed and django.utils.feedgenerator.Atom1Feed
+class AtomFeed(object):
+    
+    
+    mime_type = 'application/atom+xml'
+    ns = u'http://www.w3.org/2005/Atom'
+    
+    
+    def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
+        authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
+        if atom_id is None:
+            raise LookupError('Feed has no feed_id field')
+        if title is None:
+            raise LookupError('Feed has no feed_title field')
+        # if updated == None, we'll calculate it
+        self.feed = {
+            'id': atom_id,
+            'title': title,
+            'updated': updated,
+            'icon': icon,
+            'logo': logo,
+            'rights': rights,
+            'subtitle': subtitle,
+            'authors': authors,
+            'categories': categories,
+            'contributors': contributors,
+            'links': links,
+            'extra_attrs': extra_attrs,
+            'hide_generator': hide_generator,
+        }
+        self.items = []
+    
+    
+    def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
+        authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
+        if atom_id is None:
+            raise LookupError('Feed has no item_id method')
+        if title is None:
+            raise LookupError('Feed has no item_title method')
+        if updated is None:
+            raise LookupError('Feed has no item_updated method')
+        self.items.append({
+            'id': atom_id,
+            'title': title,
+            'updated': updated,
+            'content': content,
+            'published': published,
+            'rights': rights,
+            'source': source,
+            'summary': summary,
+            'authors': authors,
+            'categories': categories,
+            'contributors': contributors,
+            'links': links,
+            'extra_attrs': extra_attrs,
+        })
+    
+    
+    def latest_updated(self):
+        """
+        Returns the latest item's updated or the current time if there are no items.
+        """
+        updates = [item['updated'] for item in self.items]
+        if len(updates) > 0:
+            updates.sort()
+            return updates[-1]
+        else:
+            return datetime.now() # @@@ really we should allow a feed to define its "start" for this case
+    
+    
+    def write_text_construct(self, handler, element_name, data):
+        if isinstance(data, tuple):
+            text_type, text = data
+            if text_type == 'xhtml':
+                handler.startElement(element_name, {'type': text_type})
+                handler._write(text) # write unescaped -- it had better be well-formed XML
+                handler.endElement(element_name)
+            else:
+                handler.addQuickElement(element_name, text, {'type': text_type})
+        else:
+            handler.addQuickElement(element_name, data)
+    
+    
+    def write_person_construct(self, handler, element_name, person):
+        handler.startElement(element_name, {})
+        handler.addQuickElement(u'name', person['name'])
+        if 'uri' in person:
+            handler.addQuickElement(u'uri', person['uri'])
+        if 'email' in person:
+            handler.addQuickElement(u'email', person['email'])
+        handler.endElement(element_name)
+    
+    
+    def write_link_construct(self, handler, link):
+        if 'length' in link:
+            link['length'] = str(link['length'])
+        handler.addQuickElement(u'link', None, link)
+    
+    
+    def write_category_construct(self, handler, category):
+        handler.addQuickElement(u'category', None, category)
+    
+    
+    def write_source(self, handler, data):
+        handler.startElement(u'source', {})
+        if data.get('id'):
+            handler.addQuickElement(u'id', data['id'])
+        if data.get('title'):
+            self.write_text_construct(handler, u'title', data['title'])
+        if data.get('subtitle'):
+            self.write_text_construct(handler, u'subtitle', data['subtitle'])
+        if data.get('icon'):
+            handler.addQuickElement(u'icon', data['icon'])
+        if data.get('logo'):
+            handler.addQuickElement(u'logo', data['logo'])
+        if data.get('updated'):
+            handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
+        for category in data.get('categories', []):
+            self.write_category_construct(handler, category)
+        for link in data.get('links', []):
+            self.write_link_construct(handler, link)
+        for author in data.get('authors', []):
+            self.write_person_construct(handler, u'author', author)
+        for contributor in data.get('contributors', []):
+            self.write_person_construct(handler, u'contributor', contributor)
+        if data.get('rights'):
+            self.write_text_construct(handler, u'rights', data['rights'])
+        handler.endElement(u'source')
+    
+    
+    def write_content(self, handler, data):
+        if isinstance(data, tuple):
+            content_dict, text = data
+            if content_dict.get('type') == 'xhtml':
+                handler.startElement(u'content', content_dict)
+                handler._write(text) # write unescaped -- it had better be well-formed XML
+                handler.endElement(u'content')
+            else:
+                handler.addQuickElement(u'content', text, content_dict)
+        else:
+            handler.addQuickElement(u'content', data)
+    
+    
+    def write(self, outfile, encoding):
+        handler = SimplerXMLGenerator(outfile, encoding)
+        handler.startDocument()
+        feed_attrs = {u'xmlns': self.ns}
+        if self.feed.get('extra_attrs'):
+            feed_attrs.update(self.feed['extra_attrs'])
+        handler.startElement(u'feed', feed_attrs)
+        handler.addQuickElement(u'id', self.feed['id'])
+        self.write_text_construct(handler, u'title', self.feed['title'])
+        if self.feed.get('subtitle'):
+            self.write_text_construct(handler, u'subtitle', self.feed['subtitle'])
+        if self.feed.get('icon'):
+            handler.addQuickElement(u'icon', self.feed['icon'])
+        if self.feed.get('logo'):
+            handler.addQuickElement(u'logo', self.feed['logo'])
+        if self.feed['updated']:
+            handler.addQuickElement(u'updated', rfc3339_date(self.feed['updated']))
+        else:
+            handler.addQuickElement(u'updated', rfc3339_date(self.latest_updated()))
+        for category in self.feed['categories']:
+            self.write_category_construct(handler, category)
+        for link in self.feed['links']:
+            self.write_link_construct(handler, link)
+        for author in self.feed['authors']:
+            self.write_person_construct(handler, u'author', author)
+        for contributor in self.feed['contributors']:
+            self.write_person_construct(handler, u'contributor', contributor)
+        if self.feed.get('rights'):
+            self.write_text_construct(handler, u'rights', self.feed['rights'])
+        if not self.feed.get('hide_generator'):
+            handler.addQuickElement(u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
+        
+        self.write_items(handler)
+        
+        handler.endElement(u'feed')
+    
+    
+    def write_items(self, handler):
+        for item in self.items:
+            entry_attrs = item.get('extra_attrs', {})
+            handler.startElement(u'entry', entry_attrs)
+            
+            handler.addQuickElement(u'id', item['id'])
+            self.write_text_construct(handler, u'title', item['title'])
+            handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
+            if item.get('published'):
+                handler.addQuickElement(u'published', rfc3339_date(item['published']))
+            if item.get('rights'):
+                self.write_text_construct(handler, u'rights', item['rights'])
+            if item.get('source'):
+                self.write_source(handler, item['source'])
+            
+            for author in item['authors']:
+                self.write_person_construct(handler, u'author', author)
+            for contributor in item['contributors']:
+                self.write_person_construct(handler, u'contributor', contributor)
+            for category in item['categories']:
+                self.write_category_construct(handler, category)
+            for link in item['links']:
+                self.write_link_construct(handler, link)
+            if item.get('summary'):
+                self.write_text_construct(handler, u'summary', item['summary'])
+            if item.get('content'):
+                self.write_content(handler, item['content'])
+            
+            handler.endElement(u'entry')
+    
+    
+    def validate(self):
+        
+        def validate_text_construct(obj):
+            if isinstance(obj, tuple):
+                if obj[0] not in ['text', 'html', 'xhtml']:
+                    return False
+            # @@@ no validation is done that 'html' text constructs are valid HTML
+            # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
+            
+            return True
+        
+        if not validate_text_construct(self.feed['title']):
+            raise ValidationError('feed title has invalid type')
+        if self.feed.get('subtitle'):
+            if not validate_text_construct(self.feed['subtitle']):
+                raise ValidationError('feed subtitle has invalid type')
+        if self.feed.get('rights'):
+            if not validate_text_construct(self.feed['rights']):
+                raise ValidationError('feed rights has invalid type')
+        
+        alternate_links = {}
+        for link in self.feed.get('links'):
+            if link.get('rel') == 'alternate' or link.get('rel') == None:
+                key = (link.get('type'), link.get('hreflang'))
+                if key in alternate_links:
+                    raise ValidationError('alternate links must have unique type/hreflang')
+                alternate_links[key] = link
+        
+        if self.feed.get('authors'):
+            feed_author = True
+        else:
+            feed_author = False
+        
+        for item in self.items:
+            if not feed_author and not item.get('authors'):
+                if item.get('source') and item['source'].get('authors'):
+                    pass
+                else:
+                    raise ValidationError('if no feed author, all entries must have author (possibly in source)')
+            
+            if not validate_text_construct(item['title']):
+                raise ValidationError('entry title has invalid type')
+            if item.get('rights'):
+                if not validate_text_construct(item['rights']):
+                    raise ValidationError('entry rights has invalid type')
+            if item.get('summary'):
+                if not validate_text_construct(item['summary']):
+                    raise ValidationError('entry summary has invalid type')
+            source = item.get('source')
+            if source:
+                if source.get('title'):
+                    if not validate_text_construct(source['title']):
+                        raise ValidationError('source title has invalid type')
+                if source.get('subtitle'):
+                    if not validate_text_construct(source['subtitle']):
+                        raise ValidationError('source subtitle has invalid type')
+                if source.get('rights'):
+                    if not validate_text_construct(source['rights']):
+                        raise ValidationError('source rights has invalid type')
+            
+            alternate_links = {}
+            for link in item.get('links'):
+                if link.get('rel') == 'alternate' or link.get('rel') == None:
+                    key = (link.get('type'), link.get('hreflang'))
+                    if key in alternate_links:
+                        raise ValidationError('alternate links must have unique type/hreflang')
+                    alternate_links[key] = link
+            
+            if not item.get('content'):
+                if not alternate_links:
+                    raise ValidationError('if no content, entry must have alternate link')
+            
+            if item.get('content') and isinstance(item.get('content'), tuple):
+                content_type = item.get('content')[0].get('type')
+                if item.get('content')[0].get('src'):
+                    if item.get('content')[1]:
+                        raise ValidationError('content with src should be empty')
+                    if not item.get('summary'):
+                        raise ValidationError('content with src requires a summary too')
+                    if content_type in ['text', 'html', 'xhtml']:
+                        raise ValidationError('content with src cannot have type of text, html or xhtml')
+                if content_type:
+                    if '/' in content_type and \
+                        not content_type.startswith('text/') and \
+                        not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
+                        not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
+                        # @@@ check content is Base64
+                        if not item.get('summary'):
+                            raise ValidationError('content in Base64 requires a summary too')
+                    if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
+                        raise ValidationError('content type does not appear to be valid')
+                    
+                    # @@@ no validation is done that 'html' text constructs are valid HTML
+                    # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
+                    
+                    return
+        
+        return
+
+
+
+class LegacySyndicationFeed(AtomFeed):
+    """
+    Provides an SyndicationFeed-compatible interface in its __init__ and
+    add_item but is really a new AtomFeed object.
+    """
+    
+    def __init__(self, title, link, description, language=None, author_email=None,
+            author_name=None, author_link=None, subtitle=None, categories=[],
+            feed_url=None, feed_copyright=None):
+        
+        atom_id = link
+        title = title
+        updated = None # will be calculated
+        rights = feed_copyright
+        subtitle = subtitle
+        author_dict = {'name': author_name}
+        if author_link:
+            author_dict['uri'] = author_uri
+        if author_email:
+            author_dict['email'] = author_email
+        authors = [author_dict]
+        if categories:
+            categories = [{'term': term} for term in categories]
+        links = [{'rel': 'alternate', 'href': link}]
+        if feed_url:
+            links.append({'rel': 'self', 'href': feed_url})
+        if language:
+            extra_attrs = {'xml:lang': language}
+        else:
+            extra_attrs = {}
+        
+        # description ignored (as with Atom1Feed)
+        
+        AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
+                authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
+    
+    
+    def add_item(self, title, link, description, author_email=None,
+            author_name=None, author_link=None, pubdate=None, comments=None,
+            unique_id=None, enclosure=None, categories=[], item_copyright=None):
+        
+        if unique_id:
+            atom_id = unique_id
+        else:
+            atom_id = get_tag_uri(link, pubdate)
+        title = title
+        updated = pubdate
+        if item_copyright:
+            rights = item_copyright
+        else:
+            rights = None
+        if description:
+            summary = 'html', description
+        else:
+            summary = None
+        author_dict = {'name': author_name}
+        if author_link:
+            author_dict['uri'] = author_uri
+        if author_email:
+            author_dict['email'] = author_email
+        authors = [author_dict]
+        categories = [{'term': term} for term in categories]
+        links = [{'rel': 'alternate', 'href': link}]
+        if enclosure:
+            links.append({'rel': 'enclosure', 'href': enclosure.url, 'length': enclosure.length, 'type': enclosure.mime_type})
+        
+        AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
+                authors=authors, categories=categories, links=links)

=== added file 'notification/context_processors.py'
--- notification/context_processors.py	1970-01-01 00:00:00 +0000
+++ notification/context_processors.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,9 @@
+from notification.models import Notice
+
+def notification(request):
+    if request.user.is_authenticated():
+        return {
+            'notice_unseen_count': Notice.objects.unseen_count_for(request.user, on_site=True),
+        }
+    else:
+        return {}

=== added file 'notification/decorators.py'
--- notification/decorators.py	1970-01-01 00:00:00 +0000
+++ notification/decorators.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,62 @@
+from django.utils.translation import ugettext as _
+from django.http import HttpResponse
+from django.contrib.auth import authenticate, login
+from django.conf import settings
+
+def simple_basic_auth_callback(request, user, *args, **kwargs):
+    """
+    Simple callback to automatically login the given user after a successful
+    basic authentication.
+    """
+    login(request, user)
+    request.user = user
+
+def basic_auth_required(realm=None, test_func=None, callback_func=None):
+    """
+    This decorator should be used with views that need simple authentication
+    against Django's authentication framework.
+    
+    The ``realm`` string is shown during the basic auth query.
+    
+    It takes a ``test_func`` argument that is used to validate the given
+    credentials and return the decorated function if successful.
+    
+    If unsuccessful the decorator will try to authenticate and checks if the
+    user has the ``is_active`` field set to True.
+    
+    In case of a successful authentication  the ``callback_func`` will be
+    called by passing the ``request`` and the ``user`` object. After that the
+    actual view function will be called.
+    
+    If all of the above fails a "Authorization Required" message will be shown.
+    """
+    if realm is None:
+        realm = getattr(settings, 'HTTP_AUTHENTICATION_REALM', _('Restricted Access'))
+    if test_func is None:
+        test_func = lambda u: u.is_authenticated()
+
+    def decorator(view_func):
+        def basic_auth(request, *args, **kwargs):
+            # Just return the original view because already logged in
+            if test_func(request.user):
+                return view_func(request, *args, **kwargs)
+
+            # Not logged in, look if login credentials are provided
+            if 'HTTP_AUTHORIZATION' in request.META:        
+                auth_method, auth = request.META['HTTP_AUTHORIZATION'].split(' ',1)
+                if 'basic' == auth_method.lower():
+                    auth = auth.strip().decode('base64')
+                    username, password = auth.split(':',1)
+                    user = authenticate(username=username, password=password)
+                    if user is not None:
+                        if user.is_active:
+                            if callback_func is not None and callable(callback_func):
+                                callback_func(request, user, *args, **kwargs)
+                            return view_func(request, *args, **kwargs)
+
+            response =  HttpResponse(_('Authorization Required'), mimetype="text/plain")
+            response.status_code = 401
+            response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
+            return response
+        return basic_auth
+    return decorator

=== added file 'notification/engine.py'
--- notification/engine.py	1970-01-01 00:00:00 +0000
+++ notification/engine.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,74 @@
+
+import sys
+import time
+import logging
+import traceback
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from django.conf import settings
+from django.core.mail import mail_admins
+from django.contrib.auth.models import User
+from django.contrib.sites.models import Site
+
+from lockfile import FileLock, AlreadyLocked, LockTimeout
+
+from notification.models import NoticeQueueBatch
+from notification import models as notification
+
+# lock timeout value. how long to wait for the lock to become available.
+# default behavior is to never wait for the lock to be available.
+LOCK_WAIT_TIMEOUT = getattr(settings, "NOTIFICATION_LOCK_WAIT_TIMEOUT", -1)
+
+def send_all():
+    lock = FileLock("send_notices")
+
+    logging.debug("acquiring lock...")
+    try:
+        lock.acquire(LOCK_WAIT_TIMEOUT)
+    except AlreadyLocked:
+        logging.debug("lock already in place. quitting.")
+        return
+    except LockTimeout:
+        logging.debug("waiting for the lock timed out. quitting.")
+        return
+    logging.debug("acquired.")
+
+    batches, sent = 0, 0
+    start_time = time.time()
+
+    try:
+        # nesting the try statement to be Python 2.4
+        try:
+            for queued_batch in NoticeQueueBatch.objects.all():
+                notices = pickle.loads(str(queued_batch.pickled_data).decode("base64"))
+                for user, label, extra_context, on_site in notices:
+                    user = User.objects.get(pk=user)
+                    logging.info("emitting notice to %s" % user)
+                    # call this once per user to be atomic and allow for logging to
+                    # accurately show how long each takes.
+                    notification.send_now([user], label, extra_context, on_site)
+                    sent += 1
+                queued_batch.delete()
+                batches += 1
+        except:
+            # get the exception
+            exc_class, e, t = sys.exc_info()
+            # email people
+            current_site = Site.objects.get_current()
+            subject = "[%s emit_notices] %r" % (current_site.name, e)
+            message = "%s" % ("\n".join(traceback.format_exception(*sys.exc_info())),)
+            mail_admins(subject, message, fail_silently=True)
+            # log it as critical
+            logging.critical("an exception occurred: %r" % e)
+    finally:
+        logging.debug("releasing lock...")
+        lock.release()
+        logging.debug("released.")
+    
+    logging.info("")
+    logging.info("%s batches, %s sent" % (batches, sent,))
+    logging.info("done in %.2f seconds" % (time.time() - start_time))

=== added file 'notification/feeds.py'
--- notification/feeds.py	1970-01-01 00:00:00 +0000
+++ notification/feeds.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,71 @@
+from datetime import datetime
+
+from django.core.urlresolvers import reverse
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.contrib.auth.models import User
+from django.shortcuts import get_object_or_404
+from django.template.defaultfilters import linebreaks, escape, striptags
+from django.utils.translation import ugettext_lazy as _
+
+from notification.models import Notice
+from notification.atomformat import Feed
+
+ITEMS_PER_FEED = getattr(settings, 'ITEMS_PER_FEED', 20)
+
+class BaseNoticeFeed(Feed):
+    def item_id(self, notification):
+        return "http://%s%s"; % (
+            Site.objects.get_current().domain,
+            notification.get_absolute_url(),
+        )
+    
+    def item_title(self, notification):
+        return striptags(notification.message)
+    
+    def item_updated(self, notification):
+        return notification.added
+    
+    def item_published(self, notification):
+        return notification.added
+    
+    def item_content(self, notification):
+        return {"type" : "html", }, linebreaks(escape(notification.message))
+    
+    def item_links(self, notification):
+        return [{"href" : self.item_id(notification)}]
+    
+    def item_authors(self, notification):
+        return [{"name" : notification.user.username}]
+
+class NoticeUserFeed(BaseNoticeFeed):
+    def get_object(self, params):
+        return get_object_or_404(User, username=params[0].lower())
+
+    def feed_id(self, user):
+        return "http://%s%s"; % (
+                Site.objects.get_current().domain,
+                reverse('notification_feed_for_user'),
+            )
+
+    def feed_title(self, user):
+        return _('Notices Feed')
+
+    def feed_updated(self, user):
+        qs = Notice.objects.filter(user=user)
+        # We return an arbitrary date if there are no results, because there
+        # must be a feed_updated field as per the Atom specifications, however
+        # there is no real data to go by, and an arbitrary date can be static.
+        if qs.count() == 0:
+            return datetime(year=2008, month=7, day=1)
+        return qs.latest('added').added
+
+    def feed_links(self, user):
+        complete_url = "http://%s%s"; % (
+                Site.objects.get_current().domain,
+                reverse('notification_notices'),
+            )
+        return ({'href': complete_url},)
+
+    def items(self, user):
+        return Notice.objects.notices_for(user).order_by("-added")[:ITEMS_PER_FEED]

=== added file 'notification/lockfile.py'
--- notification/lockfile.py	1970-01-01 00:00:00 +0000
+++ notification/lockfile.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,500 @@
+
+"""
+lockfile.py - Platform-independent advisory file locks.
+
+Requires Python 2.5 unless you apply 2.4.diff
+Locking is done on a per-thread basis instead of a per-process basis.
+
+Usage:
+
+>>> lock = FileLock('somefile')
+>>> try:
+...     lock.acquire()
+... except AlreadyLocked:
+...     print 'somefile', 'is locked already.'
+... except LockFailed:
+...     print 'somefile', 'can\\'t be locked.'
+... else:
+...     print 'got lock'
+got lock
+>>> print lock.is_locked()
+True
+>>> lock.release()
+
+>>> lock = FileLock('somefile')
+>>> print lock.is_locked()
+False
+>>> with lock:
+...    print lock.is_locked()
+True
+>>> print lock.is_locked()
+False
+>>> # It is okay to lock twice from the same thread...
+>>> with lock:
+...     lock.acquire()
+...
+>>> # Though no counter is kept, so you can't unlock multiple times...
+>>> print lock.is_locked()
+False
+
+Exceptions:
+
+    Error - base class for other exceptions
+        LockError - base class for all locking exceptions
+            AlreadyLocked - Another thread or process already holds the lock
+            LockFailed - Lock failed for some other reason
+        UnlockError - base class for all unlocking exceptions
+            AlreadyUnlocked - File was not locked.
+            NotMyLock - File was locked but not by the current thread/process
+"""
+
+from __future__ import division
+
+import sys
+import socket
+import os
+import threading
+import time
+import errno
+
+# Work with PEP8 and non-PEP8 versions of threading module.
+try:
+    threading.current_thread
+except AttributeError:
+    threading.current_thread = threading.currentThread
+try:
+    # python 2.6 has threading.current_thread so we need to do this separately.
+    threading.Thread.get_name
+except AttributeError:
+    threading.Thread.get_name = threading.Thread.getName
+
+__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
+           'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
+           'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
+
+class Error(Exception):
+    """
+    Base class for other exceptions.
+
+    >>> try:
+    ...   raise Error
+    ... except Exception:
+    ...   pass
+    """
+    pass
+
+class LockError(Error):
+    """
+    Base class for error arising from attempts to acquire the lock.
+
+    >>> try:
+    ...   raise LockError
+    ... except Error:
+    ...   pass
+    """
+    pass
+
+class LockTimeout(LockError):
+    """Raised when lock creation fails within a user-defined period of time.
+
+    >>> try:
+    ...   raise LockTimeout
+    ... except LockError:
+    ...   pass
+    """
+    pass
+
+class AlreadyLocked(LockError):
+    """Some other thread/process is locking the file.
+
+    >>> try:
+    ...   raise AlreadyLocked
+    ... except LockError:
+    ...   pass
+    """
+    pass
+
+class LockFailed(LockError):
+    """Lock file creation failed for some other reason.
+
+    >>> try:
+    ...   raise LockFailed
+    ... except LockError:
+    ...   pass
+    """
+    pass
+
+class UnlockError(Error):
+    """
+    Base class for errors arising from attempts to release the lock.
+
+    >>> try:
+    ...   raise UnlockError
+    ... except Error:
+    ...   pass
+    """
+    pass
+
+class NotLocked(UnlockError):
+    """Raised when an attempt is made to unlock an unlocked file.
+
+    >>> try:
+    ...   raise NotLocked
+    ... except UnlockError:
+    ...   pass
+    """
+    pass
+
+class NotMyLock(UnlockError):
+    """Raised when an attempt is made to unlock a file someone else locked.
+
+    >>> try:
+    ...   raise NotMyLock
+    ... except UnlockError:
+    ...   pass
+    """
+    pass
+
+class LockBase:
+    """Base class for platform-specific lock classes."""
+    def __init__(self, path, threaded=True):
+        """
+        >>> lock = LockBase('somefile')
+        >>> lock = LockBase('somefile', threaded=False)
+        """
+        self.path = path
+        self.lock_file = os.path.abspath(path) + ".lock"
+        self.hostname = socket.gethostname()
+        self.pid = os.getpid()
+        if threaded:
+            tname = "%s-" % threading.current_thread().get_name()
+        else:
+            tname = ""
+        dirname = os.path.dirname(self.lock_file)
+        self.unique_name = os.path.join(dirname,
+                                        "%s.%s%s" % (self.hostname,
+                                                     tname,
+                                                     self.pid))
+
+    def acquire(self, timeout=None):
+        """
+        Acquire the lock.
+
+        * If timeout is omitted (or None), wait forever trying to lock the
+          file.
+
+        * If timeout > 0, try to acquire the lock for that many seconds.  If
+          the lock period expires and the file is still locked, raise
+          LockTimeout.
+
+        * If timeout <= 0, raise AlreadyLocked immediately if the file is
+          already locked.
+        """
+        raise NotImplemented("implement in subclass")
+
+    def release(self):
+        """
+        Release the lock.
+
+        If the file is not locked, raise NotLocked.
+        """
+        raise NotImplemented("implement in subclass")
+
+    def is_locked(self):
+        """
+        Tell whether or not the file is locked.
+        """
+        raise NotImplemented("implement in subclass")
+
+    def i_am_locking(self):
+        """
+        Return True if this object is locking the file.
+        """
+        raise NotImplemented("implement in subclass")
+
+    def break_lock(self):
+        """
+        Remove a lock.  Useful if a locking thread failed to unlock.
+        """
+        raise NotImplemented("implement in subclass")
+
+    def __enter__(self):
+        """
+        Context manager support.
+        """
+        self.acquire()
+        return self
+
+    def __exit__(self, *_exc):
+        """
+        Context manager support.
+        """
+        self.release()
+
+class LinkFileLock(LockBase):
+    """Lock access to a file using atomic property of link(2)."""
+
+    def acquire(self, timeout=None):
+        try:
+            open(self.unique_name, "wb").close()
+        except IOError:
+            raise LockFailed
+
+        end_time = time.time()
+        if timeout is not None and timeout > 0:
+            end_time += timeout
+
+        while True:
+            # Try and create a hard link to it.
+            try:
+                os.link(self.unique_name, self.lock_file)
+            except OSError:
+                # Link creation failed.  Maybe we've double-locked?
+                nlinks = os.stat(self.unique_name).st_nlink
+                if nlinks == 2:
+                    # The original link plus the one I created == 2.  We're
+                    # good to go.
+                    return
+                else:
+                    # Otherwise the lock creation failed.
+                    if timeout is not None and time.time() > end_time:
+                        os.unlink(self.unique_name)
+                        if timeout > 0:
+                            raise LockTimeout
+                        else:
+                            raise AlreadyLocked
+                    time.sleep(timeout is not None and timeout/10 or 0.1)
+            else:
+                # Link creation succeeded.  We're good to go.
+                return
+
+    def release(self):
+        if not self.is_locked():
+            raise NotLocked
+        elif not os.path.exists(self.unique_name):
+            raise NotMyLock
+        os.unlink(self.unique_name)
+        os.unlink(self.lock_file)
+
+    def is_locked(self):
+        return os.path.exists(self.lock_file)
+
+    def i_am_locking(self):
+        return (self.is_locked() and
+                os.path.exists(self.unique_name) and
+                os.stat(self.unique_name).st_nlink == 2)
+
+    def break_lock(self):
+        if os.path.exists(self.lock_file):
+            os.unlink(self.lock_file)
+
+class MkdirFileLock(LockBase):
+    """Lock file by creating a directory."""
+    def __init__(self, path, threaded=True):
+        """
+        >>> lock = MkdirFileLock('somefile')
+        >>> lock = MkdirFileLock('somefile', threaded=False)
+        """
+        LockBase.__init__(self, path, threaded)
+        if threaded:
+            tname = "%x-" % thread.get_ident()
+        else:
+            tname = ""
+        # Lock file itself is a directory.  Place the unique file name into
+        # it.
+        self.unique_name  = os.path.join(self.lock_file,
+                                         "%s.%s%s" % (self.hostname,
+                                                      tname,
+                                                      self.pid))
+
+    def acquire(self, timeout=None):
+        end_time = time.time()
+        if timeout is not None and timeout > 0:
+            end_time += timeout
+
+        if timeout is None:
+            wait = 0.1
+        else:
+            wait = max(0, timeout / 10)
+
+        while True:
+            try:
+                os.mkdir(self.lock_file)
+            except OSError:
+                err = sys.exc_info()[1]
+                if err.errno == errno.EEXIST:
+                    # Already locked.
+                    if os.path.exists(self.unique_name):
+                        # Already locked by me.
+                        return
+                    if timeout is not None and time.time() > end_time:
+                        if timeout > 0:
+                            raise LockTimeout
+                        else:
+                            # Someone else has the lock.
+                            raise AlreadyLocked
+                    time.sleep(wait)
+                else:
+                    # Couldn't create the lock for some other reason
+                    raise LockFailed
+            else:
+                open(self.unique_name, "wb").close()
+                return
+
+    def release(self):
+        if not self.is_locked():
+            raise NotLocked
+        elif not os.path.exists(self.unique_name):
+            raise NotMyLock
+        os.unlink(self.unique_name)
+        os.rmdir(self.lock_file)
+
+    def is_locked(self):
+        return os.path.exists(self.lock_file)
+
+    def i_am_locking(self):
+        return (self.is_locked() and
+                os.path.exists(self.unique_name))
+
+    def break_lock(self):
+        if os.path.exists(self.lock_file):
+            for name in os.listdir(self.lock_file):
+                os.unlink(os.path.join(self.lock_file, name))
+            os.rmdir(self.lock_file)
+
+class SQLiteFileLock(LockBase):
+    "Demonstration of using same SQL-based locking."
+
+    import tempfile
+    _fd, testdb = tempfile.mkstemp()
+    os.close(_fd)
+    os.unlink(testdb)
+    del _fd, tempfile
+
+    def __init__(self, path, threaded=True):
+        LockBase.__init__(self, path, threaded)
+        self.lock_file = unicode(self.lock_file)
+        self.unique_name = unicode(self.unique_name)
+
+        import sqlite3
+        self.connection = sqlite3.connect(SQLiteFileLock.testdb)
+        
+        c = self.connection.cursor()
+        try:
+            c.execute("create table locks"
+                      "("
+                      "   lock_file varchar(32),"
+                      "   unique_name varchar(32)"
+                      ")")
+        except sqlite3.OperationalError:
+            pass
+        else:
+            self.connection.commit()
+            import atexit
+            atexit.register(os.unlink, SQLiteFileLock.testdb)
+
+    def acquire(self, timeout=None):
+        end_time = time.time()
+        if timeout is not None and timeout > 0:
+            end_time += timeout
+
+        if timeout is None:
+            wait = 0.1
+        elif timeout <= 0:
+            wait = 0
+        else:
+            wait = timeout / 10
+
+        cursor = self.connection.cursor()
+
+        while True:
+            if not self.is_locked():
+                # Not locked.  Try to lock it.
+                cursor.execute("insert into locks"
+                               "  (lock_file, unique_name)"
+                               "  values"
+                               "  (?, ?)",
+                               (self.lock_file, self.unique_name))
+                self.connection.commit()
+
+                # Check to see if we are the only lock holder.
+                cursor.execute("select * from locks"
+                               "  where unique_name = ?",
+                               (self.unique_name,))
+                rows = cursor.fetchall()
+                if len(rows) > 1:
+                    # Nope.  Someone else got there.  Remove our lock.
+                    cursor.execute("delete from locks"
+                                   "  where unique_name = ?",
+                                   (self.unique_name,))
+                    self.connection.commit()
+                else:
+                    # Yup.  We're done, so go home.
+                    return
+            else:
+                # Check to see if we are the only lock holder.
+                cursor.execute("select * from locks"
+                               "  where unique_name = ?",
+                               (self.unique_name,))
+                rows = cursor.fetchall()
+                if len(rows) == 1:
+                    # We're the locker, so go home.
+                    return
+                    
+            # Maybe we should wait a bit longer.
+            if timeout is not None and time.time() > end_time:
+                if timeout > 0:
+                    # No more waiting.
+                    raise LockTimeout
+                else:
+                    # Someone else has the lock and we are impatient..
+                    raise AlreadyLocked
+
+            # Well, okay.  We'll give it a bit longer.
+            time.sleep(wait)
+
+    def release(self):
+        if not self.is_locked():
+            raise NotLocked
+        if not self.i_am_locking():
+            raise NotMyLock((self._who_is_locking(), self.unique_name))
+        cursor = self.connection.cursor()
+        cursor.execute("delete from locks"
+                       "  where unique_name = ?",
+                       (self.unique_name,))
+        self.connection.commit()
+
+    def _who_is_locking(self):
+        cursor = self.connection.cursor()
+        cursor.execute("select unique_name from locks"
+                       "  where lock_file = ?",
+                       (self.lock_file,))
+        return cursor.fetchone()[0]
+        
+    def is_locked(self):
+        cursor = self.connection.cursor()
+        cursor.execute("select * from locks"
+                       "  where lock_file = ?",
+                       (self.lock_file,))
+        rows = cursor.fetchall()
+        return not not rows
+
+    def i_am_locking(self):
+        cursor = self.connection.cursor()
+        cursor.execute("select * from locks"
+                       "  where lock_file = ?"
+                       "    and unique_name = ?",
+                       (self.lock_file, self.unique_name))
+        return not not cursor.fetchall()
+
+    def break_lock(self):
+        cursor = self.connection.cursor()
+        cursor.execute("delete from locks"
+                       "  where lock_file = ?",
+                       (self.lock_file,))
+        self.connection.commit()
+
+if hasattr(os, "link"):
+    FileLock = LinkFileLock
+else:
+    FileLock = MkdirFileLock

=== added directory 'notification/management'
=== added file 'notification/management/__init__.py'
=== added directory 'notification/management/commands'
=== added file 'notification/management/commands/__init__.py'
=== added file 'notification/management/commands/emit_notices.py'
--- notification/management/commands/emit_notices.py	1970-01-01 00:00:00 +0000
+++ notification/management/commands/emit_notices.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,15 @@
+
+import logging
+
+from django.core.management.base import NoArgsCommand
+
+from notification.engine import send_all
+
+class Command(NoArgsCommand):
+    help = "Emit queued notices."
+    
+    def handle_noargs(self, **options):
+        logging.basicConfig(level=logging.DEBUG, format="%(message)s")
+        logging.info("-" * 72)
+        send_all()
+    
\ No newline at end of file

=== added directory 'notification/migrations'
=== added file 'notification/migrations/0001_initial.py'
--- notification/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ notification/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,107 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import datetime
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Notice',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('message', models.TextField(verbose_name='message')),
+                ('added', models.DateTimeField(default=datetime.datetime.now, verbose_name='added')),
+                ('unseen', models.BooleanField(default=True, verbose_name='unseen')),
+                ('archived', models.BooleanField(default=False, verbose_name='archived')),
+                ('on_site', models.BooleanField(verbose_name='on site')),
+            ],
+            options={
+                'ordering': ['-added'],
+                'verbose_name': 'notice',
+                'verbose_name_plural': 'notices',
+            },
+        ),
+        migrations.CreateModel(
+            name='NoticeQueueBatch',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('pickled_data', models.TextField()),
+            ],
+        ),
+        migrations.CreateModel(
+            name='NoticeSetting',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('medium', models.CharField(max_length=1, verbose_name='medium', choices=[(b'1', 'Email')])),
+                ('send', models.BooleanField(verbose_name='send')),
+            ],
+            options={
+                'verbose_name': 'notice setting',
+                'verbose_name_plural': 'notice settings',
+            },
+        ),
+        migrations.CreateModel(
+            name='NoticeType',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('label', models.CharField(max_length=40, verbose_name='label')),
+                ('display', models.CharField(max_length=50, verbose_name='display')),
+                ('description', models.CharField(max_length=100, verbose_name='description')),
+                ('default', models.IntegerField(verbose_name='default')),
+            ],
+            options={
+                'verbose_name': 'notice type',
+                'verbose_name_plural': 'notice types',
+            },
+        ),
+        migrations.CreateModel(
+            name='ObservedItem',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField()),
+                ('added', models.DateTimeField(default=datetime.datetime.now, verbose_name='added')),
+                ('signal', models.TextField(verbose_name='signal')),
+                ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+                ('notice_type', models.ForeignKey(verbose_name='notice type', to='notification.NoticeType')),
+                ('user', models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-added'],
+                'verbose_name': 'observed item',
+                'verbose_name_plural': 'observed items',
+            },
+        ),
+        migrations.AddField(
+            model_name='noticesetting',
+            name='notice_type',
+            field=models.ForeignKey(verbose_name='notice type', to='notification.NoticeType'),
+        ),
+        migrations.AddField(
+            model_name='noticesetting',
+            name='user',
+            field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='notice',
+            name='notice_type',
+            field=models.ForeignKey(verbose_name='notice type', to='notification.NoticeType'),
+        ),
+        migrations.AddField(
+            model_name='notice',
+            name='user',
+            field=models.ForeignKey(verbose_name='user', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AlterUniqueTogether(
+            name='noticesetting',
+            unique_together=set([('user', 'notice_type', 'medium')]),
+        ),
+    ]

=== added file 'notification/migrations/__init__.py'
=== added file 'notification/models.py'
--- notification/models.py	1970-01-01 00:00:00 +0000
+++ notification/models.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,431 @@
+import datetime
+
+try:
+    import cPickle as pickle
+except ImportError:
+    import pickle
+
+from django.db import models
+from django.db.models.query import QuerySet
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.template import Context
+from django.template.loader import render_to_string
+
+from django.core.exceptions import ImproperlyConfigured
+
+from django.contrib.sites.models import Site
+from django.contrib.auth.models import User
+from django.contrib.auth.models import AnonymousUser
+
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.fields import GenericForeignKey
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext, get_language, activate
+
+# favour django-mailer but fall back to django.core.mail
+if 'mailer' in settings.INSTALLED_APPS:
+    from mailer import send_mail
+else:
+    from django.core.mail import send_mail
+
+QUEUE_ALL = getattr(settings, "NOTIFICATION_QUEUE_ALL", False)
+
+class LanguageStoreNotAvailable(Exception):
+    pass
+
+class NoticeType(models.Model):
+
+    label = models.CharField(_('label'), max_length=40)
+    display = models.CharField(_('display'), max_length=50)
+    description = models.CharField(_('description'), max_length=100)
+
+    # by default only on for media with sensitivity less than or equal to this number
+    default = models.IntegerField(_('default'))
+
+    def __unicode__(self):
+        return self.label
+
+    class Meta:
+        verbose_name = _("notice type")
+        verbose_name_plural = _("notice types")
+
+
+# if this gets updated, the create() method below needs to be as well...
+NOTICE_MEDIA = (
+    ("1", _("Email")),
+)
+
+# how spam-sensitive is the medium
+NOTICE_MEDIA_DEFAULTS = {
+    "1": 2 # email
+}
+
+class NoticeSetting(models.Model):
+    """
+    Indicates, for a given user, whether to send notifications
+    of a given type to a given medium.
+    """
+
+    user = models.ForeignKey(User, verbose_name=_('user'))
+    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
+    medium = models.CharField(_('medium'), max_length=1, choices=NOTICE_MEDIA)
+    send = models.BooleanField(_('send'))
+
+    class Meta:
+        verbose_name = _("notice setting")
+        verbose_name_plural = _("notice settings")
+        unique_together = ("user", "notice_type", "medium")
+
+def get_notification_setting(user, notice_type, medium):
+    try:
+        return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
+    except NoticeSetting.DoesNotExist:
+        default = (NOTICE_MEDIA_DEFAULTS[medium] <= notice_type.default)
+        setting = NoticeSetting(user=user, notice_type=notice_type, medium=medium, send=default)
+        setting.save()
+        return setting
+
+def should_send(user, notice_type, medium):
+    return get_notification_setting(user, notice_type, medium).send
+
+
+class NoticeManager(models.Manager):
+
+    def notices_for(self, user, archived=False, unseen=None, on_site=None):
+        """
+        returns Notice objects for the given user.
+
+        If archived=False, it only include notices not archived.
+        If archived=True, it returns all notices for that user.
+
+        If unseen=None, it includes all notices.
+        If unseen=True, return only unseen notices.
+        If unseen=False, return only seen notices.
+        """
+        if archived:
+            qs = self.filter(user=user)
+        else:
+            qs = self.filter(user=user, archived=archived)
+        if unseen is not None:
+            qs = qs.filter(unseen=unseen)
+        if on_site is not None:
+            qs = qs.filter(on_site=on_site)
+        return qs
+
+    def unseen_count_for(self, user, **kwargs):
+        """
+        returns the number of unseen notices for the given user but does not
+        mark them seen
+        """
+        return self.notices_for(user, unseen=True, **kwargs).count()
+
+class Notice(models.Model):
+
+    user = models.ForeignKey(User, verbose_name=_('user'))
+    message = models.TextField(_('message'))
+    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
+    added = models.DateTimeField(_('added'), default=datetime.datetime.now)
+    unseen = models.BooleanField(_('unseen'), default=True)
+    archived = models.BooleanField(_('archived'), default=False)
+    on_site = models.BooleanField(_('on site'))
+
+    objects = NoticeManager()
+
+    def __unicode__(self):
+        return self.message
+
+    def archive(self):
+        self.archived = True
+        self.save()
+
+    def is_unseen(self):
+        """
+        returns value of self.unseen but also changes it to false.
+
+        Use this in a template to mark an unseen notice differently the first
+        time it is shown.
+        """
+        unseen = self.unseen
+        if unseen:
+            self.unseen = False
+            self.save()
+        return unseen
+
+    class Meta:
+        ordering = ["-added"]
+        verbose_name = _("notice")
+        verbose_name_plural = _("notices")
+
+    def get_absolute_url(self):
+        return ("notification_notice", [str(self.pk)])
+    get_absolute_url = models.permalink(get_absolute_url)
+
+class NoticeQueueBatch(models.Model):
+    """
+    A queued notice.
+    Denormalized data for a notice.
+    """
+    pickled_data = models.TextField()
+
+def create_notice_type(label, display, description, default=2, verbosity=1):
+    """
+    Creates a new NoticeType.
+
+    This is intended to be used by other apps as a post_syncdb manangement step.
+    """
+    try:
+        notice_type = NoticeType.objects.get(label=label)
+        updated = False
+        if display != notice_type.display:
+            notice_type.display = display
+            updated = True
+        if description != notice_type.description:
+            notice_type.description = description
+            updated = True
+        if default != notice_type.default:
+            notice_type.default = default
+            updated = True
+        if updated:
+            notice_type.save()
+            if verbosity > 1:
+                print "Updated %s NoticeType" % label
+    except NoticeType.DoesNotExist:
+        NoticeType(label=label, display=display, description=description, default=default).save()
+        if verbosity > 1:
+            print "Created %s NoticeType" % label
+
+def get_notification_language(user):
+    """
+    Returns site-specific notification language for this user. Raises
+    LanguageStoreNotAvailable if this site does not use translated
+    notifications.
+    """
+    if getattr(settings, 'NOTIFICATION_LANGUAGE_MODULE', False):
+        try:
+            app_label, model_name = settings.NOTIFICATION_LANGUAGE_MODULE.split('.')
+            model = models.get_model(app_label, model_name)
+            language_model = model._default_manager.get(user__id__exact=user.id)
+            if hasattr(language_model, 'language'):
+                return language_model.language
+        except (ImportError, ImproperlyConfigured, model.DoesNotExist):
+            raise LanguageStoreNotAvailable
+    raise LanguageStoreNotAvailable
+
+def get_formatted_messages(formats, label, context):
+    """
+    Returns a dictionary with the format identifier as the key. The values are
+    are fully rendered templates with the given context.
+    """
+    format_templates = {}
+    for format in formats:
+        # conditionally turn off autoescaping for .txt extensions in format
+        if format.endswith(".txt"):
+            context.autoescape = False
+        else:
+            context.autoescape = True
+        format_templates[format] = render_to_string((
+            'notification/%s/%s' % (label, format),
+            'notification/%s' % format), context_instance=context)
+    return format_templates
+
+def send_now(users, label, extra_context=None, on_site=True):
+    """
+    Creates a new notice.
+
+    This is intended to be how other apps create new notices.
+
+    notification.send(user, 'friends_invite_sent', {
+        'spam': 'eggs',
+        'foo': 'bar',
+    )
+    
+    You can pass in on_site=False to prevent the notice emitted from being
+    displayed on the site.
+    """
+    if extra_context is None:
+        extra_context = {}
+    
+    notice_type = NoticeType.objects.get(label=label)
+
+    current_site = Site.objects.get_current()
+    notices_url = u"http://%s%s"; % (
+        unicode(current_site),
+        reverse("notification_notices"),
+    )
+
+    current_language = get_language()
+
+    formats = (
+        'short.txt',
+        'full.txt',
+        'notice.html',
+        'full.html',
+    ) # TODO make formats configurable
+
+    for user in users:
+        recipients = []
+        # get user language for user from language store defined in
+        # NOTIFICATION_LANGUAGE_MODULE setting
+        try:
+            language = get_notification_language(user)
+        except LanguageStoreNotAvailable:
+            language = None
+
+        if language is not None:
+            # activate the user's language
+            activate(language)
+
+        # update context with user specific translations
+        context = Context({
+            "user": user,
+            "notice": ugettext(notice_type.display),
+            "notices_url": notices_url,
+            "current_site": current_site,
+        })
+        context.update(extra_context)
+
+        # get prerendered format messages
+        messages = get_formatted_messages(formats, label, context)
+
+        # Strip newlines from subject
+        subject = ''.join(render_to_string('notification/email_subject.txt', {
+            'message': messages['short.txt'],
+        }, context).splitlines())
+
+        body = render_to_string('notification/email_body.txt', {
+            'message': messages['full.txt'],
+        }, context)
+
+        notice = Notice.objects.create(user=user, message=messages['notice.html'],
+            notice_type=notice_type, on_site=on_site)
+        if should_send(user, notice_type, "1") and user.email: # Email
+            recipients.append(user.email)
+        send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
+
+    # reset environment to original language
+    activate(current_language)
+
+def send(*args, **kwargs):
+    """
+    A basic interface around both queue and send_now. This honors a global
+    flag NOTIFICATION_QUEUE_ALL that helps determine whether all calls should
+    be queued or not. A per call ``queue`` or ``now`` keyword argument can be
+    used to always override the default global behavior.
+    """
+    queue_flag = kwargs.pop("queue", False)
+    now_flag = kwargs.pop("now", False)
+    assert not (queue_flag and now_flag), "'queue' and 'now' cannot both be True."
+    if queue_flag:
+        return queue(*args, **kwargs)
+    elif now_flag:
+        return send_now(*args, **kwargs)
+    else:
+        if QUEUE_ALL:
+            return queue(*args, **kwargs)
+        else:
+            return send_now(*args, **kwargs)
+        
+def queue(users, label, extra_context=None, on_site=True):
+    """
+    Queue the notification in NoticeQueueBatch. This allows for large amounts
+    of user notifications to be deferred to a seperate process running outside
+    the webserver.
+    """
+    if extra_context is None:
+        extra_context = {}
+    if isinstance(users, QuerySet):
+        users = [row["pk"] for row in users.values("pk")]
+    else:
+        users = [user.pk for user in users]
+    notices = []
+    for user in users:
+        notices.append((user, label, extra_context, on_site))
+    NoticeQueueBatch(pickled_data=pickle.dumps(notices).encode("base64")).save()
+
+class ObservedItemManager(models.Manager):
+
+    def all_for(self, observed, signal):
+        """
+        Returns all ObservedItems for an observed object,
+        to be sent when a signal is emited.
+        """
+        content_type = ContentType.objects.get_for_model(observed)
+        observed_items = self.filter(content_type=content_type, object_id=observed.id, signal=signal)
+        return observed_items
+
+    def get_for(self, observed, observer, signal):
+        content_type = ContentType.objects.get_for_model(observed)
+        observed_item = self.get(content_type=content_type, object_id=observed.id, user=observer, signal=signal)
+        return observed_item
+
+
+class ObservedItem(models.Model):
+
+    user = models.ForeignKey(User, verbose_name=_('user'))
+
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField()
+    observed_object = GenericForeignKey('content_type', 'object_id')
+
+    notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
+
+    added = models.DateTimeField(_('added'), default=datetime.datetime.now)
+
+    # the signal that will be listened to send the notice
+    signal = models.TextField(verbose_name=_('signal'))
+
+    objects = ObservedItemManager()
+
+    class Meta:
+        ordering = ['-added']
+        verbose_name = _('observed item')
+        verbose_name_plural = _('observed items')
+
+    def send_notice(self):
+        send([self.user], self.notice_type.label,
+             {'observed': self.observed_object})
+
+
+def observe(observed, observer, notice_type_label, signal='post_save'):
+    """
+    Create a new ObservedItem.
+
+    To be used by applications to register a user as an observer for some object.
+    """
+    notice_type = NoticeType.objects.get(label=notice_type_label)
+    observed_item = ObservedItem(user=observer, observed_object=observed,
+                                 notice_type=notice_type, signal=signal)
+    observed_item.save()
+    return observed_item
+
+def stop_observing(observed, observer, signal='post_save'):
+    """
+    Remove an observed item.
+    """
+    observed_item = ObservedItem.objects.get_for(observed, observer, signal)
+    observed_item.delete()
+
+def send_observation_notices_for(observed, signal='post_save'):
+    """
+    Send a notice for each registered user about an observed object.
+    """
+    observed_items = ObservedItem.objects.all_for(observed, signal)
+    for observed_item in observed_items:
+        observed_item.send_notice()
+    return observed_items
+
+def is_observing(observed, observer, signal='post_save'):
+    if isinstance(observer, AnonymousUser):
+        return False
+    try:
+        observed_items = ObservedItem.objects.get_for(observed, observer, signal)
+        return True
+    except ObservedItem.DoesNotExist:
+        return False
+    except ObservedItem.MultipleObjectsReturned:
+        return True
+
+def handle_observations(sender, instance, *args, **kw):
+    send_observation_notices_for(instance)

=== added file 'notification/urls.py'
--- notification/urls.py	1970-01-01 00:00:00 +0000
+++ notification/urls.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,10 @@
+from django.conf.urls import url
+
+from notification.views import notices, mark_all_seen, feed_for_user, single
+
+urlpatterns = [
+    url(r'^$', notices, name="notification_notices"),
+    url(r'^(\d+)/$', single, name="notification_notice"),
+    #url(r'^feed/$', feed_for_user, name="notification_feed_for_user"),
+    url(r'^mark_all_seen/$', mark_all_seen, name="notification_mark_all_seen"),
+]

=== added file 'notification/views.py'
--- notification/views.py	1970-01-01 00:00:00 +0000
+++ notification/views.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,92 @@
+from django.core.urlresolvers import reverse
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponseRedirect, Http404
+from django.template import RequestContext
+from django.contrib.auth.decorators import login_required
+from django.contrib.syndication.views import Feed
+
+from notification.models import *
+from notification.decorators import basic_auth_required, simple_basic_auth_callback
+from notification.feeds import NoticeUserFeed
+
+@basic_auth_required(realm='Notices Feed', callback_func=simple_basic_auth_callback)
+def feed_for_user(request):
+    url = "feed/%s" % request.user.username
+    return Feed(request, url, {
+        "feed": NoticeUserFeed,
+    })
+
+@login_required
+def notices(request):
+    notice_types = NoticeType.objects.all()
+    notices = Notice.objects.notices_for(request.user, on_site=True)
+    settings_table = []
+    for notice_type in NoticeType.objects.all():
+        settings_row = []
+        for medium_id, medium_display in NOTICE_MEDIA:
+            form_label = "%s_%s" % (notice_type.label, medium_id)
+            setting = get_notification_setting(request.user, notice_type, medium_id)
+            if request.method == "POST":
+                if request.POST.get(form_label) == "on":
+                    setting.send = True
+                else:
+                    setting.send = False
+                setting.save()
+            settings_row.append((form_label, setting.send))
+        settings_table.append({"notice_type": notice_type, "cells": settings_row})
+    
+    notice_settings = {
+        "column_headers": [medium_display for medium_id, medium_display in NOTICE_MEDIA],
+        "rows": settings_table,
+    }
+    
+    return render_to_response("notification/notices.html", {
+        "notices": notices,
+        "notice_types": notice_types,
+        "notice_settings": notice_settings,
+    }, context_instance=RequestContext(request))
+
+@login_required
+def single(request, id):
+    notice = get_object_or_404(Notice, id=id)
+    if request.user == notice.user:
+        return render_to_response("notification/single.html", {
+            "notice": notice,
+        }, context_instance=RequestContext(request))
+    raise Http404
+
+@login_required
+def archive(request, noticeid=None, next_page=None):
+    if noticeid:
+        try:
+            notice = Notice.objects.get(id=noticeid)
+            if request.user == notice.user or request.user.is_superuser:
+                notice.archive()
+            else:   # you can archive other users' notices
+                    # only if you are superuser.
+                return HttpResponseRedirect(next_page)
+        except Notice.DoesNotExist:
+            return HttpResponseRedirect(next_page)
+    return HttpResponseRedirect(next_page)
+
+@login_required
+def delete(request, noticeid=None, next_page=None):
+    if noticeid:
+        try:
+            notice = Notice.objects.get(id=noticeid)
+            if request.user == notice.user or request.user.is_superuser:
+                notice.delete()
+            else:   # you can delete other users' notices
+                    # only if you are superuser.
+                return HttpResponseRedirect(next_page)
+        except Notice.DoesNotExist:
+            return HttpResponseRedirect(next_page)
+    return HttpResponseRedirect(next_page)
+
+@login_required
+def mark_all_seen(request):
+    for notice in Notice.objects.notices_for(request.user, unseen=True):
+        notice.unseen = False
+        notice.save()
+    return HttpResponseRedirect(reverse("notification_notices"))
+    
\ No newline at end of file

=== modified file 'pip_requirements.txt'
--- pip_requirements.txt	2015-07-04 07:35:29 +0000
+++ pip_requirements.txt	2016-06-28 17:58:37 +0000
@@ -1,25 +1,30 @@
-Django==1.3.7
-wsgiref==0.1.2
--e git://github.com/kerin/django-sphinx.git/@1c5ef8abcf86f9a9458f763ceb9e5d882247ea37#egg=djangosphinx
-docutils==0.7
-Pillow==2.5.0
-pyparsing==1.5.6
--e svn+http://django-pagination.googlecode.com/svn/trunk/#egg=pagination
--e hg+https://django-tracking.googlecode.com/hg/#egg=tracking
-Markdown==2.6.2
-BeautifulSoup==3.2
--e hg+http://bitbucket.org/ubernostrum/django-registration/@1086c6a#egg=registration
--e svn+http://django-tagging.googlecode.com/svn/trunk/#egg=tagging
--e git://github.com/dcramer/django-ratings.git#egg=djangoratings
-django-threadedcomments==0.5.2
--e svn+http://django-messages.googlecode.com/svn/trunk/#egg=django_messages
--e git://github.com/jtauber/django-notification.git@3f023adf0ce2eafcee744904e2c358792f253721#egg=notification
-# Sphinx and sphinxdoc need to match - choose from a similar time.
-sphinx==0.6.5
-django-sphinxdoc==0.3.2
-South==0.7.3
-gunicorn==0.17.4
-numpy
-pydot
-MySQL-python==1.2.5
-ipython
+BeautifulSoup==3.2.0
+Django==1.8
+django-appconf==1.0.1
+django-contrib-comments==1.6.2
+django-messages==0.5.3
+django-nocaptcha-recaptcha==0.0.19
+# next is a more updated version of linaro-django-pagination
+-e git://github.com/zyga/django-pagination.git#egg=django-pagination
+django-registration==2.0.4
+django-tagging==0.4.1
+docutils==0.12
+gunicorn==19.4.5
+Jinja2==2.8
+Markdown==2.6.5
+MarkupSafe==0.23
+mysqlclient==1.3.7
+numpy==1.10.4
+Pillow==3.1.1
+pydot==1.1.0
+Pygments==2.1.3
+pyparsing==2.1.4
+six==1.10.0
+#franku: sphinxdoc is now included in wl
+#django-sphinxdoc==0.3.2
+Sphinx==0.6.5
+untokenize==0.1.1
+#-e git://github.com/kerin/django-sphinx.git/@1c5ef8abcf86f9a9458f763ceb9e5d882247ea37#egg=djangosphinx
+-e git://github.com/kerin/django-sphinx.git#egg=django-sphinx
+bleach==1.4.3
+

=== modified file 'pybb/feeds.py'
--- pybb/feeds.py	2013-10-28 18:56:58 +0000
+++ pybb/feeds.py	2016-06-28 17:58:37 +0000
@@ -1,9 +1,7 @@
-from django.contrib.syndication.feeds import Feed
+from django.contrib.syndication.views import Feed
 from django.core.urlresolvers import reverse
-from django.utils.translation import ugettext_lazy as _
 from django.core.exceptions import ObjectDoesNotExist
-from django.utils.feedgenerator import Atom1Feed, Rss201rev2Feed
-
+from django.utils.feedgenerator import Atom1Feed
 from pybb.models import Post, Topic, Forum
 
 class PybbFeed(Feed):
@@ -14,56 +12,45 @@
             return self.all_title
         else:
             return self.one_title % obj.name
-
-    def description(self,obj):
-        if obj == self.all_objects:
-            return self.all_description
-        else:
-            return self.one_description % obj.name
-
+        
     def items(self, obj):
         if obj == self.all_objects:
             return obj.order_by('-created')[:15]
         else:
             return self.items_for_object(obj)
-
+        
     def link(self, obj):
         if obj == self.all_objects:
             return reverse('pybb_index')
         return "/ewfwevw%s" % reverse('pybb_forum', args=(obj.pk,))
 
-    def get_object(self,bits):
+    def get_object(self,request, *args, **kwargs):
         """
         Implement getting feeds for a specific subforum
         """
-        if len(bits) == 0:
+        if not 'topic_id' in kwargs:
+            # Latest Posts/Topics on all forums
             return self.all_objects
-        if len(bits) == 1:
+        else:
+            # Latest Posts/Topics for specific Forum
             try:
-                forum=Forum.objects.get(pk=int(bits[0]))
+                forum=Forum.objects.get(pk=int(kwargs['topic_id']))
                 return forum
             except ValueError:
                 pass
         raise ObjectDoesNotExist
-
-    ##########################
-    # Individual items below #
-    ##########################
-    def item_id(self, obj):
-        return str(obj.id)
-
-    def item_pubdate(self, obj):
+    
+    # Must be used for valid Atom feeds    
+    def item_updateddate(self, obj):
         return obj.created
-
-    def item_links(self, item):
-        return [{'href': item.get_absolute_url()}, ]
-
-
+    
+    def item_link(self, item):
+        return item.get_absolute_url()
+
+# Validated through http://validator.w3.org/feed/
 class LastPosts(PybbFeed):
-    all_title = _('Latest posts on all forums')
-    all_description = _('Latest posts on all forums')
-    one_title = _('Latest topics on forum %s')
-    one_description = _('Latest topics on forum %s')
+    all_title = 'Latest posts on all forums'
+    one_title = 'Latest posts on forum %s'
     title_template = 'pybb/feeds/posts_title.html'
     description_template = 'pybb/feeds/posts_description.html'
 
@@ -79,19 +66,17 @@
         """
         return item.user.username
 
-
+# Validated through http://validator.w3.org/feed/
 class LastTopics(PybbFeed):
-    all_title = _('Latest topics on all forums')
-    all_description = _('Latest topics on all forums')
-    one_title = _('Latest topics on forum %s')
-    one_description = _('Latest topics on forum %s')
+    all_title = 'Latest topics on all forums'
+    one_title = 'Latest topics on forum %s'
     title_template = 'pybb/feeds/topics_title.html'
     description_template = 'pybb/feeds/topics_description.html'
-
+    
     all_objects = Topic.objects
 
-    def items_for_object(self,obj):
-        return Topic.objects.filter( forum = obj ).order_by('-created')[:15]
+    def items_for_object(self,item):
+        return Topic.objects.filter( forum = item ).order_by('-created')[:15]
 
     def item_author_name(self, item):
         """

=== modified file 'pybb/forms.py'
--- pybb/forms.py	2012-04-20 11:48:50 +0000
+++ pybb/forms.py	2016-06-28 17:58:37 +0000
@@ -10,7 +10,7 @@
 from pybb.models import Topic, Post, PrivateMessage, Attachment
 from pybb import settings as pybb_settings
 
-from notification import models as notification
+from notification.models import send
 
 class AddPostForm(forms.ModelForm):
     name = forms.CharField(label=_('Subject'))
@@ -18,7 +18,8 @@
 
     class Meta:
         model = Post
-        fields = ['body', 'markup',]
+        # Listing fields again to get the the right order; See also the NOCOMM
+        fields = ['name','body', 'markup', 'attachment',]
 
     def __init__(self, *args, **kwargs):
         self.user = kwargs.pop('user', None)
@@ -26,7 +27,8 @@
         self.forum = kwargs.pop('forum', None)
         self.ip = kwargs.pop('ip', None)
         super(AddPostForm, self).__init__(*args, **kwargs)
-
+        
+        # NOCOMM: This doesn't work anymore with django 1.8 Use 'field_order' with django 1.9
         self.fields.keyOrder = ['name', 
                                 'body', 
                                 'markup', 
@@ -64,16 +66,17 @@
         post = Post(topic=topic, user=self.user, user_ip=self.ip,
                     markup=self.cleaned_data['markup'],
                     body=self.cleaned_data['body'])
+
         post.save(*args, **kwargs)
 
         if pybb_settings.ATTACHMENT_ENABLE:
             self.save_attachment(post, self.cleaned_data['attachment'])
 
         if topic_is_new:
-            notification.send(User.objects.all(), "forum_new_topic",
+            send(User.objects.all(), "forum_new_topic",
                 {'topic': topic, 'post':post, 'user':topic.user})
         else:
-            notification.send(self.topic.subscribers.all(), "forum_new_post",
+            send(self.topic.subscribers.all(), "forum_new_post",
                 {'post':post, 'topic':topic, 'user':post.user})
         return post
 

=== added directory 'pybb/migrations'
=== removed directory 'pybb/migrations'
=== added file 'pybb/migrations/0001_initial.py'
--- pybb/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ pybb/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Attachment',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('size', models.IntegerField(verbose_name='Size')),
+                ('content_type', models.CharField(max_length=255, verbose_name='Content type')),
+                ('path', models.CharField(max_length=255, verbose_name='Path')),
+                ('name', models.TextField(verbose_name='Name')),
+                ('hash', models.CharField(default=b'', max_length=40, verbose_name='Hash', db_index=True, blank=True)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='Category',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=80, verbose_name='Name')),
+                ('position', models.IntegerField(default=0, verbose_name='Position', blank=True)),
+            ],
+            options={
+                'ordering': ['position'],
+                'verbose_name': 'Category',
+                'verbose_name_plural': 'Categories',
+            },
+        ),
+        migrations.CreateModel(
+            name='Forum',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=80, verbose_name='Name')),
+                ('position', models.IntegerField(default=0, verbose_name='Position', blank=True)),
+                ('description', models.TextField(default=b'', verbose_name='Description', blank=True)),
+                ('updated', models.DateTimeField(null=True, verbose_name='Updated')),
+                ('category', models.ForeignKey(related_name='forums', verbose_name='Category', to='pybb.Category')),
+                ('moderators', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Moderators', blank=True)),
+            ],
+            options={
+                'ordering': ['position'],
+                'verbose_name': 'Forum',
+                'verbose_name_plural': 'Forums',
+            },
+        ),
+        migrations.CreateModel(
+            name='Post',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('created', models.DateTimeField(verbose_name='Created', blank=True)),
+                ('updated', models.DateTimeField(null=True, verbose_name='Updated', blank=True)),
+                ('markup', models.CharField(default=b'markdown', max_length=15, verbose_name='Markup', choices=[(b'markdown', b'markdown'), (b'bbcode', b'bbcode')])),
+                ('body', models.TextField(verbose_name='Message')),
+                ('body_html', models.TextField(verbose_name='HTML version')),
+                ('body_text', models.TextField(verbose_name='Text version')),
+                ('user_ip', models.GenericIPAddressField(default=b'', verbose_name='User IP')),
+            ],
+            options={
+                'ordering': ['created'],
+                'verbose_name': 'Post',
+                'verbose_name_plural': 'Posts',
+            },
+        ),
+        migrations.CreateModel(
+            name='PrivateMessage',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('read', models.BooleanField(default=False, verbose_name='Read')),
+                ('created', models.DateTimeField(verbose_name='Created', blank=True)),
+                ('markup', models.CharField(default=b'markdown', max_length=15, verbose_name='Markup', choices=[(b'markdown', b'markdown'), (b'bbcode', b'bbcode')])),
+                ('subject', models.CharField(max_length=255, verbose_name='Subject')),
+                ('body', models.TextField(verbose_name='Message')),
+                ('body_html', models.TextField(verbose_name='HTML version')),
+                ('body_text', models.TextField(verbose_name='Text version')),
+                ('dst_user', models.ForeignKey(related_name='dst_users', verbose_name='Recipient', to=settings.AUTH_USER_MODEL)),
+                ('src_user', models.ForeignKey(related_name='src_users', verbose_name='Author', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-created'],
+                'verbose_name': 'Private message',
+                'verbose_name_plural': 'Private messages',
+            },
+        ),
+        migrations.CreateModel(
+            name='Read',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('time', models.DateTimeField(verbose_name='Time', blank=True)),
+            ],
+            options={
+                'verbose_name': 'Read',
+                'verbose_name_plural': 'Reads',
+            },
+        ),
+        migrations.CreateModel(
+            name='Topic',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=255, verbose_name='Subject')),
+                ('created', models.DateTimeField(null=True, verbose_name='Created')),
+                ('updated', models.DateTimeField(null=True, verbose_name='Updated')),
+                ('views', models.IntegerField(default=0, verbose_name='Views count', blank=True)),
+                ('sticky', models.BooleanField(default=False, verbose_name='Sticky')),
+                ('closed', models.BooleanField(default=False, verbose_name='Closed')),
+                ('forum', models.ForeignKey(related_name='topics', verbose_name='Forum', to='pybb.Forum')),
+                ('subscribers', models.ManyToManyField(related_name='subscriptions', verbose_name='Subscribers', to=settings.AUTH_USER_MODEL, blank=True)),
+                ('user', models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ['-updated'],
+                'verbose_name': 'Topic',
+                'verbose_name_plural': 'Topics',
+            },
+        ),
+        migrations.AddField(
+            model_name='read',
+            name='topic',
+            field=models.ForeignKey(verbose_name='Topic', to='pybb.Topic'),
+        ),
+        migrations.AddField(
+            model_name='read',
+            name='user',
+            field=models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='post',
+            name='topic',
+            field=models.ForeignKey(related_name='posts', verbose_name='Topic', to='pybb.Topic'),
+        ),
+        migrations.AddField(
+            model_name='post',
+            name='user',
+            field=models.ForeignKey(related_name='posts', verbose_name='User', to=settings.AUTH_USER_MODEL),
+        ),
+        migrations.AddField(
+            model_name='attachment',
+            name='post',
+            field=models.ForeignKey(related_name='attachments', verbose_name='Post', to='pybb.Post'),
+        ),
+        migrations.AlterUniqueTogether(
+            name='read',
+            unique_together=set([('user', 'topic')]),
+        ),
+    ]

=== removed file 'pybb/migrations/0001_initial.py'
--- pybb/migrations/0001_initial.py	2012-03-17 16:22:06 +0000
+++ pybb/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
@@ -1,261 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
-    def forwards(self, orm):
-        
-        # Adding model 'Category'
-        db.create_table('pybb_category', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=80)),
-            ('position', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
-        ))
-        db.send_create_signal('pybb', ['Category'])
-
-        # Adding model 'Forum'
-        db.create_table('pybb_forum', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('category', self.gf('django.db.models.fields.related.ForeignKey')(related_name='forums', to=orm['pybb.Category'])),
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=80)),
-            ('position', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
-            ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
-            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True)),
-        ))
-        db.send_create_signal('pybb', ['Forum'])
-
-        # Adding M2M table for field moderators on 'Forum'
-        db.create_table('pybb_forum_moderators', (
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
-            ('forum', models.ForeignKey(orm['pybb.forum'], null=False)),
-            ('user', models.ForeignKey(orm['auth.user'], null=False))
-        ))
-        db.create_unique('pybb_forum_moderators', ['forum_id', 'user_id'])
-
-        # Adding model 'Topic'
-        db.create_table('pybb_topic', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(related_name='topics', to=orm['pybb.Forum'])),
-            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
-            ('created', self.gf('django.db.models.fields.DateTimeField')(null=True)),
-            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True)),
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
-            ('views', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
-            ('sticky', self.gf('django.db.models.fields.BooleanField')(default=False)),
-            ('closed', self.gf('django.db.models.fields.BooleanField')(default=False)),
-            ('post_count', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
-        ))
-        db.send_create_signal('pybb', ['Topic'])
-
-        # Adding M2M table for field subscribers on 'Topic'
-        db.create_table('pybb_topic_subscribers', (
-            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
-            ('topic', models.ForeignKey(orm['pybb.topic'], null=False)),
-            ('user', models.ForeignKey(orm['auth.user'], null=False))
-        ))
-        db.create_unique('pybb_topic_subscribers', ['topic_id', 'user_id'])
-
-        # Adding model 'Post'
-        db.create_table('pybb_post', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', to=orm['pybb.Topic'])),
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', to=orm['auth.User'])),
-            ('created', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
-            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
-            ('markup', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=15)),
-            ('body', self.gf('django.db.models.fields.TextField')()),
-            ('body_html', self.gf('django.db.models.fields.TextField')()),
-            ('body_text', self.gf('django.db.models.fields.TextField')()),
-            ('user_ip', self.gf('django.db.models.fields.IPAddressField')(default='', max_length=15, blank=True)),
-        ))
-        db.send_create_signal('pybb', ['Post'])
-
-        # Adding model 'Read'
-        db.create_table('pybb_read', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
-            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['pybb.Topic'])),
-            ('time', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
-        ))
-        db.send_create_signal('pybb', ['Read'])
-
-        # Adding unique constraint on 'Read', fields ['user', 'topic']
-        db.create_unique('pybb_read', ['user_id', 'topic_id'])
-
-        # Adding model 'PrivateMessage'
-        db.create_table('pybb_privatemessage', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('dst_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='dst_users', to=orm['auth.User'])),
-            ('src_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='src_users', to=orm['auth.User'])),
-            ('read', self.gf('django.db.models.fields.BooleanField')(default=False)),
-            ('created', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
-            ('markup', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=15)),
-            ('subject', self.gf('django.db.models.fields.CharField')(max_length=255)),
-            ('body', self.gf('django.db.models.fields.TextField')()),
-            ('body_html', self.gf('django.db.models.fields.TextField')()),
-            ('body_text', self.gf('django.db.models.fields.TextField')()),
-        ))
-        db.send_create_signal('pybb', ['PrivateMessage'])
-
-        # Adding model 'Attachment'
-        db.create_table('pybb_attachment', (
-            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
-            ('post', self.gf('django.db.models.fields.related.ForeignKey')(related_name='attachments', to=orm['pybb.Post'])),
-            ('size', self.gf('django.db.models.fields.IntegerField')()),
-            ('content_type', self.gf('django.db.models.fields.CharField')(max_length=255)),
-            ('path', self.gf('django.db.models.fields.CharField')(max_length=255)),
-            ('name', self.gf('django.db.models.fields.TextField')()),
-            ('hash', self.gf('django.db.models.fields.CharField')(default='', max_length=40, db_index=True, blank=True)),
-        ))
-        db.send_create_signal('pybb', ['Attachment'])
-
-
-    def backwards(self, orm):
-        
-        # Removing unique constraint on 'Read', fields ['user', 'topic']
-        db.delete_unique('pybb_read', ['user_id', 'topic_id'])
-
-        # Deleting model 'Category'
-        db.delete_table('pybb_category')
-
-        # Deleting model 'Forum'
-        db.delete_table('pybb_forum')
-
-        # Removing M2M table for field moderators on 'Forum'
-        db.delete_table('pybb_forum_moderators')
-
-        # Deleting model 'Topic'
-        db.delete_table('pybb_topic')
-
-        # Removing M2M table for field subscribers on 'Topic'
-        db.delete_table('pybb_topic_subscribers')
-
-        # Deleting model 'Post'
-        db.delete_table('pybb_post')
-
-        # Deleting model 'Read'
-        db.delete_table('pybb_read')
-
-        # Deleting model 'PrivateMessage'
-        db.delete_table('pybb_privatemessage')
-
-        # Deleting model 'Attachment'
-        db.delete_table('pybb_attachment')
-
-
-    models = {
-        'auth.group': {
-            'Meta': {'object_name': 'Group'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
-        },
-        'auth.permission': {
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
-        },
-        'auth.user': {
-            'Meta': {'object_name': 'User'},
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
-        },
-        'contenttypes.contenttype': {
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
-        },
-        'pybb.attachment': {
-            'Meta': {'object_name': 'Attachment'},
-            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'db_index': 'True', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.TextField', [], {}),
-            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['pybb.Post']"}),
-            'size': ('django.db.models.fields.IntegerField', [], {})
-        },
-        'pybb.category': {
-            'Meta': {'ordering': "['position']", 'object_name': 'Category'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
-            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
-        },
-        'pybb.forum': {
-            'Meta': {'ordering': "['position']", 'object_name': 'Forum'},
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'forums'", 'to': "orm['pybb.Category']"}),
-            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'moderators': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
-            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'})
-        },
-        'pybb.post': {
-            'Meta': {'ordering': "['created']", 'object_name': 'Post'},
-            'body': ('django.db.models.fields.TextField', [], {}),
-            'body_html': ('django.db.models.fields.TextField', [], {}),
-            'body_text': ('django.db.models.fields.TextField', [], {}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
-            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['pybb.Topic']"}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
-            'user_ip': ('django.db.models.fields.IPAddressField', [], {'default': "''", 'max_length': '15', 'blank': 'True'})
-        },
-        'pybb.privatemessage': {
-            'Meta': {'ordering': "['-created']", 'object_name': 'PrivateMessage'},
-            'body': ('django.db.models.fields.TextField', [], {}),
-            'body_html': ('django.db.models.fields.TextField', [], {}),
-            'body_text': ('django.db.models.fields.TextField', [], {}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'dst_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dst_users'", 'to': "orm['auth.User']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
-            'read': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'src_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'src_users'", 'to': "orm['auth.User']"}),
-            'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
-        },
-        'pybb.read': {
-            'Meta': {'unique_together': "(['user', 'topic'],)", 'object_name': 'Read'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'time': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pybb.Topic']"}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        },
-        'pybb.topic': {
-            'Meta': {'ordering': "['-updated']", 'object_name': 'Topic'},
-            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
-            'forum': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': "orm['pybb.Forum']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'post_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
-            'sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'subscriptions'", 'blank': 'True', 'to': "orm['auth.User']"}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
-            'views': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
-        }
-    }
-
-    complete_apps = ['pybb']

=== removed file 'pybb/migrations/0002_auto__del_field_topic_post_count.py'
--- pybb/migrations/0002_auto__del_field_topic_post_count.py	2012-03-17 16:22:06 +0000
+++ pybb/migrations/0002_auto__del_field_topic_post_count.py	1970-01-01 00:00:00 +0000
@@ -1,132 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
-    def forwards(self, orm):
-        
-        # Deleting field 'Topic.post_count'
-        db.delete_column('pybb_topic', 'post_count')
-
-
-    def backwards(self, orm):
-        
-        # Adding field 'Topic.post_count'
-        db.add_column('pybb_topic', 'post_count', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True), keep_default=False)
-
-
-    models = {
-        'auth.group': {
-            'Meta': {'object_name': 'Group'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
-            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
-        },
-        'auth.permission': {
-            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
-            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
-        },
-        'auth.user': {
-            'Meta': {'object_name': 'User'},
-            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
-            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
-            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
-            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
-            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
-            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
-            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
-        },
-        'contenttypes.contenttype': {
-            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
-            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
-        },
-        'pybb.attachment': {
-            'Meta': {'object_name': 'Attachment'},
-            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'db_index': 'True', 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.TextField', [], {}),
-            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['pybb.Post']"}),
-            'size': ('django.db.models.fields.IntegerField', [], {})
-        },
-        'pybb.category': {
-            'Meta': {'ordering': "['position']", 'object_name': 'Category'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
-            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
-        },
-        'pybb.forum': {
-            'Meta': {'ordering': "['position']", 'object_name': 'Forum'},
-            'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'forums'", 'to': "orm['pybb.Category']"}),
-            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'moderators': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
-            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'})
-        },
-        'pybb.post': {
-            'Meta': {'ordering': "['created']", 'object_name': 'Post'},
-            'body': ('django.db.models.fields.TextField', [], {}),
-            'body_html': ('django.db.models.fields.TextField', [], {}),
-            'body_text': ('django.db.models.fields.TextField', [], {}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
-            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['pybb.Topic']"}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
-            'user_ip': ('django.db.models.fields.IPAddressField', [], {'default': "''", 'max_length': '15', 'blank': 'True'})
-        },
-        'pybb.privatemessage': {
-            'Meta': {'ordering': "['-created']", 'object_name': 'PrivateMessage'},
-            'body': ('django.db.models.fields.TextField', [], {}),
-            'body_html': ('django.db.models.fields.TextField', [], {}),
-            'body_text': ('django.db.models.fields.TextField', [], {}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'dst_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dst_users'", 'to': "orm['auth.User']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
-            'read': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'src_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'src_users'", 'to': "orm['auth.User']"}),
-            'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
-        },
-        'pybb.read': {
-            'Meta': {'unique_together': "(['user', 'topic'],)", 'object_name': 'Read'},
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'time': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
-            'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pybb.Topic']"}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
-        },
-        'pybb.topic': {
-            'Meta': {'ordering': "['-updated']", 'object_name': 'Topic'},
-            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
-            'forum': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': "orm['pybb.Forum']"}),
-            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
-            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
-            'sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
-            'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'subscriptions'", 'blank': 'True', 'to': "orm['auth.User']"}),
-            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
-            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
-            'views': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
-        }
-    }
-
-    complete_apps = ['pybb']

=== added file 'pybb/migrations/__init__.py'
=== removed file 'pybb/migrations/__init__.py'
=== modified file 'pybb/models.py'
--- pybb/models.py	2012-03-17 16:22:06 +0000
+++ pybb/models.py	2016-06-28 17:58:37 +0000
@@ -62,7 +62,7 @@
     name = models.CharField(_('Name'), max_length=80)
     position = models.IntegerField(_('Position'), blank=True, default=0)
     description = models.TextField(_('Description'), blank=True, default='')
-    moderators = models.ManyToManyField(User, blank=True, null=True, verbose_name=_('Moderators'))
+    moderators = models.ManyToManyField(User, blank=True, verbose_name=_('Moderators'))
     updated = models.DateTimeField(_('Updated'), null=True)
 
     class Meta:
@@ -171,7 +171,7 @@
         if self.markup == 'bbcode':
             self.body_html = mypostmarkup.markup(self.body, auto_urls=False)
         elif self.markup == 'markdown':
-            self.body_html = unicode(do_wl_markdown(self.body, safe_mode='escape', wikiwords=False))
+            self.body_html = unicode(do_wl_markdown(self.body, 'bleachit', wikiwords=False))
         else:
             raise Exception('Invalid markup property: %s' % self.markup)
 
@@ -183,7 +183,6 @@
 
         self.body_html = urlize(self.body_html)
 
-
 class Post(RenderableItem):
     topic = models.ForeignKey(Topic, related_name='posts', verbose_name=_('Topic'))
     user = models.ForeignKey(User, related_name='posts', verbose_name=_('User'))
@@ -193,7 +192,7 @@
     body = models.TextField(_('Message'))
     body_html = models.TextField(_('HTML version'))
     body_text = models.TextField(_('Text version'))
-    user_ip = models.IPAddressField(_('User IP'), blank=True, default='')
+    user_ip = models.GenericIPAddressField(_('User IP'), default='')
 
     # Django sphinx
     if settings.USE_SPHINX:
@@ -349,8 +348,8 @@
                             self.path)
 
 
-#if notification is not None:
-#    signals.post_save.connect(notification.handle_observations, sender=Post)
+if notification is not None:
+    signals.post_save.connect(notification.handle_observations, sender=Post)
 
 from pybb import signals
 signals.setup_signals()

=== modified file 'pybb/settings.py'
--- pybb/settings.py	2009-02-25 16:55:36 +0000
+++ pybb/settings.py	2016-06-28 17:58:37 +0000
@@ -3,7 +3,6 @@
 def get(key, default):
     return getattr(settings, key, default)
 
-
 TOPIC_PAGE_SIZE = get('PYBB_TOPIC_PAGE_SIZE', 10)
 FORUM_PAGE_SIZE = get('PYBB_FORUM_PAGE_SIZE', 20)
 USERS_PAGE_SIZE = get('PYBB_USERS_PAGE_SIZE', 20)

=== added directory 'pybb/south_migrations'
=== added file 'pybb/south_migrations/0001_initial.py'
--- pybb/south_migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ pybb/south_migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,261 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Adding model 'Category'
+        db.create_table('pybb_category', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=80)),
+            ('position', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
+        ))
+        db.send_create_signal('pybb', ['Category'])
+
+        # Adding model 'Forum'
+        db.create_table('pybb_forum', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('category', self.gf('django.db.models.fields.related.ForeignKey')(related_name='forums', to=orm['pybb.Category'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=80)),
+            ('position', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
+            ('description', self.gf('django.db.models.fields.TextField')(default='', blank=True)),
+            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+        ))
+        db.send_create_signal('pybb', ['Forum'])
+
+        # Adding M2M table for field moderators on 'Forum'
+        db.create_table('pybb_forum_moderators', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('forum', models.ForeignKey(orm['pybb.forum'], null=False)),
+            ('user', models.ForeignKey(orm['auth.user'], null=False))
+        ))
+        db.create_unique('pybb_forum_moderators', ['forum_id', 'user_id'])
+
+        # Adding model 'Topic'
+        db.create_table('pybb_topic', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(related_name='topics', to=orm['pybb.Forum'])),
+            ('name', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('created', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('views', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
+            ('sticky', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('closed', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('post_count', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True)),
+        ))
+        db.send_create_signal('pybb', ['Topic'])
+
+        # Adding M2M table for field subscribers on 'Topic'
+        db.create_table('pybb_topic_subscribers', (
+            ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
+            ('topic', models.ForeignKey(orm['pybb.topic'], null=False)),
+            ('user', models.ForeignKey(orm['auth.user'], null=False))
+        ))
+        db.create_unique('pybb_topic_subscribers', ['topic_id', 'user_id'])
+
+        # Adding model 'Post'
+        db.create_table('pybb_post', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', to=orm['pybb.Topic'])),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='posts', to=orm['auth.User'])),
+            ('created', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
+            ('updated', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
+            ('markup', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=15)),
+            ('body', self.gf('django.db.models.fields.TextField')()),
+            ('body_html', self.gf('django.db.models.fields.TextField')()),
+            ('body_text', self.gf('django.db.models.fields.TextField')()),
+            ('user_ip', self.gf('django.db.models.fields.IPAddressField')(default='', max_length=15, blank=True)),
+        ))
+        db.send_create_signal('pybb', ['Post'])
+
+        # Adding model 'Read'
+        db.create_table('pybb_read', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+            ('topic', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['pybb.Topic'])),
+            ('time', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
+        ))
+        db.send_create_signal('pybb', ['Read'])
+
+        # Adding unique constraint on 'Read', fields ['user', 'topic']
+        db.create_unique('pybb_read', ['user_id', 'topic_id'])
+
+        # Adding model 'PrivateMessage'
+        db.create_table('pybb_privatemessage', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('dst_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='dst_users', to=orm['auth.User'])),
+            ('src_user', self.gf('django.db.models.fields.related.ForeignKey')(related_name='src_users', to=orm['auth.User'])),
+            ('read', self.gf('django.db.models.fields.BooleanField')(default=False)),
+            ('created', self.gf('django.db.models.fields.DateTimeField')(blank=True)),
+            ('markup', self.gf('django.db.models.fields.CharField')(default='markdown', max_length=15)),
+            ('subject', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('body', self.gf('django.db.models.fields.TextField')()),
+            ('body_html', self.gf('django.db.models.fields.TextField')()),
+            ('body_text', self.gf('django.db.models.fields.TextField')()),
+        ))
+        db.send_create_signal('pybb', ['PrivateMessage'])
+
+        # Adding model 'Attachment'
+        db.create_table('pybb_attachment', (
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('post', self.gf('django.db.models.fields.related.ForeignKey')(related_name='attachments', to=orm['pybb.Post'])),
+            ('size', self.gf('django.db.models.fields.IntegerField')()),
+            ('content_type', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('path', self.gf('django.db.models.fields.CharField')(max_length=255)),
+            ('name', self.gf('django.db.models.fields.TextField')()),
+            ('hash', self.gf('django.db.models.fields.CharField')(default='', max_length=40, db_index=True, blank=True)),
+        ))
+        db.send_create_signal('pybb', ['Attachment'])
+
+
+    def backwards(self, orm):
+        
+        # Removing unique constraint on 'Read', fields ['user', 'topic']
+        db.delete_unique('pybb_read', ['user_id', 'topic_id'])
+
+        # Deleting model 'Category'
+        db.delete_table('pybb_category')
+
+        # Deleting model 'Forum'
+        db.delete_table('pybb_forum')
+
+        # Removing M2M table for field moderators on 'Forum'
+        db.delete_table('pybb_forum_moderators')
+
+        # Deleting model 'Topic'
+        db.delete_table('pybb_topic')
+
+        # Removing M2M table for field subscribers on 'Topic'
+        db.delete_table('pybb_topic_subscribers')
+
+        # Deleting model 'Post'
+        db.delete_table('pybb_post')
+
+        # Deleting model 'Read'
+        db.delete_table('pybb_read')
+
+        # Deleting model 'PrivateMessage'
+        db.delete_table('pybb_privatemessage')
+
+        # Deleting model 'Attachment'
+        db.delete_table('pybb_attachment')
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'pybb.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'db_index': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['pybb.Post']"}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'pybb.category': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Category'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        },
+        'pybb.forum': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Forum'},
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'forums'", 'to': "orm['pybb.Category']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'moderators': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'})
+        },
+        'pybb.post': {
+            'Meta': {'ordering': "['created']", 'object_name': 'Post'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'body_html': ('django.db.models.fields.TextField', [], {}),
+            'body_text': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['pybb.Topic']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+            'user_ip': ('django.db.models.fields.IPAddressField', [], {'default': "''", 'max_length': '15', 'blank': 'True'})
+        },
+        'pybb.privatemessage': {
+            'Meta': {'ordering': "['-created']", 'object_name': 'PrivateMessage'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'body_html': ('django.db.models.fields.TextField', [], {}),
+            'body_text': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'dst_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dst_users'", 'to': "orm['auth.User']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
+            'read': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'src_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'src_users'", 'to': "orm['auth.User']"}),
+            'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'pybb.read': {
+            'Meta': {'unique_together': "(['user', 'topic'],)", 'object_name': 'Read'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'time': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pybb.Topic']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'pybb.topic': {
+            'Meta': {'ordering': "['-updated']", 'object_name': 'Topic'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': "orm['pybb.Forum']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post_count': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'subscriptions'", 'blank': 'True', 'to': "orm['auth.User']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'views': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['pybb']

=== added file 'pybb/south_migrations/0002_auto__del_field_topic_post_count.py'
--- pybb/south_migrations/0002_auto__del_field_topic_post_count.py	1970-01-01 00:00:00 +0000
+++ pybb/south_migrations/0002_auto__del_field_topic_post_count.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,132 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        
+        # Deleting field 'Topic.post_count'
+        db.delete_column('pybb_topic', 'post_count')
+
+
+    def backwards(self, orm):
+        
+        # Adding field 'Topic.post_count'
+        db.add_column('pybb_topic', 'post_count', self.gf('django.db.models.fields.IntegerField')(default=0, blank=True), keep_default=False)
+
+
+    models = {
+        'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        'auth.permission': {
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        'pybb.attachment': {
+            'Meta': {'object_name': 'Attachment'},
+            'content_type': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'hash': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '40', 'db_index': 'True', 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.TextField', [], {}),
+            'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attachments'", 'to': "orm['pybb.Post']"}),
+            'size': ('django.db.models.fields.IntegerField', [], {})
+        },
+        'pybb.category': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Category'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        },
+        'pybb.forum': {
+            'Meta': {'ordering': "['position']", 'object_name': 'Forum'},
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'forums'", 'to': "orm['pybb.Category']"}),
+            'description': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'moderators': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '80'}),
+            'position': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'})
+        },
+        'pybb.post': {
+            'Meta': {'ordering': "['created']", 'object_name': 'Post'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'body_html': ('django.db.models.fields.TextField', [], {}),
+            'body_text': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['pybb.Topic']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+            'user_ip': ('django.db.models.fields.IPAddressField', [], {'default': "''", 'max_length': '15', 'blank': 'True'})
+        },
+        'pybb.privatemessage': {
+            'Meta': {'ordering': "['-created']", 'object_name': 'PrivateMessage'},
+            'body': ('django.db.models.fields.TextField', [], {}),
+            'body_html': ('django.db.models.fields.TextField', [], {}),
+            'body_text': ('django.db.models.fields.TextField', [], {}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'dst_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dst_users'", 'to': "orm['auth.User']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'markup': ('django.db.models.fields.CharField', [], {'default': "'markdown'", 'max_length': '15'}),
+            'read': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'src_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'src_users'", 'to': "orm['auth.User']"}),
+            'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        'pybb.read': {
+            'Meta': {'unique_together': "(['user', 'topic'],)", 'object_name': 'Read'},
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'time': ('django.db.models.fields.DateTimeField', [], {'blank': 'True'}),
+            'topic': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['pybb.Topic']"}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+        },
+        'pybb.topic': {
+            'Meta': {'ordering': "['-updated']", 'object_name': 'Topic'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'created': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'topics'", 'to': "orm['pybb.Forum']"}),
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'sticky': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'subscriptions'", 'blank': 'True', 'to': "orm['auth.User']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+            'views': ('django.db.models.fields.IntegerField', [], {'default': '0', 'blank': 'True'})
+        }
+    }
+
+    complete_apps = ['pybb']

=== added file 'pybb/south_migrations/__init__.py'
=== modified file 'pybb/templatetags/pybb_extras.py'
--- pybb/templatetags/pybb_extras.py	2013-06-14 19:23:53 +0000
+++ pybb/templatetags/pybb_extras.py	2016-06-28 17:58:37 +0000
@@ -10,7 +10,6 @@
 from django.template import RequestContext
 from django.template.defaultfilters import stringfilter
 from django.utils.encoding import smart_unicode
-from django.db import settings
 from django.utils.html import escape
 from django.utils.translation import ugettext as _
 from django.utils import dateformat

=== modified file 'pybb/urls.py'
--- pybb/urls.py	2012-04-19 19:46:21 +0000
+++ pybb/urls.py	2016-06-28 17:58:37 +0000
@@ -1,20 +1,19 @@
-from django.conf.urls.defaults import *
+from django.conf.urls import *
 
 from pybb import views
 from pybb.feeds import LastPosts, LastTopics
 
-feeds = {
-    'posts': LastPosts,
-    'topics': LastTopics,
-}
-
-urlpatterns = patterns('',
+urlpatterns = [
     # Misc
     url('^$', views.index, name='pybb_index'),
     url('^category/(?P<category_id>\d+)/$', views.show_category, name='pybb_category'),
     url('^forum/(?P<forum_id>\d+)/$', views.show_forum, name='pybb_forum'),
-    url('^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
-        {'feed_dict': feeds}, name='pybb_feed'),
+    
+    # Feeds
+    url('^feeds/topics/(?P<topic_id>\d+)/$', LastTopics(), name='pybb_feed_topics'),
+    url('^feeds/posts/(?P<topic_id>\d+)/$', LastPosts(), name='pybb_feed_posts'),
+    url('^feeds/topics/$', LastTopics(), name='pybb_feed_topics'),
+    url('^feeds/posts/$', LastPosts(), name='pybb_feed_posts'),
 
     # Topic
     url('^topic/(?P<topic_id>\d+)/$', views.show_topic, name='pybb_topic'),
@@ -41,4 +40,4 @@
     # Subsciption
     url('^topic/(?P<topic_id>\d+)/subscribe/$', views.add_subscription, name='pybb_add_subscription'),
     url('^topic/(?P<topic_id>\d+)/unsubscribe/$', views.delete_subscription, name='pybb_delete_subscription'),
-)
+]

=== modified file 'pybb/util.py'
--- pybb/util.py	2015-01-08 21:40:25 +0000
+++ pybb/util.py	2016-06-28 17:58:37 +0000
@@ -1,20 +1,20 @@
-from datetime import datetime
 import os.path
 import random
+import traceback
+import json
+
 from BeautifulSoup import BeautifulSoup
-import traceback
-
+from datetime import datetime
 from django.shortcuts import render_to_response
 from django.template import RequestContext
 from django.http import HttpResponse
 from django.utils.functional import Promise
-from django.utils.translation import force_unicode, check_for_language
-from django.utils.simplejson import JSONEncoder
+from django.utils.translation import check_for_language
+from django.utils.encoding import force_unicode
 from django import forms
 from django.template.defaultfilters import urlize as django_urlize
 from django.core.paginator import Paginator, EmptyPage, InvalidPage
 from django.conf import settings
-
 from pybb import settings as pybb_settings
 
 
@@ -32,12 +32,16 @@
             if not isinstance(output, dict):
                 return output
             kwargs = {'context_instance': RequestContext(request)}
+ 
+            # NOCOMM: 'MIME_TYPE' is never in output as i can see for now.
+            # But if, this should maybe 'content_type' instead
             if 'MIME_TYPE' in output:
                 kwargs['mimetype'] = output.pop('MIME_TYPE')
             if 'TEMPLATE' in output:
                 template = output.pop('TEMPLATE')
             else:
                 template = template_path
+                
             return render_to_response(template, output, **kwargs)
         return wrapper
 
@@ -106,7 +110,7 @@
     return wrapper
 
 
-class LazyJSONEncoder(JSONEncoder):
+class LazyJSONEncoder(json.JSONEncoder):
     """
     This fing need to save django from crashing.
     """
@@ -122,11 +126,11 @@
     """
     HttpResponse subclass that serialize data into JSON format.
     """
-
+    # NOCOMM: The mimetype argument maybe must be replaced with content_type
     def __init__(self, data, mimetype='application/json'):
         json_data = LazyJSONEncoder().encode(data)
         super(JsonResponse, self).__init__(
-            content=json_data, mimetype=mimetype)
+            content=json_data, content_type=mimetype)
 
         
 def build_form(Form, _request, GET=False, *args, **kwargs):
@@ -159,9 +163,9 @@
                     islink = True
                     break
                 ptr = ptr.parent
-
             if not islink:
-                chunk = chunk.replaceWith(django_urlize(unicode(chunk)))
+                # Using unescape to prevent conversation of f.e. &gt; to &amp;gt;
+                chunk = chunk.replaceWith(django_urlize(unicode(unescape(chunk))))
 
         return unicode(soup)
 
@@ -240,11 +244,12 @@
     return page, paginator
 
 
+# NOCOMM: This function is never used AFAIK
+# 'django_language' isn't available since django 1.8
 def set_language(request, language):
     """
     Change the language of session of authenticated user.
     """
-
     if language and check_for_language(language):
         if hasattr(request, 'session'):
             request.session['django_language'] = language

=== modified file 'pybb/views.py'
--- pybb/views.py	2012-04-20 11:48:50 +0000
+++ pybb/views.py	2016-06-28 17:58:37 +0000
@@ -76,6 +76,7 @@
 
     
 def show_topic_ctx(request, topic_id):
+
     try:
         topic = Topic.objects.select_related().get(pk=topic_id)
     except Topic.DoesNotExist:
@@ -107,7 +108,6 @@
     page, paginator = paginate(posts, request, pybb_settings.TOPIC_PAGE_SIZE,
                                total_count=topic.post_count)
 
-
     # TODO: fetch profiles
     # profiles = Profile.objects.filter(user__pk__in=
     #     set(x.user.id for x in page.object_list))
@@ -349,8 +349,7 @@
     if markup == 'bbcode':
         html = mypostmarkup.markup(content, auto_urls=False)
     elif markup == 'markdown':
-        html = unicode(do_wl_markdown(content, safe_mode='escape', wikiwords=False))
-
+        html = unicode(do_wl_markdown(content, 'bleachit', wikiwords=False))
 
     html = urlize(html)
     return {'content': html}

=== modified file 'settings.py'
--- settings.py	2016-01-14 19:12:16 +0000
+++ settings.py	2016-06-28 17:58:37 +0000
@@ -1,7 +1,10 @@
 # Django settings for widelands project.
 
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+ '/widelands'
 DEBUG = True
-TEMPLATE_DEBUG = DEBUG
 
 ADMINS = (
     # ('Your Name', 'your_email@xxxxxxxxxx'),
@@ -10,14 +13,16 @@
 MANAGERS = ADMINS
 
 DATABASES = {
-   'default': {
-      'ENGINE': 'django.db.backends.sqlite3',
-      'NAME': 'dev.db',
-      'USER': '',      # Not used with sqlite3.
-      'PASSWORD': '',  # Not used with sqlite3.
-      'HOST': '',      # Set to empty string for localhost. Not used with sqlite3.
-      'PORT': '',      # Set to empty string for default. Not used with sqlite3.
-   }
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': 'dev.db',
+        'USER': '',      # Not used with sqlite3.
+        'PASSWORD': '',  # Not used with sqlite3.
+        # Set to empty string for localhost. Not used with sqlite3.
+        'HOST': '',
+        # Set to empty string for default. Not used with sqlite3.
+        'PORT': '',
+    }
 }
 
 # Local time zone for this installation. Choices can be found here:
@@ -26,6 +31,7 @@
 # If running in a Windows environment this must be set to the same as your
 # system time zone.
 TIME_ZONE = 'Europe/Berlin'
+USE_TZ = False # See https://docs.djangoproject.com/en/1.8/ref/settings/#std:setting-TIME_ZONE
 
 # Language code for this installation. All choices can be found here:
 # http://www.i18nguy.com/unicode/language-identifiers.html
@@ -34,7 +40,7 @@
 SITE_ID = 1
 
 # Where should logged in user go by default?
-LOGIN_REDIRECT_URL="/"
+LOGIN_REDIRECT_URL = '/'
 
 # If you set this to False, Django will make some optimizations so as not
 # to load the internationalization machinery.
@@ -49,57 +55,114 @@
 # Examples: "http://media.lawrence.com";, "http://example.com/media/";
 MEDIA_URL = '/wlmedia/'
 
-# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
-# trailing slash.
-# Examples: "http://foo.com/media/";, "/media/".
-ADMIN_MEDIA_PREFIX = '/media/'
-
 # Make this unique, and don't share it with anybody.
 SECRET_KEY = '#*bc7*q0-br42fc&6l^x@zzk&(=-#gr!)fn@t30n54n05jkqcu'
 
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
-    'django.template.loaders.filesystem.Loader',
-    'django.template.loaders.app_directories.Loader',
-#     'django.template.loaders.eggs.load_template_source',
+ROOT_URLCONF = 'urls'
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = [
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+]
+
+INSTALLED_APPS = (
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'django.contrib.sites',
+    'django.contrib.humanize',
+    'django_comments',
+    'nocaptcha_recaptcha',
+    # Thirdparty apps, but need preload
+    'tracking', # included as wlapp
+
+    # Our own apps
+    'wiki.templatetags.restructuredtext',
+    'mainpage',
+    'wlhelp',
+    'wlimages',
+    'wlwebchat',
+    'wlprofile',
+    'wlsearch',
+    'wlpoll',
+    'wlevents',
+    'wlmaps',
+    'wlscreens',
+    'wlggz',
+
+    # Modified 3rd party apps
+    'wiki',  # This is based on wikiapp, but has some local modifications
+    'news',  # This is based on simple-blog, but has some local modifications
+    'news.managers',
+    'pybb',  # Feature enriched version of pybb
+
+    # Thirdparty apps
+    'threadedcomments', # included as wlapp
+    'notification',     # included as wlapp
+    'django_messages',
+    #'pagination',
+     'linaro_django_pagination',
+    'tagging',
+    'djangoratings',    # included as wlapp
+    'sphinxdoc',        # included as wlapp
+    #'south',           included in django itself
 )
 
 MIDDLEWARE_CLASSES = (
     # 'simplestats.middleware.RegexLoggingMiddleware',
-    'django.middleware.gzip.GZipMiddleware', # Remove this, when load gets to high or attachments are enabled
     'django.middleware.common.CommonMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'pagination.middleware.PaginationMiddleware',
+    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+    'django.middleware.security.SecurityMiddleware',
+
+    # Remove this, when load gets to high or attachments are enabled
+    'django.middleware.gzip.GZipMiddleware',
+    #'pagination.middleware.PaginationMiddleware',
+    'linaro_django_pagination.middleware.PaginationMiddleware',
     'tracking.middleware.VisitorTrackingMiddleware',
     'tracking.middleware.VisitorCleanUpMiddleware',
 )
 
-ROOT_URLCONF = 'widelands.urls'
-
-TEMPLATE_DIRS = (
-    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
-    # Always use forward slashes, even on Windows.
-    # Don't forget to use absolute paths, not relative paths.
-    '/var/www/django_projects/widelands/templates',
-)
-
-TEMPLATE_CONTEXT_PROCESSORS = (
-    "django.contrib.auth.context_processors.auth",
-    "django_messages.context_processors.inbox",
-    "django.core.context_processors.debug",
-    "django.core.context_processors.i18n",
-    "django.core.context_processors.media",
-    'django.core.context_processors.request',
-    'widelands.mainpage.context_processors.settings_for_templates',
-)
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [os.path.join(BASE_DIR, 'templates')],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+                'django.template.context_processors.i18n',
+                'django.template.context_processors.media',
+                'django.template.context_processors.static',
+                'django.template.context_processors.tz',
+                'django_messages.context_processors.inbox',
+                'mainpage.context_processors.settings_for_templates'
+            ],
+        },
+    },
+]
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.8/howto/static-files/
+STATIC_URL = '/media/'
 
 ############################
 # Activation configuration #
 ############################
 DEFAULT_FROM_EMAIL = 'noreply@xxxxxxxxxxxxx'
-ACCOUNT_ACTIVATION_DAYS=2 # Days an activation token keeps active
+ACCOUNT_ACTIVATION_DAYS = 2  # Days an activation token keeps active
 
 ######################
 # Wiki configuration #
@@ -111,19 +174,21 @@
 ######################
 # User configuration #
 ######################
-AUTH_PROFILE_MODULE = 'wlprofile.Profile'
+#AUTH_PROFILE_MODULE = 'wlprofile.Profile' # NOCOMM: This is not longer used anymore, see:
+# https://docs.djangoproject.com/en/1.8/releases/1.5/#auth-profile-module
+
 DEFAULT_TIME_ZONE = 3
-DEFAULT_TIME_DISPLAY = r"%ND(Y-m-d,) H:i" #According to ISO 8601
-DEFAULT_MARKUP ="markdown"
+DEFAULT_TIME_DISPLAY = r"%ND(Y-m-d,) H:i"  # According to ISO 8601
+DEFAULT_MARKUP = 'markdown'
 SIGNATURE_MAX_LENGTH = 255
 SIGNATURE_MAX_LINES = 8
-AVATARS_UPLOAD_TO = "profile/avatars"
+AVATARS_UPLOAD_TO = 'profile/avatars'
 AVATAR_HEIGHT = AVATAR_WIDTH = 80
 
 ######################
 # Pybb Configuration #
 ######################
-PYBB_ATTACHMENT_ENABLE = False # disable gzip middleware when enabling attachments
+PYBB_ATTACHMENT_ENABLE = False  # disable gzip middleware when enabling attachments
 PYBB_DEFAULT_MARKUP = 'markdown'
 PYBB_FREEZE_FIRST_POST = False
 
@@ -131,67 +196,69 @@
 # Link classification and other Markup stuff #
 ##############################################
 LOCAL_DOMAINS = [
-    "xoops.widelands.org"
+    'xoops.widelands.org'
 ]
-SMILEY_DIR = MEDIA_URL + "img/smileys/"
+
+SMILEY_DIR = MEDIA_URL + 'img/smileys/'
 # Keep this list ordered by length of smileys
 SMILEYS = [
-    ("O:-)", "face-angel.png"),
-    ("O:)", "face-angel.png"),
-    (":-/", "face-confused.png"),
-    (":/", "face-confused.png"),
-    ("B-)", "face-cool.png"),
-    ("B)", "face-cool.png"),
-    (":'-(", "face-crying.png"),
-    (":'(", "face-crying.png"),
-    (":-))", "face-smile-big.png"),
-    (":))", "face-smile-big.png"),
-    (":-)", "face-smile.png"),
-    (":)", "face-smile.png"),
-    ("&gt;:-)", "face-devilish.png"), # Hack around markdown replacement. see also SMILEY_PREESCAPING
-    ("8-)", "face-glasses.png"),
-    ("8)", "face-glasses.png"),
-    (":-D", "face-grin.png"),
-    (":D", "face-grin.png"),
-    (":-x", "face-kiss.png"),
-    (":x", "face-kiss.png"),
-    (":-*", "face-kiss.png"),
-    (":*", "face-kiss.png"),
-    (":-((", "face-mad.png"),
-    (":((", "face-mad.png"),
-    (":-||", "face-mad.png"),
-    (":||", "face-mad.png"),
-    (":(|)", "face-monkey.png"),
-    (":-|", "face-plain.png"),
-    (":|", "face-plain.png"),
-    (":-(", "face-sad.png"),
-    (":(", "face-sad.png"),
-    (":-O", "face-shock.png"),
-    (":O", "face-shock.png"),
-    (":-o", "face-surprise.png"),
-    (":o", "face-surprise.png"),
-    (":-P", "face-tongue.png"),
-    (":P", "face-tongue.png"),
-    (":-S", "face-upset.png"),
-    (":S", "face-upset.png"),
-    (";-)", "face-wink.png"),
-    (";)", "face-wink.png"),
+    ('O:-)', 'face-angel.png'),
+    ('O:)', 'face-angel.png'),
+    (':-/', 'face-confused.png'),
+    (':/', 'face-confused.png'),
+    ('B-)', 'face-cool.png'),
+    ('B)', 'face-cool.png'),
+    (":'-(", 'face-crying.png'),
+    (":'(", 'face-crying.png'),
+    (':-))', 'face-smile-big.png'),
+    (':))', 'face-smile-big.png'),
+    (':-)', 'face-smile.png'),
+    (':)', 'face-smile.png'),
+    # Hack around markdown replacement. see also SMILEY_PREESCAPING
+    ('&gt;:-)', 'face-devilish.png'),
+    ('8-)', 'face-glasses.png'),
+    ('8)', 'face-glasses.png'),
+    (':-D', 'face-grin.png'),
+    (':D', 'face-grin.png'),
+    (':-x', 'face-kiss.png'),
+    (':x', 'face-kiss.png'),
+    (':-*', 'face-kiss.png'),
+    (':*', 'face-kiss.png'),
+    (':-((', 'face-mad.png'),
+    (':((', 'face-mad.png'),
+    (':-||', 'face-mad.png'),
+    (':||', 'face-mad.png'),
+    (':(|)', 'face-monkey.png'),
+    (':-|', 'face-plain.png'),
+    (':|', 'face-plain.png'),
+    (':-(', 'face-sad.png'),
+    (':(', 'face-sad.png'),
+    (':-O', 'face-shock.png'),
+    (':O', 'face-shock.png'),
+    (':-o', 'face-surprise.png'),
+    (':o', 'face-surprise.png'),
+    (':-P', 'face-tongue.png'),
+    (':P', 'face-tongue.png'),
+    (':-S', 'face-upset.png'),
+    (':S', 'face-upset.png'),
+    (';-)', 'face-wink.png'),
+    (';)', 'face-wink.png'),
 ]
 # This needs to be done to keep some stuff hidden from markdown
 SMILEY_PREESCAPING = [
-    (">:-)", "\>:-)")
+    ('>:-)', '\>:-)')
 ]
 
 ###############################
 # Sphinx (Search prog) Config #
 ###############################
-USE_SPHINX=False
+USE_SPHINX = True
 SPHINX_API_VERSION = 0x116
 
 ############
 # Tracking #
 ############
-TRACKING_CLEANUP_TIMEOUT=48
+TRACKING_CLEANUP_TIMEOUT = 48
 
 ###########################
 # Widelands SVN directory #
@@ -199,86 +266,66 @@
 # This is needed for various thinks, for example
 # to access media (for minimap creation) or for online help
 # or for ChangeLog displays
-WIDELANDS_SVN_DIR=""
+WIDELANDS_SVN_DIR = ''
 
 #####################
 # ChangeLog display #
 #####################
-BZR_URL = r"http://bazaar.launchpad.net/%%7Ewidelands-dev/widelands/trunk/revision/%s";
+# NOCOMM franku: This is only used in mainpage/wl_markdown/templatetags/wl_markdon.py/_insert_revision()
+# Since there is a plan to have only some prosa in the Changelog, both (this setting and the function)
+# could be removed. It didn't worked either...
+BZR_URL = r'http://bazaar.launchpad.net/~widelands-dev/widelands/trunk/revision/%s'
 
 ###############
 # Screenshots #
 ###############
-THUMBNAIL_SIZE = ( 160, 160 )
+THUMBNAIL_SIZE = (160, 160)
 
 ########
 # Maps #
 ########
 MAPS_PER_PAGE = 10
 
-INSTALLED_APPS = (
-    'django.contrib.auth',
-    'django.contrib.contenttypes',
-    'django.contrib.sessions',
-    'django.contrib.sites',
-    'django.contrib.admin',
-    'django.contrib.markup',
-    'django.contrib.humanize',
-
-    # TODO: only temporary for webdesign stuff
-    'django.contrib.webdesign',
-
-    # Thirdparty apps, but need preload
-    'tracking',
-
-    # Our own apps
-    'widelands.mainpage',
-    'widelands.wlhelp',
-    'widelands.wlimages',
-    'widelands.wlwebchat',
-    'widelands.wlrecaptcha',
-    'widelands.wlprofile',
-    'widelands.wlsearch',
-    'widelands.wlpoll',
-    'widelands.wlevents',
-    'widelands.wlmaps',
-    'widelands.wlscreens',
-    'widelands.wlggz',
-
-    # Modified 3rd party apps
-    'widelands.wiki', # This is based on wikiapp, but has some local modifications
-    'widelands.news', # This is based on simple-blog, but has some local modifications
-    'pybb', # Feature enriched version of pybb
-
-    # Thirdparty apps
-    'threadedcomments',
-    'django_messages',
-    'registration', # User registration (per Email validation)
-    'pagination',
-    'tagging',
-    'notification',
-    'djangoratings',
-    'sphinxdoc',
-    'south',
-)
-
-USE_GOOGLE_ANALYTICS=False
+
+USE_GOOGLE_ANALYTICS = False
 
 ##############################################
 ## Recipient(s) who get an email if someone ##
-##       uses the on legal notice page      ##
+## uses the form on legal notice page       ##
 ## Use allways the form ('name', 'Email')   ##
 ##############################################
-INQUIRY_RECIPIENTS = (
-   ('franku','somal@xxxxxxxx'),
-)
+INQUIRY_RECIPIENTS = [
+    ('peter', 'peter@xxxxxxxxxxx'),
+]
+
+##########################################
+## Allowed tags/attributes for 'bleach' ##
+## Used for sanitizing user input.      ##
+##########################################
+BLEACH_ALLOWED_TAGS = [u'a',
+                       u'abbr',
+                       u'acronym',
+                       u'blockquote',
+                       u'br',
+                       u'em',  u'i',  u'strong', u'b',
+                       u'ul',  u'ol', u'li',
+                       u'div', u'p',
+                       u'h1',  u'h2', u'h3', u'h4', u'h5', u'h6',
+                       u'pre', u'code',
+                       u'img',
+                       u'hr',
+                       u'table', u'tbody', u'thead', u'th', u'tr', u'td',
+		       u'sup',
+]
+
+BLEACH_ALLOWED_ATTRIBUTES = {'img': ['src', 'alt'], 'a': ['href'], '*': ['class', 'id', 'title']}
 
 try:
-    from local_settings import *
+   from local_settings import *
 except ImportError:
-    pass
+   pass
 
 if USE_SPHINX:
-    INSTALLED_APPS += (
-        'djangosphinx',
-    )
+   INSTALLED_APPS += (
+       'djangosphinx',
+   )

=== added directory 'sphinxdoc'
=== added file 'sphinxdoc/__init__.py'
=== added file 'sphinxdoc/admin.py'
--- sphinxdoc/admin.py	1970-01-01 00:00:00 +0000
+++ sphinxdoc/admin.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,16 @@
+# encoding: utf-8
+'''
+Admin interface for the sphinxdoc app.
+'''
+
+from django.contrib import admin
+
+from sphinxdoc.models import App
+
+
+class AppAdmin(admin.ModelAdmin):
+    list_display = ('name', 'path',)
+    prepopulated_fields = {'slug': ('name',)}
+    
+
+admin.site.register(App, AppAdmin)

=== added directory 'sphinxdoc/migrations'
=== added file 'sphinxdoc/migrations/0001_initial.py'
--- sphinxdoc/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ sphinxdoc/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='App',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=100)),
+                ('slug', models.SlugField(help_text='Used in the URL for the app. Must be unique.', unique=True)),
+                ('path', models.CharField(max_length=255)),
+            ],
+        ),
+    ]

=== added file 'sphinxdoc/migrations/__init__.py'
=== added file 'sphinxdoc/models.py'
--- sphinxdoc/models.py	1970-01-01 00:00:00 +0000
+++ sphinxdoc/models.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,23 @@
+# encoding: utf-8
+"""
+Models for django-sphinxdoc.
+"""
+
+from django.db import models
+
+
+class App(models.Model):
+    name = models.CharField(max_length=100)
+    slug = models.SlugField(unique=True,
+            help_text=u'Used in the URL for the app. Must be unique.')
+    path = models.CharField(max_length=255)
+    
+    def __unicode__(self):
+        return self.name
+
+    @models.permalink
+    def get_absolute_url(self):
+        return ('doc-index', (), {'slug': self.slug})
+    
+    class Meta:
+        app_label = 'sphinxdoc'
\ No newline at end of file

=== added directory 'sphinxdoc/templates'
=== added directory 'sphinxdoc/templates/sphinxdoc'
=== added file 'sphinxdoc/templates/sphinxdoc/app_list.html'
--- sphinxdoc/templates/sphinxdoc/app_list.html	1970-01-01 00:00:00 +0000
+++ sphinxdoc/templates/sphinxdoc/app_list.html	2016-06-28 17:58:37 +0000
@@ -0,0 +1,14 @@
+{% extends 'base.html' %}
+
+{% block title %}{{ block.super }} » Documentation Overview{% endblock %}
+
+{% block content %}
+<div>
+    <h2 class="pagetitle">Documentation Overview</h2>
+    <ul>
+    {% for app in app_list %}
+        <li><a href="{{ app.get_absolute_url }}">{{ app.name }}</a></li>
+    {% endfor %}
+    </ul>
+</div>
+{% endblock content %}

=== added file 'sphinxdoc/templates/sphinxdoc/documentation.html'
--- sphinxdoc/templates/sphinxdoc/documentation.html	1970-01-01 00:00:00 +0000
+++ sphinxdoc/templates/sphinxdoc/documentation.html	2016-06-28 17:58:37 +0000
@@ -0,0 +1,65 @@
+{% extends 'base.html' %}
+
+{% block title %}{{ block.super }} » {{ app.name }}{% for p in doc.parents %} » {{ p.title|striptags|safe }}{% endfor %} » {{ doc.title|striptags|safe }}{% endblock %}
+
+{% block content %}
+<div class="pagination-top">
+    » <a href="{{ app.get_absolute_url }}">{{ app.name }}</a>
+    {% for p in doc.parents %}
+    » <a href="{{ p.link }}">{{ p.title|safe }}</a>
+    {% endfor %}
+    » {{ doc.title|safe }}
+    {% if doc.prev or doc.next %}
+    <br /><br />
+    <span class="left">
+        {% if doc.prev %}
+          Prev: <a href="{{ doc.prev.link }}">{{ doc.prev.title|safe }}</a>
+        {% endif %}</span><span class="right">
+        {% if doc.next %}
+          Next: <a href="{{ doc.next.link }}">{{ doc.next.title|safe }}</a>
+        {% endif %}</span>
+    {% endif %}
+</div>
+
+<div class="sphinx">
+    {% block doc_body %}
+    {{ doc.body|safe }}    
+    {% endblock %}
+</div>
+
+<div class="pagination-bottom">
+    {% if doc.prev or doc.next %}
+    <span class="left">
+        {% if doc.prev %}
+          Prev: <a href="{{ doc.prev.link }}">{{ doc.prev.title|safe }}</a>
+        {% endif %}</span><span class="right">
+        {% if doc.next %}
+          Next: <a href="{{ doc.next.link }}">{{ doc.next.title|safe }}</a>
+        {% endif %}</span>
+    <br /><br />
+    {% endif %}
+    » <a href="{{ app.get_absolute_url }}">{{ app.name }} documentation</a>
+    {% for p in doc.parents %}
+    » <a href="{{ p.link }}">{{ p.title|safe }}</a>
+    {% endfor %}
+    » {{ doc.title|safe }}
+    <br /><br />
+    Last update: {{ update_date|date:"Y-m-d H:i" }} (<a href="http://www.timeanddate.com/worldclock/city.html?n=37";>CET</a>)
+</div>
+{% endblock content %}
+
+{% block sidebar %}
+    {% block doc_toc %}
+<div class="box">
+    <h2>Contents</h2>
+    {{ doc.toc|safe }}
+</div>
+    {% endblock %}
+<div class="box">
+    <h2>Search</h2>
+    <em>Not yet implemented</em>
+    {# {% load docs %} #}
+    {# {% search_form %} #}
+</div>
+    {{ block.super }}
+{% endblock sidebar %}

=== added file 'sphinxdoc/templates/sphinxdoc/genindex.html'
--- sphinxdoc/templates/sphinxdoc/genindex.html	1970-01-01 00:00:00 +0000
+++ sphinxdoc/templates/sphinxdoc/genindex.html	2016-06-28 17:58:37 +0000
@@ -0,0 +1,38 @@
+{% extends 'sphinxdoc/documentation.html' %}
+
+{% block doc_body %}
+    <h1>General Index</h1>
+    <p class="indexletters">
+    {% for letter, _ in doc.genindexentries %}
+        <a href="#{{ letter }}">{{ letter }}</a> {% if not forloop.last %} •{% endif %}
+    {% endfor %}
+    </p>
+
+    {% for letter, entries in doc.genindexentries %}
+    <br />    
+    <h2 id="{{ letter }}">{{ letter }}</h2>
+    <dl class="index">
+        {% for name, contents in entries %}
+        <dt>
+          {# contents.0 is a list of links for the item #}
+            {% if contents.0 %}
+            <a href="{{ contents.0.0 }}">{{ name }}</a>
+            {% else %}
+            {{ name }}
+            {% endif %}
+        </dt>
+        {# contents.1 is a list of subitems #}
+            {% if contents.1 %}
+                {% for subname, sublinks in contents.1 %}
+        <dd>
+            <a href="{{ sublinks.0 }}">{{ subname }}</a>
+                    {% for link in sublinks|slice:"1:" %}, <a href="{{ link }}">[Link]</a>{% endfor %}
+        </dd>
+                {% endfor %}
+            {% endif %}
+        {% endfor %}
+    </dl>
+    {% endfor %}
+    <br />
+{% endblock doc_body %}
+{% block doc_toc %}{% endblock %}

=== added file 'sphinxdoc/templates/sphinxdoc/modindex.html'
--- sphinxdoc/templates/sphinxdoc/modindex.html	1970-01-01 00:00:00 +0000
+++ sphinxdoc/templates/sphinxdoc/modindex.html	2016-06-28 17:58:37 +0000
@@ -0,0 +1,13 @@
+{% extends 'sphinxdoc/documentation.html' %}
+
+{% block doc_body %}
+    <h1>Module Index</h1>
+    <dl>
+    {% for modname, collapse, cgroup, indent, fname, synops, pform, dep in doc.modindexentries %}
+        <dt><a href="{{ fname }}"><tt class="literal xref">{{ modname }}</tt></a></dt>
+        <dd>{{ synops }}</dd>
+    {% endfor %}
+    </dl>
+    <br />
+{% endblock doc_body %}
+{% block doc_toc %}{% endblock %}

=== added file 'sphinxdoc/templates/sphinxdoc/search_form.html'
--- sphinxdoc/templates/sphinxdoc/search_form.html	1970-01-01 00:00:00 +0000
+++ sphinxdoc/templates/sphinxdoc/search_form.html	2016-06-28 17:58:37 +0000
@@ -0,0 +1,12 @@
+<form action="{{ action|escape }}" id="{{ search_form_id|escape }}" class="search">
+    <div>
+        <input type="hidden" name="cx" value="009763561546736975936:e88ek0eurf4" />
+        <input type="hidden" name="cof" value="FORID:11" />
+        <input type="hidden" name="ie" value="UTF-8" />
+        <input type="hidden" name="hl" value="{{ lang|escape }}" />
+        {{ form.q }}
+        <input type="submit" name="sa" class="submit" value="Search" />
+        {{ form.as_q }}
+    </div>
+</form>
+<script type="text/javascript" src="http://www.google.com/coop/cse/brand?form={{ search_form_id|escape }}&amp;lang={{ lang|escape }}"></script>
\ No newline at end of file

=== added file 'sphinxdoc/urls.py'
--- sphinxdoc/urls.py	1970-01-01 00:00:00 +0000
+++ sphinxdoc/urls.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,49 @@
+# encoding: utf-8
+
+from django.conf.urls import *
+from django.views.generic.list import ListView
+
+from sphinxdoc import models
+
+app_info = {
+    'queryset': models.App.objects.all().order_by('name'),
+    'template_object_name': 'app',
+}
+
+
+urlpatterns = patterns('sphinxdoc.views',
+    url(
+        r'^$',
+        ListView.as_view(),
+        app_info,
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/search/$',
+        'search',
+        name='doc-search',
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/_images/(?P<path>.*)$',
+        'images',
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/_source/(?P<path>.*)$',
+        'source',
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/_objects/$',
+        'objects_inventory',
+        name='objects-inv',
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/$',
+        'documentation',
+        {'url': ''},
+        name='doc-index',
+    ),
+    url(
+        r'^(?P<slug>[\w-]+)/(?P<url>(([\w-]+)/)+)$',
+        'documentation',
+        name='doc-detail',
+    ),
+)

=== added file 'sphinxdoc/views.py'
--- sphinxdoc/views.py	1970-01-01 00:00:00 +0000
+++ sphinxdoc/views.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,87 @@
+# encoding: utf-8
+
+import datetime
+import os.path
+
+from django.http import Http404
+from django.shortcuts import get_object_or_404, render_to_response
+from django.template import RequestContext
+#from django.utils import simplejson as json
+import json
+from django.views import static
+
+from sphinxdoc.models import App
+
+
+SPECIAL_TITLES = {
+    'genindex': 'General Index',
+    'modindex': 'Module Index',
+    'search': 'Search',
+}
+
+
+def documentation(request, slug, url):
+    app = get_object_or_404(App, slug=slug)
+    url = url.strip('/')
+    page_name = os.path.basename(url)
+    
+    path = os.path.join(app.path, url, 'index.fjson')
+    if not os.path.exists(path):
+        path = os.path.dirname(path) + '.fjson'
+        if not os.path.exists(path):
+            raise Http404('"%s" does not exist' % path)
+
+    templates = (
+        'sphinxdoc/%s.html' % page_name,
+        'sphinxdoc/documentation.html',
+    )
+    
+    data = {
+        'app': app,
+        'doc': json.load(open(path, 'rb')),
+        'env': json.load(open(
+                os.path.join(app.path, 'globalcontext.json'), 'rb')),
+        'version': app.name,
+        'docurl': url,
+        'update_date':  datetime.datetime.fromtimestamp(
+                os.path.getmtime(os.path.join(app.path, 'last_build'))),
+        'home': app.get_absolute_url(),
+        # 'search': urlresolvers.reverse('document-search', kwargs={'lang':lang, 'version':version}),
+        'redirect_from': request.GET.get('from', None),
+    
+    }
+    if 'title' not in data['doc']:
+        data['doc']['title'] = SPECIAL_TITLES[page_name]
+        
+    return render_to_response(templates, data,
+            context_instance=RequestContext(request))
+
+def search(request, slug):
+    from django.http import HttpResponse
+    return HttpResponse('Not yet implemented.')
+    
+def objects_inventory(request, slug):
+    app = get_object_or_404(App, slug=slug)
+    response = static.serve(
+        request, 
+        document_root = app.path,
+        path = "objects.inv",
+    )
+    response['Content-Type'] = "text/plain"
+    return response
+
+def images(request, slug, path):
+    app = get_object_or_404(App, slug=slug)
+    return static.serve(
+        request, 
+        document_root = os.path.join(app.path, '_images'),
+        path = path,
+    )
+    
+def source(request, slug, path):
+    app = get_object_or_404(App, slug=slug)
+    return static.serve(
+        request,
+        document_root = os.path.join(app.path, '_sources'),
+        path = path,
+    )

=== modified file 'templates/base.html'
--- templates/base.html	2015-02-18 22:30:08 +0000
+++ templates/base.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,4 @@
+
 <!DOCTYPE html>
 {% comment %}
  vim:ft=htmldjango:
@@ -52,7 +53,7 @@
 				<div class="loginBox posRight">
 					{% include "login_box.html" %}
 				</div>
-				<a href="{% url mainpage %}"><img src="{{ MEDIA_URL }}img/Logo.png" class="posLeft" alt="Widelands Logo" /></a>
+				<a href="{% url 'mainpage' %}"><img src="{{ MEDIA_URL }}img/{{ LOGO_FILE }}" class="posLeft" alt="Widelands Logo" /></a>
 			</div>
 			<div id="topmenu">
 				<!-- Navigation -->

=== modified file 'templates/django_messages/base.html'
--- templates/django_messages/base.html	2015-02-18 22:30:08 +0000
+++ templates/django_messages/base.html	2016-06-28 17:58:37 +0000
@@ -15,10 +15,10 @@
 	<tr>
 	<th class="msg_menu">
 		<ul>
-			<li><a href="{% url messages_compose %}">{% trans "New Message" %}</a>
-			<li><a href="{% url messages_inbox %}">{% trans "Inbox" %}</a>
-			<li><a href="{% url messages_outbox %}">{% trans "Outbox" %}</a>
-			<li><a href="{% url messages_trash %}">{% trans "Trash" %}</a>
+			<li><a href="{% url 'messages_compose' %}">{% trans "New Message" %}</a>
+			<li><a href="{% url 'messages_inbox' %}">{% trans "Inbox" %}</a>
+			<li><a href="{% url 'messages_outbox' %}">{% trans "Outbox" %}</a>
+			<li><a href="{% url 'messages_trash' %}">{% trans "Trash" %}</a>
 		</ul>
 	</th>
 	<td class="msg_box">

=== modified file 'templates/django_messages/compose.html'
--- templates/django_messages/compose.html	2012-05-06 20:52:08 +0000
+++ templates/django_messages/compose.html	2016-06-28 17:58:37 +0000
@@ -12,7 +12,7 @@
 	<table class="messages">
 		{% for field in form %}
 		<tr>
-			<td class="grey">{{ field.label_tag }}:</td>
+			<td class="grey">{{ field.label_tag }}</td>
 			<td>{{ field }}</td>
 			<td class="errormessage">{{ field.errors }}</td>
 		</tr>

=== modified file 'templates/django_messages/inbox.html'
--- templates/django_messages/inbox.html	2015-02-18 22:30:08 +0000
+++ templates/django_messages/inbox.html	2016-06-28 17:58:37 +0000
@@ -1,7 +1,7 @@
 {% extends "django_messages/base.html" %} 
 {% load i18n %} 
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% block title %}
 Inbox - {{ block.super }}
@@ -31,7 +31,7 @@
 				</td>
 				<td>{{ message.sent_at|custom_date:user }}</td>
 				<td>
-					<a href="{% url django_messages.views.delete message.id %}">
+					<a href="{% url 'django_messages.views.delete' message.id %}">
 						<img src="{{ MEDIA_URL }}img/delete.png" alt="delete" title="delete" />
 					</a>
 				</td>

=== modified file 'templates/django_messages/inlines/message_row.html'
--- templates/django_messages/inlines/message_row.html	2010-03-14 14:13:24 +0000
+++ templates/django_messages/inlines/message_row.html	2016-06-28 17:58:37 +0000
@@ -18,5 +18,5 @@
           {% if message.new %}</u>{% endif %}
       </td>
       <td>{{ message.sent_at|custom_date:user }}</td>
-      <td><a href="{% url django_messages.views.delete message.id %}">{% trans "delete" %}</a></td>
+      <td><a href="{% url 'django_messages.views.delete' message.id %}">{% trans "delete" %}</a></td>
    </tr>

=== modified file 'templates/django_messages/inlines/navigation.html'
--- templates/django_messages/inlines/navigation.html	2010-10-31 10:25:03 +0000
+++ templates/django_messages/inlines/navigation.html	2016-06-28 17:58:37 +0000
@@ -7,10 +7,10 @@
             <td width="">&nbsp;
             </td>
             <td width="370" align="right" style="table-layout: fixed; background-image: url(/wlmedia/img/background-4F4F4F.png);">
-                <a href="{% url messages_inbox %} ">{% trans "Inbox" %}</a>
-                | <a href="{% url messages_outbox %} ">{% trans "Sent Messages" %}</a>
-                | <a href="{% url messages_compose %} ">{% trans "New Message" %}</a>
-                | <a href="{% url messages_trash %} ">{% trans "Message Trash" %}</a>
+                <a href="{% url 'messages_inbox' %} ">{% trans "Inbox" %}</a>
+                | <a href="{% url 'messages_outbox' %} ">{% trans "Sent Messages" %}</a>
+                | <a href="{% url 'messages_compose' %} ">{% trans "New Message" %}</a>
+                | <a href="{% url 'messages_trash' %} ">{% trans "Message Trash" %}</a>
             </td>
         </tr>
     </table>

=== modified file 'templates/django_messages/new_message.html'
--- templates/django_messages/new_message.html	2009-02-26 11:32:18 +0000
+++ templates/django_messages/new_message.html	2016-06-28 17:58:37 +0000
@@ -7,5 +7,5 @@
 
 --
 {% blocktrans %}Sent from {{ site_url }}{% endblocktrans %}
-{% trans "Inbox" %}: {{ site_url }}{% url messages_inbox %}
-{% trans "Reply" %}: {{ site_url }}{% url messages_reply message.pk %}     
\ No newline at end of file
+{% trans "Inbox" %}: {{ site_url }}{% url 'messages_inbox' %}
+{% trans "Reply" %}: {{ site_url }}{% url 'messages_reply' message.pk %}     
\ No newline at end of file

=== modified file 'templates/django_messages/outbox.html'
--- templates/django_messages/outbox.html	2015-02-18 22:30:08 +0000
+++ templates/django_messages/outbox.html	2016-06-28 17:58:37 +0000
@@ -1,7 +1,7 @@
 {% extends "django_messages/base.html" %} 
 {% load i18n %} 
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% block title %}
 Outbox - {{ block.super }}
@@ -27,7 +27,7 @@
 				</td>
 				<td>{{ message.sent_at|custom_date:user }}</td>
 				<td>
-					<a href="{% url django_messages.views.delete message.id %}">
+					<a href="{% url 'django_messages.views.delete' message.id %}">
 						<img src="{{ MEDIA_URL }}img/delete.png" alt="delete" title="delete" />
 					</a>
 				</td>

=== modified file 'templates/django_messages/trash.html'
--- templates/django_messages/trash.html	2015-02-18 22:30:08 +0000
+++ templates/django_messages/trash.html	2016-06-28 17:58:37 +0000
@@ -1,7 +1,7 @@
 {% extends "django_messages/base.html" %} 
 {% load i18n %} 
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% block title %}
 Trash - {{ block.super }}
@@ -33,7 +33,7 @@
 				</td>
 				<td>{{ message.sent_at|custom_date:user }}</td>
 				<td>
-					<a href="{% url django_messages.views.undelete message.id %}">
+					<a href="{% url 'django_messages.views.undelete' message.id %}">
 						<img src="{{ MEDIA_URL }}img/undelete.png" alt="undelete" title="undelete" />
 					</a>
 				</td>

=== modified file 'templates/django_messages/view.html'
--- templates/django_messages/view.html	2012-05-06 20:52:08 +0000
+++ templates/django_messages/view.html	2016-06-28 17:58:37 +0000
@@ -1,7 +1,7 @@
 {% extends "django_messages/base.html" %} 
 {% load i18n %} 
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% block title %}
 View - {{ block.super }}
@@ -31,7 +31,7 @@
 		</tr>
 	</table>
 	{% ifequal message.recipient user %}
-		<button type="button" onclick="location.href='{% url messages_reply message.id %}';">{% trans "Reply" %}</button>
+		<button type="button" onclick="location.href='{% url 'messages_reply' message.id %}';">{% trans "Reply" %}</button>
 	{% endifequal %}
-	<button type="button" onclick="location.href='{% url messages_delete message.id %}';">{% trans "Delete" %}</button>
+	<button type="button" onclick="location.href='{% url 'messages_delete' message.id %}';">{% trans "Delete" %}</button>
 {% endblock %}

=== modified file 'templates/footer.html'
--- templates/footer.html	2016-01-12 08:05:17 +0000
+++ templates/footer.html	2016-06-28 17:58:37 +0000
@@ -6,7 +6,9 @@
  (which contains nothing at the moment)
 {% endcomment %}
 
+
+
 <div id="footer">
 	Copyright &copy; 2009 - 2016 By the Widelands Development Team</br>
-	<a class="small" href="{% url legal_notice %}">Legal notice (contact)</a>
+	<a class="small" href="{% url 'legal_notice' %}">Legal notice (contact)</a>
 </div>

=== modified file 'templates/login_box.html'
--- templates/login_box.html	2013-06-16 14:26:58 +0000
+++ templates/login_box.html	2016-06-28 17:58:37 +0000
@@ -1,17 +1,20 @@
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 <!-- Login form / User information -->
 {% if user.is_authenticated %}
 <div class="small posLeft">
 	Welcome {{ user|user_link }},<br/>
-   you have <a href="{% url messages_inbox %}">{{ messages_inbox_count}} new message{{ messages_inbox_count|pluralize }}</a>.
+    {% comment %}
+    you have <a href="{% url 'messages_inbox' request.user %}">{{ messages_inbox_count}} new message{{ messages_inbox_count|pluralize }}</a>.
+    {% endcomment %}
+    you have <a href="{% url 'messages_inbox' %}">{{ messages_inbox_count}} new message{{ messages_inbox_count|pluralize }}</a>.
 </div>
 <div class="right small posRight">
 	<ul>
-		<li><a href="{% url messages_inbox %}">Messages</a></li>
-		<li><a href="{% url notification_notices %}">Notifications</a></li>
-		<li><a href="{% url profile_edit %}">Edit Profile</a></li>
-		<li><a href="{% url auth_logout %}?next={{ request.path|iriencode }}">Logout</a></li>
+		<li><a href="{% url 'messages_inbox' %}">Messages</a></li>
+		<li><a href="{% url 'notification_notices' %}">Notifications</a></li>
+		<li><a href="{% url 'profile_edit' %}">Edit Profile</a></li>
+		<li><a href="{% url 'auth_logout' %}?next={{ request.path|iriencode }}">Logout</a></li>
 	</ul>
 </div>
 {% else %}
@@ -27,8 +30,8 @@
 </form>
 {% endcomment %}
 <div class="small center">
-	<a href="{% url auth_login %}?next={{ request.path|iriencode }}">Click here to login</a><br />
-	<a href="{% url auth_password_reset %}">Lost password?</a> | <a href="{% url registration_register %}">Register now!</a>
+	<a href="{% url 'auth_login' %}?next={{ request.path|iriencode }}">Click here to login</a><br />
+	<a href="{% url 'auth_password_reset' %}">Lost password?</a> | <a href="{% url 'registration_register' %}">Register now!</a>
 </div>
 {% endif %}
 {% comment %}

=== modified file 'templates/mainpage.html'
--- templates/mainpage.html	2015-02-18 22:30:08 +0000
+++ templates/mainpage.html	2016-06-28 17:58:37 +0000
@@ -8,7 +8,7 @@
 
 {% endcomment %}
 
-{% load news %}
+{% load news_extras %}
 {% block extra_head %}
 <meta name="google-site-verification" content="1A5uFV_zNuXazJ46-572-_lLzcCTEQ77iHaSPFZd53Y" />
 <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/news.css" />
@@ -20,22 +20,22 @@
 <div class="blogEntry" style="min-height: 380px;">
 	<img class="landing posRight" src="{{ MEDIA_URL }}img/welcome.jpg" alt="Welcome!" />
 	<p>
-	<a href="{% url wiki_article "Description" %}">Widelands</a> is a 
-	<a href="{% url wiki_article "The Widelands Project" %}">free, open source</a>
+	<a href="{% url 'wiki_article' "Description" %}">Widelands</a> is a 
+	<a href="{% url 'wiki_article' "The Widelands Project" %}">free, open source</a>
 	real-time strategy game with singleplayer campaigns and a multiplayer mode.
 	The game was inspired by Settlers&nbsp;II&#8482; (&copy;&nbsp;Bluebyte) but
 	has significantly more variety and depth to it. Still, it is easy to get
 	started through playable tutorials.
 	</p>
 	<p>
-	For more information read <a href="{% url wiki_article "Description" %}">the
-	full description</a> and look at some <a href="{% url wlscreens_index %}">screenshots</a>.
-	Or you can <a href="{% url wiki_article "Download" %}">download</a>
+	For more information read <a href="{% url 'wiki_article' "Description" %}">the
+	full description</a> and look at some <a href="{% url 'wlscreens_index' %}">screenshots</a>.
+	Or you can <a href="{% url 'wiki_article' "Download" %}">download</a>
 	the latest release and just try it out for yourself.
 	</p>
 	<p>
 	This website is the home of the Widelands community.
-	You are invited to visit the <a href="{% url pybb_index %}">forums</a>:
+	You are invited to visit the <a href="{% url 'pybb_index' %}">forums</a>:
 	discuss strategies, find partners for multiplayer games, help with translations,
 	voice your opinion on graphics, music and much more.
 	</p>
@@ -45,20 +45,19 @@
 	Everybody is invited to help out too - we need 2D and 3D artists, sound
 	effect creators, composers, map makers, translators, test players,
 	web programmers and C++ coders. All skill levels are welcome - just
-	start working on something or ask in the <a href="{% url pybb_index %}">forums</a>
+	start working on something or ask in the <a href="{% url 'pybb_index' %}">forums</a>
 	for pointers.
 	</p>
 	<div style="clear: left"></div>
 </div>
 
-
 {% get_latest_posts 3 as latest_posts_list %}
 {% if latest_posts_list %}
 	<h1>News</h1>
 	{% for object in latest_posts_list %}
 		{% include "news/inlines/post_detail.html" %}
 	{% endfor %}
-	<div class="center"><p><a class="invertedColor" href="{% url news_index %}">News archive</a></p></div>
+	<div class="center"><p><a class="invertedColor" href="{% url 'news_index' %}">News archive</a></p></div>
 {% endif %}
 
 {% endblock %}

=== modified file 'templates/mainpage/legal_notice.html'
--- templates/mainpage/legal_notice.html	2016-01-12 08:05:17 +0000
+++ templates/mainpage/legal_notice.html	2016-06-28 17:58:37 +0000
@@ -20,8 +20,13 @@
 
 	<ul>
 	<li>E-Mail Holger Rapp: sirver(at)gmx.de</li>
-	<li>Contact form:
-
+	<li>Contact form. Using this form sends E-Mails to following person(s):
+    <ul>
+        {% for name, recipient in inquiry_recipients %}
+        <li>{{ name }}: {{ recipient }} </li>
+        {% endfor %}
+    </ul>
+        
 	<form action="/legal_notice/" method="post">{% csrf_token %}
 	{% if form.errors %}
 	<p class="errormessage" style="text-align: left;">Please fill out all fields!</p>

=== modified file 'templates/mainpage/online_users.html'
--- templates/mainpage/online_users.html	2012-03-30 23:26:23 +0000
+++ templates/mainpage/online_users.html	2016-06-28 17:58:37 +0000
@@ -1,4 +1,4 @@
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% if users %}
 <div class="columnModule">
@@ -7,7 +7,7 @@
 		{% if users %}
 		<ul class="player">
 			{% for user in users %}
-			<li><a href="{% url profile_view user %}">{{user.username}}</a></li>
+			<li><a href="{% url 'profile_view' user %}">{{user.username}}</a></li>
 			{% endfor %}
 		</ul>
 		{% else %}

=== modified file 'templates/navigation.html'
--- templates/navigation.html	2016-06-09 19:37:08 +0000
+++ templates/navigation.html	2016-06-28 17:58:37 +0000
@@ -2,6 +2,8 @@
    vim:ft=htmldjango
 {% endcomment %}
 
+
+
 <script type="text/javascript">
 	/* Enable dropdown menus on touch devices */
 	$(document).ready(function(){
@@ -14,6 +16,7 @@
 <ul class="menu posLeft">
 	<li><a href="/">Home</a>
 		<ul>
+<<<<<<< TREE
 			<li><a href="{% url news_index %}">News Archive</a></li>
 			<li><a href="{% url wlpoll_archive %}">Poll Archive</a></li>
 		</ul>
@@ -36,31 +39,55 @@
 			<li><a href="{% url wiki_article "Game Manual" %}">Game Manual</a></li>
 			<li><a href="{% url wiki_article "Creating Game Content" %}">Creating Game Content</a></li>
 			<li><a href="{% url wiki_article "The Widelands Project" %}">The Widelands Project</a></li>
+=======
+			<li><a href="{% url 'news_index' %}">News Archive</a></li>
+			<li><a href="{% url 'wlpoll_archive' %}">Poll Archive</a></li>
+		</ul>
+	</li>
+	<li><a href="{% url 'wiki_article' "Description" %}">The Game</a>
+		<ul>
+			<li><a href="{% url 'wiki_article' "Description" %}">Description</a></li>
+			<li><a href="{% url 'wiki_article' "Download" %}">Download</a></li>
+			<li><a href="{% url 'wlscreens_index' %}">Screenshots</a></li>
+			<li><a href="{% url 'wiki_article' "Artwork" %}">Artwork</a></li>
+			<li><a href="{% url 'wlmaps_index' %}">Maps</a></li>
+			<li><a href="{% url 'wlhelp_index' %}">Encyclopedia</a></li>
+			<li><a href="{% url 'changelog' %}">Changelog</a></li>
+			<li><a href="{% url 'developers' %}">Widelands Development Team</a></li>
+			<li><a href="/wiki/LinksPage/">Links</a></li>
+		</ul>
+	</li>
+	<li><a href="{% url 'wiki_index' %}">Wiki</a>
+		<ul>
+			<li><a href="{% url 'wiki_article' "Game Manual" %}">Game Manual</a></li>
+			<li><a href="{% url 'wiki_article' "Creating Game Content" %}">Creating Game Content</a></li>
+			<li><a href="{% url 'wiki_article' "The Widelands Project" %}">The Widelands Project</a></li>
+>>>>>>> MERGE-SOURCE
 			<li><a href="/wiki/list/">List Of All Pages</a></li>
 			<li><a href="/wiki/history/">Recent changes</a></li>
 		</ul>
 	</li>
-	<li><a href="{% url pybb_index %}">Forums</a>
+	<li><a href="{% url 'pybb_index' %}">Forums</a>
 		<ul>
-			<li><a href="{% url pybb_forum 1 %}">Technical Help</a></li>
-			<li><a href="{% url pybb_forum 2 %}">Game Suggestions</a></li>
-			<li><a href="{% url pybb_forum 3 %}">Playing Widelands</a></li>
-			<li><a href="{% url pybb_forum 4 %}">Editor Forum</a></li>
-			<li><a href="{% url pybb_forum 5 %}">[Deutsch] - Spielerforum</a></li>
-			<li><a href="{% url pybb_forum 6 %}">[Español] - Foro de jugadores</a></li>
-			<li><a href="{% url pybb_forum 7 %}">[Français] - Forum de joueurs</a></li>
-			<li><a href="{% url pybb_forum 13 %}">[English] - Player Forum</a></li>
-			<li><a href="{% url pybb_forum 9 %}">Graphic Development</a></li>
-			<li><a href="{% url pybb_forum 10 %}">Sound &amp; Music Development</a></li>
-			<li><a href="{% url pybb_forum 11 %}">Homepage</a></li>
-			<li><a href="{% url pybb_forum 12 %}">Translations &amp; Internationalization</a></li>
+			<li><a href="{% url 'pybb_forum' 1 %}">Technical Help</a></li>
+			<li><a href="{% url 'pybb_forum' 2 %}">Game Suggestions</a></li>
+			<li><a href="{% url 'pybb_forum' 3 %}">Playing Widelands</a></li>
+			<li><a href="{% url 'pybb_forum' 4 %}">Editor Forum</a></li>
+			<li><a href="{% url 'pybb_forum' 5 %}">[Deutsch] - Spielerforum</a></li>
+			<li><a href="{% url 'pybb_forum' 6 %}">[Español] - Foro de jugadores</a></li>
+			<li><a href="{% url 'pybb_forum' 7 %}">[Français] - Forum de joueurs</a></li>
+			<li><a href="{% url 'pybb_forum' 13 %}">[English] - Player Forum</a></li>
+			<li><a href="{% url 'pybb_forum' 9 %}">Graphic Development</a></li>
+			<li><a href="{% url 'pybb_forum' 10 %}">Sound &amp; Music Development</a></li>
+			<li><a href="{% url 'pybb_forum' 11 %}">Homepage</a></li>
+			<li><a href="{% url 'pybb_forum' 12 %}">Translations &amp; Internationalization</a></li>
 		</ul>
 	</li>
-	<li><a href="{% url webchat_index %}">Chat</a></li>
-	<li><a href="{% url wiki_article "Development" %}">Development</a>
+	<li><a href="{% url 'webchat_index' %}">Chat</a></li>
+	<li><a href="{% url 'wiki_article' "Development" %}">Development</a>
 		<ul>
-			<li><a href="{% url wiki_article "Contribute" %}">Contribute</a></li>
-			<li><a href="{% url developers %}">Widelands Development Team</a></li>
+			<li><a href="{% url 'wiki_article' "Contribute" %}">Contribute</a></li>
+			<li><a href="{% url 'developers' %}">Widelands Development Team</a></li>
 			<li><a href="/docs/wl/" target="_blank">Documentation</a></li>
 			<li><a href="https://bugs.launchpad.net/widelands"; target="_blank">Widelands Bugtracker</a></li>
 			<li><a href="https://bugs.launchpad.net/widelands-website"; target="_blank">Website Bugtracker</a></li>

=== renamed directory 'templates/feeds' => 'templates/news/feeds'
=== modified file 'templates/news/feeds/posts_description.html'
--- templates/feeds/posts_description.html	2010-09-27 06:00:36 +0000
+++ templates/news/feeds/posts_description.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,3 @@
 {% load wl_markdown %}
 
-{{ obj.body|wl_markdown:"safe" }}
+{{ obj.body|wl_markdown }}

=== modified file 'templates/news/inlines/post_detail.html'
--- templates/news/inlines/post_detail.html	2012-04-02 09:41:12 +0000
+++ templates/news/inlines/post_detail.html	2016-06-28 17:58:37 +0000
@@ -5,21 +5,20 @@
 
 {% endcomment %}
 {% load threadedcommentstags %}
-{% load news wl_markdown tagging_tags wlprofile custom_date %}
+{% load news_extras wl_markdown tagging_tags wlprofile_extras custom_date %}
 
 <div class="blogEntry">
 	{% if object.has_image %}
 	<a href="{{ object.get_absolute_url }}"><img class="title posLeft" src='{{MEDIA_URL}}{{ object.image|urlencode }}' alt='{{ object.image_alt }}' /></a>
 	{% endif %}
 	{% if perms.news %}
-	<div class="small posRight">
-		{% if perms.news.post_can_add %}<a href="/admin/news/post/add/">Add New Post</a>{% endif %}
-		{% if perms.news.post_can_edit %}| <a href="/admin/news/post/{{object.id}}/">Edit</a>{% endif %}
-		{% if perms.news.post_can_delete %}| <a href="/admin/news/post/{{object.id}}/delete/">Delete</a>{% endif %}
+	<div class="small posRight invertedColor">
+		{% if perms.news.add_post %}<a href="/admin/news/post/add/">Add New News</a>{% endif %}
+		{% if perms.news.change_post %}| <a href="/admin/news/post/{{object.id}}/">Edit</a>{% endif %}
 	</div>
-	{% endif %}
+    {% endif %}
 	<h2><a href="{{ object.get_absolute_url }}" class="invertedColor">{{ object.title }}</a></h2>
-	{{ object.body|wl_markdown:"safe" }}
+	{{ object.body|wl_markdown }}
 	<hr />
 	{% get_comment_count for object as ccount %}
 	<span class="small posLeft"><a href="{{ object.get_absolute_url }}">{{ ccount }} comments</a></span>

=== modified file 'templates/news/post_archive_day.html'
--- templates/news/post_archive_day.html	2010-06-14 18:13:06 +0000
+++ templates/news/post_archive_day.html	2016-06-28 17:58:37 +0000
@@ -10,7 +10,7 @@
 <br />
 <div class="muttis_liebling">
     <div class="box_item_model even show_center">
-        <a href="{% url news_index %}{{ day|date:"Y" }}/{{ day|date:"m" }}">Archiv {{ day|date:"F" }}</a>
+        <a href="{% url 'news_index' %}{{ day|date:"Y" }}/{{ day|date:"m" }}">Archiv {{ day|date:"F" }}</a>
     {% autopaginate object_list 10 %}
     {% paginate %}
     </div>

=== modified file 'templates/news/post_archive_month.html'
--- templates/news/post_archive_month.html	2012-04-02 09:41:12 +0000
+++ templates/news/post_archive_month.html	2016-06-28 17:58:37 +0000
@@ -1,17 +1,17 @@
 {% extends "news/base_news.html" %}
 {% load custom_date %}
-{% load news %}
+{% load news_extras %}
 {% load pagination_tags %}
-{% load markup %}
+{#{% load markup %}#}
 
 {% block title %}{{ month|date:"F - Y" }} - {{block.super}}}{% endblock %}
 
 {% block content %}
 
 <h1>News Archive</h1>
-<a href="{% url news_index %}" class="invertedColor">News Archiv</a> &#187; 
-<a href="{% url news_index %}{{ month|date:"Y" }}/" class="invertedColor">{{ month|date:"Y" }}</a> &#187; 
-<a href="{% url news_index %}{{ month|date:"Y" }}/{{ month|date:"m" }}/" class="invertedColor">{{ month|date:"F" }}</a>
+<a href="{% url 'news_index' %}" class="invertedColor">News Archiv</a> &#187; 
+<a href="{% url 'news_index' %}{{ month|date:"Y" }}/" class="invertedColor">{{ month|date:"Y" }}</a> &#187; 
+<a href="{% url 'news_index' %}{{ month|date:"Y" }}/{{ month|date:"b" }}/" class="invertedColor">{{ month|date:"F" }}</a>
 {% for day in object_list %}
 {% endfor %}
 <br />

=== modified file 'templates/news/post_archive_year.html'
--- templates/news/post_archive_year.html	2012-04-02 09:41:12 +0000
+++ templates/news/post_archive_year.html	2016-06-28 17:58:37 +0000
@@ -1,18 +1,18 @@
 {% extends "news/base_news.html" %}
 {% load custom_date %}
-{% load news %}
+{% load news_extras %}
 {% load pagination_tags %}
-{% load markup %}
+{#{% load markup %}#}
 
 {% block title %}{{ year }} - {{ block.super }}{% endblock %}
 
 {% block content %}
 
 <h1>News Archive</h1>
-<a href="{% url news_index %}" class="invertedColor">News Archiv</a> &#187; 
-<a href="{% url news_index %}{{ year }}" class="invertedColor">{{ year }}</a>: 
+<a href="{% url 'news_index' %}" class="invertedColor">News Archiv</a> &#187;
+<a href="{% url 'news_index' %}{{ year|date:"Y" }}" class="invertedColor">{{ year|date:"Y" }}</a>: 
 {% for month in date_list %}
-	<a href="{% url news_index %}{{ year }}/{{ month|date:"m" }}/" class="invertedColor">{{ month|date:"F" }}</a>
+	<a href="{% url 'news_index' %}{{ year|date:"Y" }}/{{ month|date:"b" }}/" class="invertedColor">{{ month|date:"F" }}</a>
 	{% if not forloop.last %} | {% endif %}
 {% endfor %}
 <br />
@@ -22,7 +22,6 @@
 {% paginate %}
 </div>
 <br />
-
 {% for object in object_list %}
 	{% include "news/inlines/post_detail.html" %}
 {% endfor %}

=== modified file 'templates/news/post_detail.html'
--- templates/news/post_detail.html	2015-02-18 22:30:08 +0000
+++ templates/news/post_detail.html	2016-06-28 17:58:37 +0000
@@ -1,8 +1,8 @@
 {% extends "news/base_news.html" %}
 
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load threadedcommentstags %}
-{% load news %}
+{% load news_extras %}
 
 {% block title %}{{ object.title }} - {{ block.super }}{% endblock %}
 
@@ -13,7 +13,8 @@
 {% endblock %}
 
 {% block content %}
-<h1>{{ object.title }}</h1>
+<h1>News: {{ object.title }}</h1>
+<a class="invertedColor" href="{% url 'news_index' %}">News Archive: </a>
 {% if object.get_previous_by_publish %}
 <a class="invertedColor" href="{{ object.get_previous_post.get_absolute_url }}">&laquo; {{ object.get_previous_post }}</a>
 {% endif %}
@@ -26,6 +27,7 @@
 {% include "news/inlines/post_detail.html" %}
 
 <div class="blogEntry">
+
 	<h3>Comments on this Post:</h3>
 	{% include "threadedcomments/inlines/comments.html" %}
 </div>

=== modified file 'templates/news/post_list.html'
--- templates/news/post_list.html	2012-04-02 09:41:12 +0000
+++ templates/news/post_list.html	2016-06-28 17:58:37 +0000
@@ -1,34 +1,44 @@
 {% extends "news/base_news.html" %}
 
-{% load news %}
-{% load custom_date %}
+{% load news_extras %}
+{% load threadedcommentstags custom_date %}
 {% load pagination_tags %}
 
 {% block content %}
 
 <h1>News Archive</h1>
+<div>
 {% get_news_years as news_years %}
-<a href="{% url news_index %}" class="invertedColor">News Archiv</a>:
+<a href="{% url 'news_index' %}" class="invertedColor">News Archiv</a>:
 {% for muh in news_years %}
-	<a href="{% url news_index %}{{ muh.year }}" class="invertedColor">{{ muh.year }}</a>
+	<a href="{% url 'news_index' %}{{ muh.year }}" class="invertedColor">{{ muh.year }}</a>
 	{% if not forloop.last %} | {% endif %}
 {% endfor %}
-<br />
-
-<div class="center">
-{% autopaginate object_list 10 %}
-{% paginate %}
 </div>
+{% if perms.news %}
+	<div class="small posRight invertedColor">
+		{% if perms.news.add_post %}<a href="/admin/news/post/add/" class="invertedColor">Add New News</a>{% endif %}
+		{% if perms.news.change_post %}| <a href="/admin/news/post/{{object.id}}/" class="invertedColor">Edit</a>{% endif %}
+	</div>
+{% endif %}
 <br />
+<div class="blogEntry">
+	<div class="center">
+	{% autopaginate object_list 20 %}
+	{% paginate %}
+	</div>
+    <ul>
+        {% for object in object_list %}
+       	{% get_comment_count for object as ccount %}
+        <li><a href="{{object.get_absolute_url}}">{{ object.title }}</a> - <span class="small">posted at {{ object.publish|custom_date:user }}, {{ ccount }} comments</span></li>
+	    {% endfor %}
+	</ul>
 
-{% for object in object_list %}
-	{% include "news/inlines/post_detail.html" %}
-{% endfor %}
 {% if page_obj.has_other_pages %}
-
 <div class="center">
 {% paginate %}
 </div>
 {% endif %}
+</div>
 
 {% endblock %}

=== modified file 'templates/notification/base.html'
--- templates/notification/base.html	2015-02-18 22:30:08 +0000
+++ templates/notification/base.html	2016-06-28 17:58:37 +0000
@@ -1,6 +1,6 @@
 {% extends "base.html" %}
 
 {% block extra_head %}
-    <link rel="alternate" type="application/atom+xml" title="Notices Feed" href="{% url notification_feed_for_user %}" />
+    {#<link rel="alternate" type="application/atom+xml" title="Notices Feed" href="{% url 'notification_feed_for_user' %}" />#}
     <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/notice.css" />{{ block.super}}
 {% endblock %}
\ No newline at end of file

=== modified file 'templates/notification/forum_new_post/notice.html'
--- templates/notification/forum_new_post/notice.html	2015-03-16 07:00:43 +0000
+++ templates/notification/forum_new_post/notice.html	2016-06-28 17:58:37 +0000
@@ -1,2 +1,5 @@
-{% load i18n %}{% url profile_view user.username as user_url %}
-{% blocktrans with topic.get_absolute_url as topic_url and post.get_absolute_url as post_url%}<a href="{{ user_url }}">{{ user }}</a> has <a href="{{ post_url }}">replied</a> to the forum topic <a href="{{ topic_url }}">{{ topic }}</a>.{% endblocktrans %}
+{% load i18n %}{% url 'profile_view' user.username as user_url %}
+{% blocktrans with topic.get_absolute_url as topic_url and post.get_absolute_url as post_url%}
+<a href="{{ user_url }}">{{ user }}</a>
+has <a href="{{ post_url }}">replied</a> 
+to the forum topic <a href="{{ topic_url }}">{{ topic }}</a>.{% endblocktrans %}

=== modified file 'templates/notification/forum_new_topic/notice.html'
--- templates/notification/forum_new_topic/notice.html	2010-06-10 12:13:43 +0000
+++ templates/notification/forum_new_topic/notice.html	2016-06-28 17:58:37 +0000
@@ -1,2 +1,4 @@
-{% load i18n %}{% url profile_view user.username as user_url %}
-{% blocktrans with topic.get_absolute_url as topic_url %}A new forum topic has be created under  <a href="{{ topic_url }}">{{ topic }}</a> by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
+{% load i18n %}{% url 'profile_view' user.username as user_url %}
+{% blocktrans with topic.get_absolute_url as topic_url %}
+A new forum topic has be created under <a href="{{ topic_url }}">{{ topic }}</a>
+by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}

=== modified file 'templates/notification/messages_received/notice.html'
--- templates/notification/messages_received/notice.html	2012-05-08 21:52:15 +0000
+++ templates/notification/messages_received/notice.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,3 @@
 {% load i18n %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% blocktrans with message.get_absolute_url as message_url and message.sender|user_link as message_sender %}You have received the message <a href="{{ message_url }}">{{ message }}</a> from {{ message_sender }}.{% endblocktrans %}

=== modified file 'templates/notification/messages_replied/notice.html'
--- templates/notification/messages_replied/notice.html	2012-05-08 21:52:15 +0000
+++ templates/notification/messages_replied/notice.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,3 @@
 {% load i18n %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% blocktrans with message.parent_msg.get_absolute_url as message_url and message.parent_msg as message_parent_msg and message.recipient|user_link as message_recipient %}You have replied to <a href="{{ message_url }}">{{ message_parent_msg }}</a> from {{ message_recipient }}.{% endblocktrans %}

=== modified file 'templates/notification/messages_reply_received/notice.html'
--- templates/notification/messages_reply_received/notice.html	2012-05-08 21:52:15 +0000
+++ templates/notification/messages_reply_received/notice.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,3 @@
 {% load i18n %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% blocktrans with message.get_absolute_url as message_url and message.sender|user_link as message_sender and message.parent_msg as message_parent_msg %}{{ message_sender }} has sent you a reply to {{ message_parent_msg }}.{% endblocktrans %}

=== modified file 'templates/notification/messages_sent/notice.html'
--- templates/notification/messages_sent/notice.html	2012-05-08 21:52:15 +0000
+++ templates/notification/messages_sent/notice.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,3 @@
 {% load i18n %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% blocktrans with message.get_absolute_url as message_url and message.recipient|user_link as message_recipient %}You have sent the message <a href="{{ message_url }}">{{ message }}</a> to {{ message_recipient }}.{% endblocktrans %}

=== modified file 'templates/notification/notices.html'
--- templates/notification/notices.html	2012-09-09 18:03:59 +0000
+++ templates/notification/notices.html	2016-06-28 17:58:37 +0000
@@ -14,7 +14,7 @@
 {% autopaginate notices %}
 
 {% if notices %}
-	<a href="{% url notification_mark_all_seen %}" class="posRight small">{% trans "Mark all as seen" %}</a>
+	<a href="{% url 'notification_mark_all_seen' %}" class="posRight small">{% trans "Mark all as seen" %}</a>
 	{% paginate %}
 
 	{# TODO: get timezone support working with regroup #}
@@ -25,7 +25,7 @@
 		<table class="notifications">
 		{% for notice in date.list %}
 			<tr class="{% cycle "odd" "even" %} {% if notice.is_unseen %}italic{% endif %}">
-				<td class="type"><a href="{% url notification_notice notice.pk %} ">{% trans notice.notice_type.display %}</a></td>
+				<td class="type"><a href="{% url 'notification_notice' notice.pk %} ">{% trans notice.notice_type.display %}</a></td>
 				<td class="text">{{ notice.message|safe }}</td>
 				<td class="date">{{ notice.added|custom_date:user }}</td>
 			</tr>
@@ -43,15 +43,15 @@
 <div class="blogEntry">
 	<h2>{% trans "Settings" %}</h2>
 
-	{% url acct_email as email_url %}
+	{% url 'acct_email' as email_url %}
 	{% if user.email %}
 	<p>
 		{% trans "Primary email" %}: {{ user.email }}<br />
-		(You can change this in your <a href="{% url profile_edit %}">profile settings</a>.)
+		(You can change this in your <a href="{% url 'profile_edit' %}">profile settings</a>.)
 	</p>
 	{% else %}
 	<p class="errormessage">
-		You do not have a verified email address to which notifications can be sent. You can add one by <a href="{% url profile_edit %}">editing your profile</a>.
+		You do not have a verified email address to which notifications can be sent. You can add one by <a href="{% url 'profile_edit' %}">editing your profile</a>.
 	</p>
 	{% endif %}
 

=== modified file 'templates/notification/wiki_article_edited/notice.html'
--- templates/notification/wiki_article_edited/notice.html	2010-06-10 12:13:43 +0000
+++ templates/notification/wiki_article_edited/notice.html	2016-06-28 17:58:37 +0000
@@ -1,2 +1,4 @@
-{% load i18n %}{% url profile_view user.username as user_url %}
-{% blocktrans with article.get_absolute_url as article_url %}The wiki article <a href="{{ article_url }}">{{ article }}</a> has been edited by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
+{% load i18n %}{% url 'profile_view' user.username as user_url %}
+{% blocktrans with article.get_absolute_url as article_url %}
+The wiki article <a href="{{ article_url }}">{{ article }}</a>
+has been edited by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}

=== modified file 'templates/pybb/add_post.html'
--- templates/pybb/add_post.html	2016-01-02 10:01:11 +0000
+++ templates/pybb/add_post.html	2016-06-28 17:58:37 +0000
@@ -31,7 +31,7 @@
 </h1>
 
 <div class="blogEntry">
-	<a href="{% url pybb_index %}">Forums</a> &#187;
+	<a href="{% url 'pybb_index' %}">Forums</a> &#187;
 	{% if forum %}
 		<a href="{{ forum.category.get_absolute_url }}">{{ forum.category.name }}</a> &#187;
 		{{ forum }}

=== modified file 'templates/pybb/base.html'
--- templates/pybb/base.html	2015-12-31 10:43:25 +0000
+++ templates/pybb/base.html	2016-06-28 17:58:37 +0000
@@ -8,8 +8,8 @@
 {% block extra_head %}
 <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/forum.css" />
 
-<link rel="alternate" type="application/atom+xml" title="Latest Posts on all forums" href="{% url pybb_feed "posts" %}" />
-<link rel="alternate" type="application/atom+xml" title="Latest Topics on all forums" href="{% url pybb_feed "topics" %}" />
+<link rel="alternate" type="application/atom+xml" title="Latest Posts on all forums" href="{% url 'pybb_feed_posts' %}" />
+<link rel="alternate" type="application/atom+xml" title="Latest Topics on all forums" href="{% url 'pybb_feed_topics' %}" />
 
 {{ block.super}}
 {% endblock %}

=== modified file 'templates/pybb/category.html'
--- templates/pybb/category.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/category.html	2016-06-28 17:58:37 +0000
@@ -5,7 +5,7 @@
 <h1>Category: {{category.name}}</h1>
 
 <div class="blogEntry">
-	<a href="{% url pybb_index %}">Forums</a> &#187; {{category.name}}
+	<a href="{% url 'pybb_index' %}">Forums</a> &#187; {{category.name}}
 	<br /><br />
 	{% include 'pybb/inlines/display_category.html' %}
 </div>

=== modified file 'templates/pybb/edit_post.html'
--- templates/pybb/edit_post.html	2016-01-02 10:01:11 +0000
+++ templates/pybb/edit_post.html	2016-06-28 17:58:37 +0000
@@ -12,7 +12,7 @@
 <h1>{% trans "Edit Reply" %}</h1>
 
 <div class="blogEntry">
-	<a href="{% url pybb_index %}">Forums</a> &#187;
+	<a href="{% url 'pybb_index' %}">Forums</a> &#187;
 	{% pybb_link post.topic.forum.category %} &#187; 
 	<a href="{{ post.topic.forum.get_absolute_url }}">{{ post.topic.forum.name }}</a> &#187;
 	{{ post.topic }}

=== modified file 'templates/pybb/forum.html'
--- templates/pybb/forum.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/forum.html	2016-06-28 17:58:37 +0000
@@ -2,7 +2,7 @@
 {% load pybb_extras %}
 {% load i18n %}
 {% load humanize %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load custom_date %}
 
 {% block title %}
@@ -10,8 +10,8 @@
 {% endblock %}
 
 {% block extra_head %}
-<link rel="alternate" type="application/atom+xml" title="Latest Posts on forum '{{ forum.name }}'" href="{% url pybb_feed "posts" %}{{forum.id}}/"  />
-<link rel="alternate" type="application/atom+xml" title="Latest Topics on forum '{{ forum.name }}'" href="{% url pybb_feed "topics" %}{{forum.id}}/" />
+<link rel="alternate" type="application/atom+xml" title="Latest Posts on forum '{{ forum.name }}'" href="{% url 'pybb_feed_posts' %}{{forum.id}}/"  />
+<link rel="alternate" type="application/atom+xml" title="Latest Topics on forum '{{ forum.name }}'" href="{% url 'pybb_feed_topics' %}{{forum.id}}/" />
 {{ block.super }}
 {% endblock %}
 
@@ -19,11 +19,11 @@
 <h1>Forum: {{ forum }}</h1>
 
 <div class="blogEntry">
-	<a href="{% url pybb_index %}">Forums</a> &#187; 
+	<a href="{% url 'pybb_index' %}">Forums</a> &#187; 
 	{% pybb_link forum.category %} &#187; 
 	{{ forum }}
 	<br /><br />
-	<a class="button posRight" href="{% url pybb_add_topic forum.id %}">
+	<a class="button posRight" href="{% url 'pybb_add_topic' forum.id %}">
 		<img src="{{ MEDIA_URL }}forum/img/new_topic.png" alt ="{% trans "New Topic" %}" class="middle" />
 		<span class="middle">{% trans "New Topic" %}</span>
 	</a>
@@ -71,7 +71,7 @@
 	</table>
 
 	<br />
-	<a class="button posRight" href="{% url pybb_add_topic forum.id %}">
+	<a class="button posRight" href="{% url 'pybb_add_topic' forum.id %}">
 		<img src="{{ MEDIA_URL }}forum/img/new_topic.png" alt ="{% trans "New Topic" %}" class="middle" />
 		<span class="middle">{% trans "New Topic" %}</span>
 	</a>

=== modified file 'templates/pybb/inlines/display_category.html'
--- templates/pybb/inlines/display_category.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/inlines/display_category.html	2016-06-28 17:58:37 +0000
@@ -6,7 +6,7 @@
 
 {% load humanize %}
 {% load pybb_extras %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load custom_date %}
 
 <table class="forum">

=== modified file 'templates/pybb/inlines/forum_row.html'
--- templates/pybb/inlines/forum_row.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/inlines/forum_row.html	2016-06-28 17:58:37 +0000
@@ -3,7 +3,7 @@
 {% endcomment %}
 {% load humanize %}
 {% load pybb_extras %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load custom_date %}
     <tr>
       <td class="even" align="center" valign="middle">

=== modified file 'templates/pybb/inlines/post.html'
--- templates/pybb/inlines/post.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/inlines/post.html	2016-06-28 17:58:37 +0000
@@ -5,8 +5,8 @@
 {% load i18n %}
 {% load humanize %}
 {% load pybb_extras %}
-{% load wiki %}
-{% load wlprofile %}
+{% load wiki_extras %}
+{% load wlprofile_extras %}
 {% load custom_date %}
    <a name="post-{{ post.id }}"></a>
    <table class="{% cycle "odd" "even" %}" width="100%">
@@ -32,7 +32,7 @@
             <div class="userinfo">
                 {% if post.user.wlprofile.avatar %}
                 <div class="avatar">
-                    <a href="{% url profile_view post.user %}">
+                    <a href="{% url 'profile_view' post.user %}">
                     <img src="{{ post.user.wlprofile.avatar.url }}" alt="Avatar" />
                     </a>
                 </div>
@@ -94,25 +94,25 @@
             <div class="tools" style="float: left;">
                   {% if user.is_authenticated %}
                   {% ifnotequal user post.user %}
-                  <a href="{% url messages_compose_to post.user %}">
+                  <a href="{% url 'messages_compose_to' post.user %}">
                      <img src="{{ MEDIA_URL }}forum/img/en/send_pm.png" height="25" alt ="{% trans "Send PM" %}" />
                   </a>
                   {% endifnotequal %}
                   {% endif %}
                   {% if moderator or post|pybb_posted_by:user %}
-                  <a href="{% url pybb_edit_post post.id %}">
+                  <a href="{% url 'pybb_edit_post' post.id %}">
                     <img src="{{ MEDIA_URL }}forum/img/en/edit.png" height="25" alt ="{% trans "Edit" %}" />
                   </a>
                   {% endif %}
                   {% if moderator or post|pybb_equal_to:last_post %}
                   {% if moderator or post.user|pybb_equal_to:user %}
-                  <a href="{% url pybb_delete_post post.id %}">
+                  <a href="{% url 'pybb_delete_post' post.id %}">
                      <img src="{{ MEDIA_URL }}forum/img/en/delete.png" height="25" alt ="{% trans "Delete" %}" />
                   </a>
                   {% endif %}
             </div>
             <div class="tools" style="float: right;">
-                <a href="{% url pybb_add_post topic.id %}?quote_id={{ post.id }}">
+                <a href="{% url 'pybb_add_post' topic.id %}?quote_id={{ post.id }}">
                      <img src="{{ MEDIA_URL }}forum/img/en/quote.png" height="25" alt ="{% trans "Quote" %}" />
                   </a>
                   {% endif %}

=== modified file 'templates/pybb/inlines/topic_row.html'
--- templates/pybb/inlines/topic_row.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/inlines/topic_row.html	2016-06-28 17:58:37 +0000
@@ -3,7 +3,7 @@
 {% endcomment %}
 {% load humanize %}
 {% load pybb_extras %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load custom_date %}
    <tr class="topic_description {% cycle "odd" "even" %}">
          <td align="center" valign="middle">

=== modified file 'templates/pybb/last_posts.html'
--- templates/pybb/last_posts.html	2012-03-30 23:26:23 +0000
+++ templates/pybb/last_posts.html	2016-06-28 17:58:37 +0000
@@ -1,5 +1,5 @@
 {% load i18n %} 
-{% load wlprofile %} 
+{% load wlprofile_extras %} 
 {% load custom_date %} 
 {% load pybb_extras %}
 
@@ -12,7 +12,7 @@
 			<li>
 				{{ post.topic.forum.name }}<br />
 				<a href="{{ post.get_absolute_url }}" title="{{ post.topic.name }}">{{ post.topic.name|pybb_cut_string:30 }}</a><br />
-				by <a href="{% url profile_view post.user %}">{{post.user.username}}</a> {{ post.created|minutes }} ago
+				by <a href="{% url 'profile_view' post.user %}">{{post.user.username}}</a> {{ post.created|minutes }} ago
 			</li>
 			{% endfor %}
 		</ul>

=== modified file 'templates/pybb/post_form.html'
--- templates/pybb/post_form.html	2016-01-02 10:01:11 +0000
+++ templates/pybb/post_form.html	2016-06-28 17:58:37 +0000
@@ -15,14 +15,21 @@
 			var markup = $('.post-form #id_markup').val();
 
 			args = {'content': raw_content, 'markup': markup}
-			$.post('{% url pybb_post_ajax_preview %}', args, function(data) {
+			$.post("{% url 'pybb_post_ajax_preview' %}", args, function(data) {
 				if (data.error) {
 					alert(data.error);
 				} else {
 					$('.preview-box .content').html(data.content);
 					$('.preview-box').show();
 				}
-			}, 'json');
+			}, 'json')
+			.fail(function(o, status, error) {
+				alert( "Something has gone wrong. Please inform the Webmaster in the forum.\n"
+					  + "Object: " + o + "\n"
+					  + "Status: " + status + "\n"
+					  + "What: " + error );
+			})
+			
 		});
 	});
 </script>
@@ -42,7 +49,7 @@
 			Help on Syntax
 		</a>
 	</div>
-	
+
 	<form class="post-form" action="{{ form_url }}" method="post" enctype="multipart/form-data">
 	{{ form.as_p }}
 	{% csrf_token %}

=== modified file 'templates/pybb/topic.html'
--- templates/pybb/topic.html	2015-02-18 22:30:08 +0000
+++ templates/pybb/topic.html	2016-06-28 17:58:37 +0000
@@ -2,8 +2,8 @@
 {% load pybb_extras %}
 {% load i18n %}
 {% load humanize %}
-{% load wiki %}
-{% load wlprofile %}
+{% load wiki_extras %}
+{% load wlprofile_extras %}
 {% load custom_date %}
 
 {% block title %}
@@ -11,15 +11,15 @@
 {% endblock title %}
 
 {% block extra_head %}
-<link rel="alternate" type="application/atom+xml" title="Latest Posts on forum '{{ topic.forum.name }}'" href="{% url pybb_feed "posts" %}{{topic.forum.id}}/"  />
-<link rel="alternate" type="application/atom+xml" title="Latest Topics on forum '{{ topic.forum.name }}'" href="{% url pybb_feed "topics"%}{{topic.forum.id}}/" />
+<link rel="alternate" type="application/atom+xml" title="Latest Posts on forum '{{ topic.forum.name }}'" href="{% url 'pybb_feed_posts' %}{{topic.forum.id}}/"  />
+<link rel="alternate" type="application/atom+xml" title="Latest Topics on forum '{{ topic.forum.name }}'" href="{% url 'pybb_feed_topics' %}{{topic.forum.id}}/" />
 {{ block.super }}
 {% endblock %}
 
 {% block content %}
 <h1>Topic: {{ topic }}</h1>
 <div class="blogEntry">
-	<a href="{% url pybb_index %}">Forums</a> &#187; 
+	<a href="{% url 'pybb_index' %}">Forums</a> &#187; 
 	{% pybb_link topic.forum.category %} &#187; 
 	<a href="{{ topic.forum.get_absolute_url }}">{{ topic.forum.name }}</a> &#187;
 	{{ topic }}
@@ -27,23 +27,23 @@
 	<div class="posRight">
 	{% if moderator %}
 		{% if topic.sticky %}
-		<a class="button" href="{% url pybb_unstick_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_unstick_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/unstick.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Unstick Topic" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_stick_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_stick_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/sticky.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Stick Topic" %}</span>
 		</a>
 		{% endif %}
 		{% if topic.closed %}
-		<a class="button" href="{% url pybb_open_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_open_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/open.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Open Topic" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_close_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_close_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/closed.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Close Topic" %}</span>
 		</a>
@@ -51,17 +51,17 @@
 	{% endif %}
 	{% if user.is_authenticated %}
 		{% if subscribed %}
-		<a class="button" href="{% url pybb_delete_subscription topic.id %}?from_topic">
+		<a class="button" href="{% url 'pybb_delete_subscription' topic.id %}?from_topic">
 			<img src="{{ MEDIA_URL }}forum/img/unsubscribe.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Unsubscribe" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_add_subscription topic.id %}">
+		<a class="button" href="{% url 'pybb_add_subscription' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/subscribe.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Subscribe" %}</span>
 		</a>
 		{% endif %}
-		<a class="button" href="{% url pybb_add_post topic.id %}">
+		<a class="button" href="{% url 'pybb_add_post' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/send.png" alt ="" class="middle" />
 			<span class="middle">{% trans "New Reply" %}</span>
 		</a>
@@ -79,8 +79,8 @@
 		<tr class="odd">
 			<td class="author">
 				{{ post.user|user_link }}<br />
-				{% if post.user.wlprofile.avatar %}
-				<a href="{% url profile_view post.user %}">
+				{% if post.user.wlprofile_extras.avatar %}
+				<a href="{% url 'profile_view' post.user %}">
 					<img src="{{ post.user.wlprofile.avatar.url }}" alt="Avatar" />
 				</a>
 				{% endif %}
@@ -135,17 +135,17 @@
 					<span class="middle">{% trans "Top" %}</span>
 				</button>
 
-				<button onclick="window.location.href='{% url pybb_add_post topic.id %}?quote_id={{ post.id }}';">
+				<button onclick="window.location.href='{% url 'pybb_add_post' topic.id %}?quote_id={{ post.id }}';">
 					<img src="{{ MEDIA_URL }}forum/img/quote.png" alt ="" class="middle" />
 					<span class="middle">{% trans "Quote" %}</span>
 				</button>
 				{% if moderator or post|pybb_posted_by:user %}
-					<button onclick="window.location.href='{% url pybb_edit_post post.id %}';">
+					<button onclick="window.location.href='{% url 'pybb_edit_post' post.id %}';">
 						<img src="{{ MEDIA_URL }}forum/img/edit.png" alt ="" class="middle" />
 						<span class="middle">{% trans "Edit" %}</span>
 					</button>
 					{% if moderator or post|pybb_equal_to:last_post %}
-					<button onclick="window.location.href='{% url pybb_delete_post post.id %}';">
+					<button onclick="window.location.href='{% url 'pybb_delete_post' post.id %}';">
 						<img src="{{ MEDIA_URL }}forum/img/delete.png" alt ="" class="middle" />
 						<span class="middle">{% trans "Delete" %}</span>
 					</button>
@@ -167,7 +167,7 @@
 			<td class="author">
 				{{ post.user|user_link }}<br />
 				{% if post.user.wlprofile.avatar %}
-				<a href="{% url profile_view post.user %}">
+				<a href="{% url 'profile_view' post.user %}">
 					<img src="{{ post.user.wlprofile.avatar.url }}" alt="Avatar" />
 				</a>
 				{% endif %}
@@ -222,17 +222,17 @@
 					<span class="middle">{% trans "Top" %}</span>
 				</a>
 
-				<a class="button" href="{% url pybb_add_post topic.id %}?quote_id={{ post.id }}">
+				<a class="button" href="{% url 'pybb_add_post' topic.id %}?quote_id={{ post.id }}">
 					<img src="{{ MEDIA_URL }}forum/img/quote.png" alt ="" class="middle" />
 					<span class="middle">{% trans "Quote" %}</span>
 				</a>
 				{% if moderator or post|pybb_posted_by:user %}
-					<a class="button" href="{% url pybb_edit_post post.id %}">
+					<a class="button" href="{% url 'pybb_edit_post' post.id %}">
 						<img src="{{ MEDIA_URL }}forum/img/edit.png" alt ="" class="middle" />
 						<span class="middle">{% trans "Edit" %}</span>
 					</a>
 					{% if moderator or post|pybb_equal_to:last_post %}
-					<a class="button" href="{% url pybb_delete_post post.id %}">
+					<a class="button" href="{% url 'pybb_delete_post' post.id %}">
 						<img src="{{ MEDIA_URL }}forum/img/delete.png" alt ="" class="middle" />
 						<span class="middle">{% trans "Delete" %}</span>
 					</a>
@@ -251,23 +251,23 @@
 	<div class="posRight">
 	{% if moderator %}
 		{% if topic.sticky %}
-		<a class="button" href="{% url pybb_unstick_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_unstick_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/unstick.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Unstick Topic" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_stick_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_stick_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/sticky.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Stick Topic" %}</span>
 		</a>
 		{% endif %}
 		{% if topic.closed %}
-		<a class="button" href="{% url pybb_open_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_open_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/open.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Open Topic" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_close_topic topic.id %}">
+		<a class="button" href="{% url 'pybb_close_topic' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/closed.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Close Topic" %}</span>
 		</a>
@@ -275,17 +275,17 @@
 	{% endif %}
 	{% if user.is_authenticated %}
 		{% if subscribed %}
-		<a class="button" href="{% url pybb_delete_subscription topic.id %}?from_topic">
+		<a class="button" href="{% url 'pybb_delete_subscription' topic.id %}?from_topic">
 			<img src="{{ MEDIA_URL }}forum/img/unsubscribe.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Unsubscribe" %}</span>
 		</a>
 		{% else %}
-		<a class="button" href="{% url pybb_add_subscription topic.id %}">
+		<a class="button" href="{% url 'pybb_add_subscription' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/subscribe.png" alt ="" class="middle" />
 			<span class="middle">{% trans "Subscribe" %}</span>
 		</a>
 		{% endif %}
-		<a class="button" href="{% url pybb_add_post topic.id %}">
+		<a class="button" href="{% url 'pybb_add_post' topic.id %}">
 			<img src="{{ MEDIA_URL }}forum/img/send.png" alt ="" class="middle" />
 			<span class="middle">{% trans "New Reply" %}</span>
 		</a>

=== modified file 'templates/registration/activate.html'
--- templates/registration/activate.html	2012-05-08 21:52:15 +0000
+++ templates/registration/activate.html	2016-06-28 17:58:37 +0000
@@ -12,7 +12,7 @@
 <div class="blogEntry">
 	<p class="errormessage">
 		An error occured: Either this account has already been activated or activation key is invalid.<br />
-		Try <a href="{% url auth_login %}">logging in</a>.
+		Try <a href="{% url 'auth_login' %}">logging in</a>.
 	</p>
 </div>
 {% endblock %}

=== modified file 'templates/registration/activation_complete.html'
--- templates/registration/activation_complete.html	2012-05-08 21:52:15 +0000
+++ templates/registration/activation_complete.html	2016-06-28 17:58:37 +0000
@@ -11,7 +11,7 @@
 <h1>Activation</h1>
 <div class="blogEntry">
 	<p>
-		You are now activated and can <a href="{% url auth_login %}">log in</a>.
+		You are now activated and can <a href="{% url 'auth_login' %}">log in</a>.
 	</p>
 </div>
 {% endblock %}

=== modified file 'templates/registration/activation_email.txt'
--- templates/registration/activation_email.txt	2012-11-15 18:55:22 +0000
+++ templates/registration/activation_email.txt	2016-06-28 17:58:37 +0000
@@ -3,7 +3,7 @@
 You (or someone else) requested an account on {{ site }}. If this wasn't you, 
 please ignore this email. If this was you, please click on the link provided below
 
-http://{{ site }}{% url registration_activate activation_key %}
+http://{{ site }}{% url 'registration_activate' activation_key %}
 
 
 This link will be valid for {{ expiration_days }} days...

=== modified file 'templates/registration/base.html'
--- templates/registration/base.html	2015-02-18 22:30:08 +0000
+++ templates/registration/base.html	2016-06-28 17:58:37 +0000
@@ -5,4 +5,5 @@
 
 {% block extra_head %}
 <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/register.css" />{{ block.super}}
+<script src="https://www.google.com/recaptcha/api.js"; async defer></script>
 {% endblock %}

=== modified file 'templates/registration/login.html'
--- templates/registration/login.html	2012-05-08 21:52:15 +0000
+++ templates/registration/login.html	2016-06-28 17:58:37 +0000
@@ -19,7 +19,7 @@
 		<table>
 			<tr>
 				<td class="grey">
-					{{ form.username.label_tag }}: 
+					{{ form.username.label_tag }} 
 				</td>
 				<td>
 					{{ form.username }}
@@ -27,7 +27,7 @@
 			</tr>
 			<tr>
 				<td class="grey">
-					{{ form.password.label_tag }}: 
+					{{ form.password.label_tag }} 
 				</td>
 				<td>
 					{{ form.password }}
@@ -38,6 +38,6 @@
 		<input type="submit" value="login" />
 		<input type="hidden" name="next" value="{{ next }}" />
 	</form>
-	<p><a href="{% url auth_password_reset %}">Lost password?</a> | <a href="{% url registration_register %}">Register now!</a></p>
+	<p><a href="{% url 'auth_password_reset' %}">Lost password?</a> | <a href="{% url 'registration_register' %}">Register now!</a></p>
 </div>
 {% endblock %}

=== modified file 'templates/registration/password_reset_complete.html'
--- templates/registration/password_reset_complete.html	2012-05-08 21:52:15 +0000
+++ templates/registration/password_reset_complete.html	2016-06-28 17:58:37 +0000
@@ -13,7 +13,7 @@
 <h1>Password Reset</h1>
 <div class="blogEntry">
 	<p>
-		Your new password has been set. You may go ahead and <a href="{% url auth_login %}">log in</a> now.
+		Your new password has been set. You may go ahead and <a href="{% url 'auth_login' %}">log in</a> now.
 	</p>
 </div>
 {% endblock %}

=== modified file 'templates/registration/password_reset_email.html'
--- templates/registration/password_reset_email.html	2009-02-20 11:19:12 +0000
+++ templates/registration/password_reset_email.html	2016-06-28 17:58:37 +0000
@@ -4,7 +4,7 @@
 
 {% trans "Please go to the following page and choose a new password:" %}
 {% block reset_link %}
-{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %}
+{{ protocol }}://{{ domain }}{% url 'django.contrib.auth.views.password_reset_confirm' uidb64=uid, token=token %}
 {% endblock %}
 {% trans "Your username, in case you've forgotten:" %} {{ user.username }}
 

=== modified file 'templates/registration/registration_form.html'
--- templates/registration/registration_form.html	2012-05-08 21:52:15 +0000
+++ templates/registration/registration_form.html	2016-06-28 17:58:37 +0000
@@ -6,6 +6,7 @@
 {% block title %}
 Registration - {{ block.super }}
 {% endblock %}
+{{ form.non_field_errors }} 
 
 {% block content %}
 <h1>Registration</h1>
@@ -15,8 +16,10 @@
 			<tr>
 				<td class="grey">Username:</td>
 				<td>
-					{{ registration_form.username }}
-{% for error in registration_form.username.errors %}
+					{{ form.username }}
+
+
+{% for error in form.username.errors %}
 					<span class="errormessage">{{ error }}</span>
 {% endfor %}
 				</td>
@@ -24,8 +27,8 @@
 			<tr>
 				<td class="grey">Email:</td>
 				<td>
-					{{ registration_form.email }}
-{% for error in registration_form.email.errors %}
+					{{ form.email }}
+{% for error in form.email.errors %}
 					<span class="errormessage">{{ error }}</span>
 {% endfor %}
 				</td>
@@ -33,17 +36,14 @@
 			<tr>
 				<td class="grey">Password:</td>
 				<td>
-					{{ registration_form.password1 }}
-{% for error in registration_form.password1.errors %}
-					<span class="errormessage">{{ error }}</span>
-{% endfor %}
+					{{ form.password1 }}
 				</td>
 			</tr>
 			<tr>
 				<td class="grey">Password (again):</td>
 				<td>
-					{{ registration_form.password2 }}
- {% for error in registration_form.password2.errors %}
+					{{ form.password2 }}
+ {% for error in form.password2.errors %}
 					<span class="errormessage">{{ error }}</span>
  {% endfor %}
 				</td>
@@ -51,11 +51,8 @@
 			<tr>
 				<td class="grey">Prove that <br />you're no spambot:</td>
 				<td>
-					<div class="noshadow">{{ registration_form.captcha|safe }}</div>
- {% for error in registration_form.captcha.errors %}
-					<span class="errormessage">{{error }}</span>
- {% endfor %}
-				</td>
+					<div class="noshadow">{{ form.captcha|safe }}</div>
+ 				</td>
 			</tr>
 		</table>
 		<input type="submit" value="Register" />

=== modified file 'templates/right_boxes.html'
--- templates/right_boxes.html	2015-04-07 18:43:07 +0000
+++ templates/right_boxes.html	2016-06-28 17:58:37 +0000
@@ -4,13 +4,14 @@
  This file is included by mainpage and contains all the left menu boxes
  on the site
 {% endcomment %}
+
+
 {% load inbox %}
 {% load i18n %}
-{% load wlprofile wlpoll wlevents %}
+{% load wlprofile_extras wlpoll_extras wlevents_extras %}
 {% load pybb_extras %}
 {% load online_users %}
 
-
 <!-- Donation Box -->
 <div class="columnModule">
 	<h3>Donation</h3>
@@ -56,10 +57,10 @@
 			{% if p.user_has_voted %}
 				<p class="small">You already voted on this!</p>
 				<div class="center">
-					<input type="button" value="Results" onclick="location='{% url wlpoll_detail p.id %}'" />
+					<input type="button" value="Results" onclick="location='{% url 'wlpoll_detail' p.id %}'" />
 				</div>
 			{% else %}
-				<form method="post" action="{% url wlpoll_vote p.id %}">
+				<form method="post" action="{% url 'wlpoll_vote' p.id %}">
 					<ul class="poll">
 					{% for c in p.choices.all %}
 						<li><input id="{{ c.id }}" class="radio" type="radio" name="choice_id" value="{{ c.id }}" /><label for="{{ c.id }}">{{ c.choice }}</label></li>
@@ -67,26 +68,25 @@
 					</ul>
 					<div class="center">
 						<input type="submit" value="Vote" />
-						<input type="button" value="Results" onclick="location='{% url wlpoll_detail p.id %}'" />
+						<input type="button" value="Results" onclick="location='{% url 'wlpoll_detail' p.id %}'" />
 					</div>
 					{% csrf_token %}
 				</form>
 			{% endif %}
 		{% else %}
-			<p class="small"><a href="{% url auth_login %}?next={{ request.path|iriencode }}">Log in</a> to vote!</p>
+			<p class="small"><a href="{% url 'auth_login' %}?next={{ request.path|iriencode }}">Log in</a> to vote!</p>
 			<div class="center">
-				<input type="button" value="Results" onclick="location='{% url wlpoll_detail p.id %}'" />
+				<input type="button" value="Results" onclick="location='{% url 'wlpoll_detail' p.id %}'" />
 			</div>
 		{% endif %}
 	{% endfor %}
 		<div class="center">
-			<p><a href="{% url wlpoll_archive %}">Archive</a></p>
+			<p><a href="{% url 'wlpoll_archive' %}">Archive</a></p>
 		</div>
 	</div>
 </div>
 {% endif %}
 
-
 <!-- Future Events if any -->
 {% get_future_events as events %}
 {% if events.count %}         
@@ -111,6 +111,5 @@
 <!-- Logged in users -->
 {% online_users 10 %}
 
-
 <!-- Latest Post -->
 {% pybb_last_posts %}

=== modified file 'templates/threadedcomments/inlines/comments.html'
--- templates/threadedcomments/inlines/comments.html	2012-09-08 08:19:28 +0000
+++ templates/threadedcomments/inlines/comments.html	2016-06-28 17:58:37 +0000
@@ -2,7 +2,7 @@
    vim:ft=htmldjango
 {% endcomment %}
 
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load threadedcommentstags %}
 
 	{% get_threaded_comment_form as form %}
@@ -15,7 +15,7 @@
 				<tr>
 					<td class="author" rowspan="2">
 						{% if comment.user.wlprofile.avatar %}
-						<a href="{% url profile_view comment.user %}">
+						<a href="{% url 'profile_view' comment.user %}">
 							<img style="width: 50px; height: 50px;" src="{{ comment.user.wlprofile.avatar.url }}" />
 						</a>
 						<br />
@@ -29,7 +29,7 @@
 				<tr>
 					<td class="reply_link small">
 						{% if user.is_authenticated %}
-						<a href="javascript:show_reply_form('c{{ comment.id }}','{% get_comment_url object comment %}', {{ comment.depth }})">Reply</a>
+						<a href="javascript:show_reply_form('c{{ comment.id }}', '{% get_comment_url object comment %}', {{ comment.depth }})">Reply</a>
 						{% endif %}
 					</td>
 				</tr>
@@ -52,7 +52,7 @@
 	</form>
 	{% else %}
 	<p>
-		<a href="{% url auth_login %}?next={{ request.path }}">Log in</a> to post comments!
+		<a href="{% url 'auth_login' %}?next={{ request.path }}">Log in</a> to post comments!
 	</p>
 	{% endif %}
 

=== modified file 'templates/threadedcomments/inlines/reply_to.js'
--- templates/threadedcomments/inlines/reply_to.js	2013-06-11 18:53:04 +0000
+++ templates/threadedcomments/inlines/reply_to.js	2016-06-28 17:58:37 +0000
@@ -2,18 +2,18 @@
     vim:ft=htmldjango:
 {% endcomment %}
 {% load threadedcommentstags %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 <script type="text/javascript">
 function show_reply_form(comment_id, url, depth) {
 	var comment = $('#' + comment_id);
-	var reply_link = $('#' + comment_id + " .reply_link");
+	var reply_link = $('#' + comment_id + ' .reply_link');
 	var reply_form = $('<div class="comment odd response" style="margin-left: ' + (depth+1) +'cm;">'
 			+ '<table>'
 				+ '<tr>'
 					+ '<td class="author">'
                 {% if post.user.wlprofile.avatar %}
-						+ '<a href="{% url profile_view user %}">'
+						+ '<a href="{% url 'profile_view' user %}">'
 							+ '<img style="width: 50px; height: 50px;" src="{{ user.wlprofile.avatar.url }}" />'
 						+ '</a>'
                 {% endif %}
@@ -23,7 +23,10 @@
 					+ '<td class="text">'
 						+ '<form method="POST" action="' + url + '?next={{object.get_absolute_url}}">'
 							+ '<span class="errormessage">{{ form.comment.errors }}</span>'
-							+ '{{ form.comment }}'
+							/* NOCOMM franku: i don't know the reason why this do not work anymore
+							   I just replaced it with the next line
+							+ '{{ form.comment }}'*/
+							+ '<textarea cols="40" id="id_comment" maxlength="1000" name="comment" rows="10"></textarea>'
 							+ '<br />'
 							+ '<input type="hidden" name="markup" value="1" />'
 							+ '<input type="submit" value="Submit Comment" />'

=== modified file 'templates/wiki/article_content.html'
--- templates/wiki/article_content.html	2009-06-09 20:19:06 +0000
+++ templates/wiki/article_content.html	2016-06-28 17:58:37 +0000
@@ -1,5 +1,5 @@
-{% load wiki %}
-{% load markup %}
+{% load wiki_extras %}
+{#{% load markup %}#}
 {% load wl_markdown %}
 {% load switchcase %}
 {% load restructuredtext %}
@@ -8,7 +8,7 @@
 {% switch markup %}
     {% case 'crl' %} {{ content|creole|wikiwords|safe }} {% endcase %}
     {% case 'rst' %} {{ content|restructuredtext|wikiwords|safe }} {% endcase %}
-    {% case 'mrk' %} {{ content|wl_markdown:"escape"}} {% endcase %}
-    {% case 'txl' %} {{ content|force_escape|restore_commandsymbols|textile|wikiwords }} {% endcase %}
+    {% case 'mrk' %} {{ content|wl_markdown:"bleachit"}} {% endcase %}
+    {#{% case 'txl' %} {{ content|force_escape|restore_commandsymbols|textile|wikiwords }} {% endcase %}#}
     {% case '' %} {{ content|force_escape|wikiwords|linebreaks|safe }} {% endcase %}
 {% endswitch %}

=== modified file 'templates/wiki/article_teaser.html'
--- templates/wiki/article_teaser.html	2009-03-16 17:28:02 +0000
+++ templates/wiki/article_teaser.html	2016-06-28 17:58:37 +0000
@@ -1,5 +1,5 @@
 
-{% load wiki %}
+{% load wiki_extras %}
 {% load avatar_tags %}
 {% load custom_date %}
 
@@ -7,7 +7,7 @@
     <td class="meta">
         <div class="avatar">{% avatar article.latest_changeset.editor 40 %}</div>
         <div class="details">
-            <a href="{% url profiles.views.profile article.latest_changeset.editor.username %}">
+            <a href="{% url 'profiles.views.profile' article.latest_changeset.editor.username %}">
                 {{ article.latest_changeset.editor }}
             </a>
         </div>

=== modified file 'templates/wiki/base.html'
--- templates/wiki/base.html	2015-02-18 22:30:08 +0000
+++ templates/wiki/base.html	2016-06-28 17:58:37 +0000
@@ -6,7 +6,7 @@
 
 {% block extra_head %}
 {{ block.super}}
-<link rel="alternate" type="application/rss+xml" title="Wiki History (RSS)" href="{% url wiki_history_feed "rss" %}" />
-<link rel="alternate" type="application/atom+xml" title="Wiki History (Atom)" href="{% url wiki_history_feed "atom" %}" />
+<link rel="alternate" type="application/rss+xml" title="Wiki History (RSS)" href="{% url 'wiki_history_feed_rss' %}" />
+<link rel="alternate" type="application/atom+xml" title="Wiki History (Atom)" href="{% url 'wiki_history_feed_atom' %}" />
 <link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/wiki.css" />
 {% endblock %}

=== modified file 'templates/wiki/changeset.html'
--- templates/wiki/changeset.html	2012-09-20 20:03:21 +0000
+++ templates/wiki/changeset.html	2016-06-28 17:58:37 +0000
@@ -7,8 +7,8 @@
 
 {% block content %}
 <div class="posRight small">
-	<a href="{% url wiki_article article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
-	| <a href="{% url wiki_article_history article.title %}" class="invertedColor">{% trans "Editing history" %}</a>
+	<a href="{% url 'wiki_article' article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
+	| <a href="{% url 'wiki_article_history' article.title %}" class="invertedColor">{% trans "Editing history" %}</a>
 </div>
 <h1>{% trans "Changes in" %} {{ article.title }}</h1>
 

=== modified file 'templates/wiki/edit.html'
--- templates/wiki/edit.html	2016-01-17 08:53:17 +0000
+++ templates/wiki/edit.html	2016-06-28 17:58:37 +0000
@@ -1,6 +1,6 @@
 {% extends 'wiki/base.html' %}
 {% load i18n %}
-{% load wlimages %}
+{% load wlimages_extras %}
 
 {% block title %}
 {% trans "Editing" %} {{ article.title }} - {{ block.super }}
@@ -18,7 +18,7 @@
 		$("#id_preview").click(function(){ 
 			// Activate preview
 			$("#preview").html("<h3>Preview</h3>\n<hr>\n<div class=\"wiki_article\" id=\"content_preview\">Loading...</div>\n<hr>");
-			$("#content_preview").load( "{% url wiki_preview %}", 
+			$("#content_preview").load( "{% url 'wiki_preview' %}", 
 				{"body": $("#id_content").val()});
 		})
 
@@ -27,7 +27,7 @@
 		$("#id_diff").click(function(){ 
 			// Activate preview
 			$("#diff").html("<h3>Diff</h3>\n<hr>\n<div id=\"content_diff\">Loading...</div>\n<hr>");
-			$("#content_diff").load( "{% url wiki_preview_diff %}", 
+			$("#content_diff").load( "{% url 'wiki_preview_diff' %}", 
 				{"body": $("#id_content").val(), "article": {{ object_id }} });
 		})
 	{%endifequal%}
@@ -39,9 +39,9 @@
 {% block content %}
 {% if not new_article %} 
 <div class="posRight small">
-	<a href="{% url wiki_article article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
+	<a href="{% url 'wiki_article' article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
 	|
-	<a href="{% url wiki_article_history article.title %}" class="invertedColor">{% trans "Editing history" %}</a>
+	<a href="{% url 'wiki_article_history' article.title %}" class="invertedColor">{% trans "Editing history" %}</a>
 </div>
 {% endif %}
 <h1>{% trans "Editing" %} {{ article.title }}</h1>

=== added directory 'templates/wiki/feeds'
=== renamed file 'templates/feeds/history_description.html' => 'templates/wiki/feeds/history_description.html'
=== renamed file 'templates/feeds/history_title.html' => 'templates/wiki/feeds/history_title.html'
=== modified file 'templates/wiki/history.html'
--- templates/wiki/history.html	2013-06-12 10:05:41 +0000
+++ templates/wiki/history.html	2016-06-28 17:58:37 +0000
@@ -1,6 +1,6 @@
 {% extends 'wiki/base.html' %}
 {% load i18n %}
-{% load custom_date wlprofile %}
+{% load custom_date wlprofile_extras %}
 
 
 {% block title %}
@@ -56,13 +56,13 @@
 {% endif %}
 
 <div class="posRight small">
-	<a href="{% url wiki_article article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
-	| <a href="{% url wiki_article_history_feed title=article.title,feedtype="atom" %}"  class="invertedColor">Atom Feed</a>
+	<a href="{% url 'wiki_article' article.title %}" class="invertedColor">{% trans "Back to article" %}</a>
+	| <a href="{% url 'wiki_article_history_feed_atom' article.title %}"  class="invertedColor">Atom Feed</a>
 </div>
 <h1>{% trans "Article History of" %} {{ article.title }}</h1>
 
 <div class="blogEntry">
-	<form action="{% url wiki_revert_to_revision article.title %}" method="post" onsubmit="return check(this);">
+	<form action="{% url 'wiki_revert_to_revision' article.title %}" method="post" onsubmit="return check(this);">
 		<table class="history_list">
 		<thead>
 			<tr>

=== modified file 'templates/wiki/index.html'
--- templates/wiki/index.html	2014-12-01 05:54:55 +0000
+++ templates/wiki/index.html	2016-06-28 17:58:37 +0000
@@ -1,6 +1,6 @@
 {% extends "wiki/base.html" %}
 {% load i18n %}
-{% load custom_date wlprofile %}
+{% load custom_date wlprofile_extras %}
 
 {% block title %}Wiki Index - {{ block.super }}{% endblock %}
 
@@ -20,7 +20,7 @@
 		<tbody>
 			{% for article in articles %}
 			<tr>
-				<td><a href="{% url wiki_article article.title %}">{{ article.title }}</a></td>
+				<td><a href="{% url 'wiki_article' article.title %}">{{ article.title }}</a></td>
 				<td>{{ article.summary }}</td>
 				<td>{{ article.last_update|custom_date:user }}</td>
 			</tr>
@@ -28,7 +28,7 @@
 		</tbody>
 		</table>
 	{% else %}
-        <p><a href="{% url wiki_edit "NewArticle" %}">{% trans "Create a new article" %}</a>.</p>
+        <p><a href="{% url 'wiki_edit' "NewArticle" %}">{% trans "Create a new article" %}</a>.</p>
 	{% endif %}
 </div>
 {% endblock %}

=== modified file 'templates/wiki/recentchanges.html'
--- templates/wiki/recentchanges.html	2012-09-09 18:03:59 +0000
+++ templates/wiki/recentchanges.html	2016-06-28 17:58:37 +0000
@@ -33,13 +33,13 @@
 	<tr class="{% cycle 'odd' 'even' %}">
 		<td>
 			{% if change.old_title %} 
-				<a href="{% url wiki_changeset change.article.title,change.revision %}">Modified</a>
+				<a href="{% url 'wiki_changeset' change.article.title change.revision %}">Modified</a>
 			{% else %} 
-				<a href="{% url wiki_article change.article.title %}">Added</a>
+				<a href="{% url 'wiki_article' change.article.title %}">Added</a>
 			{% endif %}
 		</td>
 		<td>
-			<a href="{% url wiki_article change.article.title %}">{{ change.article.title }}</a>
+			<a href="{% url 'wiki_article' change.article.title %}">{{ change.article.title }}</a>
 		</td>
 		<td>{{ change.modified|custom_date:user }}</td>
 		<td>{{ change.editor }}</td>

=== modified file 'templates/wiki/view.html'
--- templates/wiki/view.html	2015-09-20 12:24:47 +0000
+++ templates/wiki/view.html	2016-06-28 17:58:37 +0000
@@ -1,29 +1,29 @@
 {% extends 'wiki/base.html' %}
 {% load i18n %}
-{% load wiki %}
+{% load wiki_extras %}
 
 {% block title %}
 {{ article.title }} - {{block.super}}
 {% endblock %}
 
 {% block extra_head %}
-<link rel="alternate" type="application/rss+xml" title="Wiki History for article {{ article.title }} (RSS)" href="{% url wiki_article_history_feed article.title "rss" %}" />
-<link rel="alternate" type="application/atom+xml" title="Wiki History for article {{ article.title }} (Atom)" href="{% url wiki_article_history_feed article.title "atom" %}" />
+<link rel="alternate" type="application/rss+xml" title="Wiki History for article {{ article.title }} (RSS)" href="{% url 'wiki_article_history_feed_rss' article.title %}" />
+<link rel="alternate" type="application/atom+xml" title="Wiki History for article {{ article.title }} (Atom)" href="{% url 'wiki_article_history_feed_atom' article.title %}" />
 {{ block.super}}
 {% endblock %}
 
 {% block content %}
 <div class="posRight small">
 	{% if article.id %}
-		<a class="invertedColor" href="{% url wiki_edit article.title %}">{% trans "Edit this article" %}</a>
+		<a class="invertedColor" href="{% url 'wiki_edit' article.title %}">{% trans "Edit this article" %}</a>
 		|
-		<a class="invertedColor" href="{% url wiki_article_history article.title %}">{% trans "Editing history" %}</a>
+		<a class="invertedColor" href="{% url 'wiki_article_history' article.title %}">{% trans "Editing history" %}</a>
 		{% if can_observe %}
 			| 
 			{% if is_observing %} 
-			<a class="invertedColor" href="{% url wiki_stop_observing article.title %}">{% trans "Stop observing" %}</a>
+			<a class="invertedColor" href="{% url 'wiki_stop_observing' article.title %}">{% trans "Stop observing" %}</a>
 			{% else %}
-			<a class="invertedColor" href="{% url wiki_observe article.title %}">{% trans "Observe" %}</a>
+			<a class="invertedColor" href="{% url 'wiki_observe' article.title %}">{% trans "Observe" %}</a>
 			{% endif %}
 		{% endif %}
 	{% endif %}
@@ -33,7 +33,7 @@
 	{% if not article.id %}
 		<p>
 			{% trans "This article does not exist." %}
-			<a href="{% url wiki_edit article.title %}">{% trans "Create it now?" %}</a>
+			<a href="{% url 'wiki_edit' article.title %}">{% trans "Create it now?" %}</a>
 		</p>
 	{% endif %}
 	{% if redirected_from %}

=== modified file 'templates/wlggz/view_ggz_highscore.html'
--- templates/wlggz/view_ggz_highscore.html	2010-10-30 12:17:49 +0000
+++ templates/wlggz/view_ggz_highscore.html	2016-06-28 17:58:37 +0000
@@ -4,7 +4,7 @@
 
 {% block content %}
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% include "django_messages/inlines/navigation.html" %}
 
@@ -22,7 +22,7 @@
         </tr>
         {% for userstat in ggzstats %}
         <tr>
-            <td class="{% cycle "odd" "even" %}"><a href="{% url wlggz_userstats userstat.handle %}">{{ userstat.handle }}</a></td>
+            <td class="{% cycle "odd" "even" %}"><a href="{% url 'wlggz_userstats' userstat.handle %}">{{ userstat.handle }}</a></td>
             <td class="{% cycle "odd" "even" %}"> {{ userstat.ranking|floatformat }} </td>
             <td class="{% cycle "odd" "even" %}"> {{ userstat.rating|floatformat }} </td>
             <td class="{% cycle "odd" "even" %}"> {{ userstat.wins|floatformat }} </td>

=== modified file 'templates/wlggz/view_ggz_matches.html'
--- templates/wlggz/view_ggz_matches.html	2010-10-30 12:17:49 +0000
+++ templates/wlggz/view_ggz_matches.html	2016-06-28 17:58:37 +0000
@@ -4,7 +4,7 @@
 
 {% block content %}
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% include "django_messages/inlines/navigation.html" %}
 

=== modified file 'templates/wlggz/view_ggz_overview.html'
--- templates/wlggz/view_ggz_overview.html	2010-10-30 12:17:49 +0000
+++ templates/wlggz/view_ggz_overview.html	2016-06-28 17:58:37 +0000
@@ -12,13 +12,13 @@
     <h3 class="title">{% trans "Available Links" %}</h3>
     <div class="info_line show_left">
         <br />
-        &nbsp;&nbsp;<a href="{% url wlggz_matches %}">Last matches</a><br />
-        &nbsp;&nbsp;<a href="{% url wlggz_ranking %}">GGZ ranking</a><br />
+        &nbsp;&nbsp;<a href="{% url 'wlggz_matches' %}">Last matches</a><br />
+        &nbsp;&nbsp;<a href="{% url 'wlggz_ranking' %}">GGZ ranking</a><br />
         <br />
         {% if user.is_authenticated %}
-        &nbsp;&nbsp;<a href="{% url wlggz_userstats %}">View your ggz statistics</a><br />
-        &nbsp;&nbsp;<a href="{% url wlggz_userinfo %}">About your ggz account</a><br />
-        &nbsp;&nbsp;<a href="{% url wlggz_changepw %}">Change your ggz password</a><br />
+        &nbsp;&nbsp;<a href="{% url 'wlggz_userstats' %}">View your ggz statistics</a><br />
+        &nbsp;&nbsp;<a href="{% url 'wlggz_userinfo' %}">About your ggz account</a><br />
+        &nbsp;&nbsp;<a href="{% url 'wlggz_changepw' %}">Change your ggz password</a><br />
         <br />
         {% endif %}
     </div>

=== modified file 'templates/wlggz/view_ggz_playerstats.html'
--- templates/wlggz/view_ggz_playerstats.html	2010-10-30 12:17:49 +0000
+++ templates/wlggz/view_ggz_playerstats.html	2016-06-28 17:58:37 +0000
@@ -4,7 +4,7 @@
 
 {% block content %}
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 
 {% include "django_messages/inlines/navigation.html" %}
 

=== modified file 'templates/wlhelp/building_details.html'
--- templates/wlhelp/building_details.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/building_details.html	2016-06-28 17:58:37 +0000
@@ -10,9 +10,9 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: {{ building.displayname }}</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
-	<a href="{% url wlhelp_buildings tribe.name %}">Buildings</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_buildings' tribe.name %}">Buildings</a> &#187;
 	{{ building.displayname }}
 	<br /><br />
 
@@ -29,7 +29,7 @@
 	<h2>Build Cost<h2>
 	{% for costs in building.get_build_cost %}
 		{% for w in costs %}
-		<a href="{% url wlhelp_ware_details w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+		<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
 		{% endfor %}
 	<br />
 	{% endfor %}
@@ -39,7 +39,7 @@
 	<h2>Produces</h2>
 	{% if building.produces and not building.trains %}
 		{% for w in building.get_ware_outputs %}
-		<a href="{% url wlhelp_ware_details w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+		<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
 		{% endfor %}
 	{% else %}
 		{% for wor in building.get_worker_outputs %}
@@ -52,7 +52,7 @@
 	<h2>Stores</h2>
 	{% for costs in building.get_stored_wares %}
 		{% for w in costs %}
-		<a href="{% url wlhelp_ware_details w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+		<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
 		{% endfor %}
 	<br />
 	{% endfor %}

=== modified file 'templates/wlhelp/buildings.html'
--- templates/wlhelp/buildings.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/buildings.html	2016-06-28 17:58:37 +0000
@@ -10,8 +10,8 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: Buildings</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
 	Buildings
 	<br /><br />
 

=== modified file 'templates/wlhelp/index.html'
--- templates/wlhelp/index.html	2016-02-28 16:44:42 +0000
+++ templates/wlhelp/index.html	2016-06-28 17:58:37 +0000
@@ -15,7 +15,7 @@
 <p>This is a list of all tribes in Widelands:</p>
 <ul>
 {% for tribe in tribes %}
-	<li><a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a></li>
+	<li><a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a></li>
 {% endfor %}
 </ul>
 </div>

=== modified file 'templates/wlhelp/inlines/display_buildings.html'
--- templates/wlhelp/inlines/display_buildings.html	2016-02-28 13:10:31 +0000
+++ templates/wlhelp/inlines/display_buildings.html	2016-06-28 17:58:37 +0000
@@ -1,3 +1,4 @@
+<<<<<<< TREE
 <table class="help">
 	<tr>
 		<th>Image</th>
@@ -47,3 +48,54 @@
 	</tr>
 {% endfor %}
 </table>
+=======
+<table class="help">
+	<tr>
+		<th>Image</th>
+		<th>Description</th>
+		<th>Build cost</th>
+		<th>Produces</th>
+		<th>Stores</th>
+	</tr>
+{% for b in buildings %}
+	<tr class="{% cycle "odd" "even" %}">
+		<td>
+			<a href="{% url 'wlhelp_building_details' b.tribe.name b.name %}" title="{{ b.displayname }}" id="{{ b.name }}">
+				{{ b.displayname }}
+				<br />
+				<img alt="{{b.displayname}}" src="{{ b.image_url }}" />
+			</a>
+		</td>
+		<td>{{ b.help }}</td>
+		<td>
+			{% for costs in b.get_build_cost %}
+				{% for w in costs %}
+				<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+				{% endfor %}
+			<br />
+			{% endfor %}
+		</td>
+		<td>
+			{% if b.produces and not b.trains %}
+				{% for w in b.get_ware_outputs %}
+				<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+				{% endfor %}
+			{% endif %}
+			{% if b.trains and not b.produces %}
+				{% for wor in b.get_worker_outputs %}
+				<img src="{{ wor.image_url }}" alt="{{ wor.name }}" />
+				{% endfor %}
+			{% endif %}
+		</td>
+		<td>
+			{% for costs in b.get_stored_wares %}
+				{% for w in costs %}
+				<a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}" title="{{w.displayname}}"><img src="{{ w.image_url }}" alt="{{ w.name }}" /></a>
+				{% endfor %}
+			<br />
+			{% endfor %}
+		</td>
+	</tr>
+{% endfor %}
+</table>
+>>>>>>> MERGE-SOURCE

=== modified file 'templates/wlhelp/tribe_details.html'
--- templates/wlhelp/tribe_details.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/tribe_details.html	2016-06-28 17:58:37 +0000
@@ -10,15 +10,15 @@
 {% block content %}
 <h1>{{ tribe.displayname }}</h1>
 <div class="blogEntry">
-<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
+<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
 {{ tribe.displayname }}
 <br /><br />
 <img class="posLeft icon" src="{{ tribe.icon_url }}" alt="" />
 <p>{{ tribe.descr }}</p>
 <ul>
-	<li><a href="{% url wlhelp_buildings tribe.name %}">Buildings</a></li>
-	<li><a href="{% url wlhelp_wares tribe.name %}">Wares</a></li>
-	<li><a href="{% url wlhelp_workers tribe.name %}">Workers</a></li>
+	<li><a href="{% url 'wlhelp_buildings' tribe.name %}">Buildings</a></li>
+	<li><a href="{% url 'wlhelp_wares' tribe.name %}">Wares</a></li>
+	<li><a href="{% url 'wlhelp_workers' tribe.name %}">Workers</a></li>
 	<li><a href="{{ tribe.network_pdf_url }}" target="_blank">Economy Network as PDF</a></li>
 	<li><a href="{{ tribe.network_gif_url }}" target="_blank">Economy Network as GIF</a></li>
 </ul>

=== modified file 'templates/wlhelp/ware_details.html'
--- templates/wlhelp/ware_details.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/ware_details.html	2016-06-28 17:58:37 +0000
@@ -10,9 +10,9 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: {{ ware.displayname }}</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
-	<a href="{% url wlhelp_wares tribe.name %}">Wares</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_wares' tribe.name %}">Wares</a> &#187;
 	{{ ware.displayname }}
 	<br /><br />
 

=== modified file 'templates/wlhelp/wares.html'
--- templates/wlhelp/wares.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/wares.html	2016-06-28 17:58:37 +0000
@@ -10,8 +10,8 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: Wares</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
 	Wares
 	<br /><br />
 
@@ -24,11 +24,11 @@
 	{% for ware in wares %}
 		<tr class="{% cycle "odd" "even" %}">
 			<td>
-				<a href="{% url wlhelp_ware_details tribe.name ware.name %}">
+				<a href="{% url 'wlhelp_ware_details' tribe.name ware.name %}">
 					<img src="{{ ware.image_url }}"  alt="{{ ware.name }}" />
 				</a>
 			</td>
-			<td><a id="{{ ware.name }}" href="{% url wlhelp_ware_details tribe.name ware.name %}">{{ ware.displayname }}</a></td>
+			<td><a id="{{ ware.name }}" href="{% url 'wlhelp_ware_details' tribe.name ware.name %}">{{ ware.displayname }}</a></td>
 			<td>{{ ware.help }}</td> 
 		</tr>
 	{% endfor %}

=== modified file 'templates/wlhelp/worker_details.html'
--- templates/wlhelp/worker_details.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/worker_details.html	2016-06-28 17:58:37 +0000
@@ -10,9 +10,9 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: {{ worker.displayname }}</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
-	<a href="{% url wlhelp_workers tribe.name %}">Workers</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_workers' tribe.name %}">Workers</a> &#187;
 	{{ worker.displayname }}
 	<br /><br />
 

=== modified file 'templates/wlhelp/workers.html'
--- templates/wlhelp/workers.html	2012-05-19 19:55:15 +0000
+++ templates/wlhelp/workers.html	2016-06-28 17:58:37 +0000
@@ -10,8 +10,8 @@
 {% block content %}
 <h1>{{ tribe.displayname }}: Workers</h1>
 <div class="blogEntry">
-	<a href="{% url wlhelp_index %}">Encyclopedia</a> &#187;
-	<a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> &#187;
+	<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> &#187;
+	<a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> &#187;
 	Workers
 	<br /><br />
 
@@ -24,11 +24,11 @@
 	{% for worker in workers %}
 		<tr class="{% cycle "odd" "even" %}">
 			<td>
-				<a href="{% url wlhelp_worker_details tribe.name worker.name %}">
+				<a href="{% url 'wlhelp_worker_details' tribe.name worker.name %}">
 					<img src="{{ worker.image_url }}"  alt="{{ worker.name }}" />
 				</a>
 			</td>
-			<td><a id="{{ worker.name }}" href="{% url wlhelp_worker_details tribe.name worker.name %}">{{ worker.displayname }}</a></td>
+			<td><a id="{{ worker.name }}" href="{% url 'wlhelp_worker_details' tribe.name worker.name %}">{{ worker.displayname }}</a></td>
 			<td>{{ worker.help }}</td> 
 		</tr>
 	{% endfor %}

=== modified file 'templates/wlmaps/edit_comment.html'
--- templates/wlmaps/edit_comment.html	2016-02-15 14:06:09 +0000
+++ templates/wlmaps/edit_comment.html	2016-06-28 17:58:37 +0000
@@ -3,9 +3,9 @@
 {% block content %} 
 <h1>Edit comment: {{ map.name }}</h1>
 <div class="blogEntry">
-    <form enctype="multipart/form-data" action="{% url wlmaps_edit_comment map.slug %}" method="post">
+    <form enctype="multipart/form-data" action="{% url 'wlmaps_edit_comment' map.slug %}" method="post">
         <div>
-        {{ form.uploader_comment.label_tag }}:
+        {{ form.uploader_comment.label_tag }}
 			<span class="posRight">
 				<a href="/wiki/WikiSyntax" title="Opens new Tab/Window" target="_blank">
 					<img src="{{ MEDIA_URL }}img/menu_help.png" alt="Help on Syntax" class="middle">

=== modified file 'templates/wlmaps/index.html'
--- templates/wlmaps/index.html	2015-04-01 20:01:41 +0000
+++ templates/wlmaps/index.html	2016-06-28 17:58:37 +0000
@@ -4,13 +4,13 @@
 {% endcomment %}
 
 {% load custom_date %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load wlmaps_extra %}
 {% load threadedcommentstags %}
 {% load pagination_tags %}
 
 {% block content %}
-<a href="{% url wlmaps_upload %}" class="posRight invertedColor small">Upload a new map</a>
+<a href="{% url 'wlmaps_upload' %}" class="posRight invertedColor small">Upload a new map</a>
 <h1>Maps</h1>
 <div class="blogEntry">
 	<p>
@@ -63,7 +63,7 @@
 						<td class="grey">Downloads:</td><td>{{ map.nr_downloads }}</td>
 						<td class="spacer"></td>
 						<td colspan="2">
-							<a class="button" href="{% url wlmaps_download map.slug %}">
+							<a class="button" href="{% url 'wlmaps_download' map.slug %}">
 								<img src="{{ MEDIA_URL }}img/download.png" alt ="" class="middle" />
 								<span class="middle">Direct Download</span>
 							</a>

=== modified file 'templates/wlmaps/map_detail.html'
--- templates/wlmaps/map_detail.html	2016-02-16 13:52:10 +0000
+++ templates/wlmaps/map_detail.html	2016-06-28 17:58:37 +0000
@@ -5,7 +5,7 @@
 
 {% load custom_date %}
 {% load wlmaps_extra %}
-{% load wlprofile %}
+{% load wlprofile_extras %}
 {% load threadedcommentstags %}
 {% load wl_markdown %}
 
@@ -24,7 +24,7 @@
         maxScore: 10,
         messages: ["","","","","","","","","",""],
         fn: function(e, score) {
-            $.post("{% url wlmaps_rate map.slug %}",{ vote: score });
+            $.post("{% url 'wlmaps_rate' map.slug %}",{ vote: score });
             }
         });
 });
@@ -36,25 +36,25 @@
 <h1>Map: {{ map.name }}</h1>
 <div class="blogEntry" style="padding-bottom: 3em">
 	<div>
-		<a href="{% url wlmaps_index %}">Maps</a> &#187; {{ map.name }}
+		<a href="{% url 'wlmaps_index' %}">Maps</a> &#187; {{ map.name }}
 	</div>
 		<img class="posLeft map" style="float: left" src="{{ MEDIA_URL }}{{ map.minimap.url }}" alt="{{ map.name }}" />
 	<div>
 		<h3>Description:</h3>
-		<p>{{ map.descr|wl_markdown:"escape" }}</p>
+		<p>{{ map.descr|wl_markdown:"bleachit" }}</p>
 	</div>
 	{% if map.hint %}
 	<div style="clear: left;">
 		<h3>Hint:</h3>
-		<p>{{ map.hint|wl_markdown:"escape" }}</p>
+		<p>{{ map.hint|wl_markdown:"bleachit" }}</p>
 	</div>
 	{% endif %}
 
 	<div style="clear: left;">
 		<h3>Comment by uploader:</h3>
-		<div>{{ map.uploader_comment|wl_markdown:"remove" }}</div>
+		<div>{{ map.uploader_comment|wl_markdown:"bleachit" }}</div>
 		{% if user == map.uploader %}
-			<a class="button posLeft" href="{% url wlmaps_edit_comment map.slug %}">
+			<a class="button posLeft" href="{% url 'wlmaps_edit_comment' map.slug %}">
 				<img alt="Edit" title="Edit your comment" class="middle" src="{{ MEDIA_URL }}forum/img/edit.png">
 				<span class="middle">Edit</span>
 			</a>
@@ -114,7 +114,7 @@
 	</div>
 	
 	<div style="margin: 1em 0px 1em 0px">
-	<a class="button posLeft" href="{% url wlmaps_download map.slug %}">
+	<a class="button posLeft" href="{% url 'wlmaps_download' map.slug %}">
 		<img src="{{ MEDIA_URL }}img/download.png" alt ="" class="middle" />
 		<span class="middle">Download this map</span>
 	</a>

=== modified file 'templates/wlmaps/upload.html'
--- templates/wlmaps/upload.html	2016-02-09 18:05:18 +0000
+++ templates/wlmaps/upload.html	2016-06-28 17:58:37 +0000
@@ -9,15 +9,15 @@
 <h1>Map Upload</h1>
 <div class="blogEntry">
 	<div class="breadCrumb">
-		<a href="{% url wlmaps_index %}">Maps</a> &#187; Upload
+		<a href="{% url 'wlmaps_index' %}">Maps</a> &#187; Upload
 	</div>
-	<form enctype="multipart/form-data" action="{% url wlmaps_upload %}" method="post">
-		{{ form.file.label_tag }}: {{ form.file }}<br />
+	<form enctype="multipart/form-data" action="{% url 'wlmaps_upload' %}" method="post">
+		{{ form.file.label_tag }} {{ form.file }}<br />
 		{% if form.file.errors %}
 			<span class="errormessage">{{ form.file.errors }}</span><br />
 		{% endif %}
 		<div>
-		{{ form.uploader_comment.label_tag }}:
+		{{ form.uploader_comment.label_tag }}
 			<span class="posRight">
 				<a href="/wiki/WikiSyntax" title="Opens new Tab/Window" target="_blank">
 					<img src="{{ MEDIA_URL }}img/menu_help.png" alt="Help on Syntax" class="middle">

=== modified file 'templates/wlpoll/poll_detail.html'
--- templates/wlpoll/poll_detail.html	2015-09-20 12:24:47 +0000
+++ templates/wlpoll/poll_detail.html	2016-06-28 17:58:37 +0000
@@ -3,7 +3,8 @@
    vim:ft=htmldjango
 {% endcomment %}
 
-{% load wlpoll wlprofile %}
+{% load wlprofile_extras wlpoll_extras %}
+{% load comments %}
 {% load threadedcommentstags custom_date %}
 
 {% block title %}{{ object.name }} - {{ block.super }}{% endblock %}
@@ -11,9 +12,9 @@
 {% block content %}
 	{% if perms.wlpoll %}
 	<div class="small posRight">
-		{% if perms.wlpoll.poll_can_add %}<a href="/admin/wlpoll/poll/add/" class="invertedColor">Add New Poll</a>{% endif %}
-		{% if perms.wlpoll.poll_can_edit %}| <a href="/admin/wlpoll/poll/{{object.id}}/" class="invertedColor">Edit</a>{% endif %}
-		{% if perms.wlpoll.poll_can_delete %}| <a href="/admin/wlpoll/poll/{{object.id}}/delete/" class="invertedColor">Delete</a>{% endif %}
+		{% if perms.wlpoll.add_poll %}<a href="/admin/wlpoll/poll/add/" class="invertedColor">Add New Poll</a>{% endif %}
+		{% if perms.wlpoll.change_poll %}| <a href="/admin/wlpoll/poll/{{object.id}}/" class="invertedColor">Edit</a>{% endif %}
+		{% if perms.wlpoll.delete_poll %}| <a href="/admin/wlpoll/poll/{{object.id}}/delete/" class="invertedColor">Delete</a>{% endif %}
 	</div>
 	{% endif %}
 	<h1>Poll: {{ object.name }}</h1>

=== modified file 'templates/wlpoll/poll_list.html'
--- templates/wlpoll/poll_list.html	2012-04-02 09:41:12 +0000
+++ templates/wlpoll/poll_list.html	2016-06-28 17:58:37 +0000
@@ -3,26 +3,43 @@
    vim:ft=htmldjango
 {% endcomment %}
 
-{% load wlpoll wlprofile %}
+{% block extra_head %}
+{{ block.super}}
+<link rel="stylesheet" type="text/css" media="all" href="{{ MEDIA_URL }}css/wiki.css" />
+{% endblock %}
+
 {% load threadedcommentstags custom_date %}
-
 {% block title %}Poll Archive - {{ block.super }}{% endblock %}
 
 {% block content %}
-{% if perms.wlpoll %}
+    {% if perms.wlpoll %}
 	<div class="small posRight">
-		{% if perms.wlpoll.poll_can_add %}<a href="/admin/wlpoll/poll/add/" class="invertedColor">Add New Poll</a>{% endif %}
+		{% if perms.wlpoll.add_poll %}<a href="/admin/wlpoll/poll/add/" class="invertedColor">Add New Poll</a>{% endif %}
 	</div>
-	{% endif %}
-<h1>Poll Archive</h1>
-<div class="blogEntry">
-	<ul>
-	{% for o in object_list %}
-	{% get_comment_count for o as ccount %}
-		<li>
-			<a href="{{o.get_absolute_url}}">{{ o.name }}</a> - <span class="small">posted at {{ o.pub_date|custom_date:user }}, {{ ccount }} comments, {{o.total_votes}} votes</span>
-		</li>
-	{% endfor %}
-	</ul>
-</div>
+    {% endif %}
+    <h1>Poll Archive</h1>
+    <div class="blogEntry">
+        <div class="post">
+        <table>
+            <tr>
+                <th>Poll</th>
+                <th>Begin</th>
+                <th>End</th>
+                <th>Votes</th>
+                <th>Comments</th>
+            </tr>
+            
+            {% for o in object_list %}
+            {% get_comment_count for o as ccount %}
+        	<tr>
+                <td><a href="{{o.get_absolute_url}}">{{ o.name }}</a></td>
+                <td>{{ o.pub_date|custom_date:user }}</td>
+                <td>{{ o.closed_date|custom_date:user }}</td>
+                <td class="center">{{o.total_votes}}</td>
+                <td class="right">{{ ccount }}</td>
+            </tr>
+            {% endfor %}
+        </table>
+        </div>
+    </div>
 {% endblock %}

=== modified file 'templates/wlprofile/edit_profile.html'
--- templates/wlprofile/edit_profile.html	2012-05-08 21:52:15 +0000
+++ templates/wlprofile/edit_profile.html	2016-06-28 17:58:37 +0000
@@ -15,7 +15,7 @@
 		{% for field in profile_form %}
 			<tr>
 				<td class="grey">
-					{{ field.label_tag }}:
+					{{ field.label_tag }}
 				</td>
 				<td>
 					{% ifequal field.name "avatar"%}
@@ -44,12 +44,12 @@
 	<br />
 	<br />
 	<p>
-		<a href="{% url auth_password_change %}">Change website password</a>
+		<a href="{% url 'auth_password_change' %}">Change website password</a>
 		<br />
 		You will be redirected to an encrypted connection. The website password is <strong>not</strong> transmitted in cleartext.
 	</p>
 	<p>
-		<a href="{% url wlggz_changepw %}">Change online gaming password</a>
+		<a href="{% url 'wlggz_changepw' %}">Change online gaming password</a>
 		<br />
 		<strong class="errormessage">WARNING: The online gaming password is transmitted in cleartext. Do not use your website password!</strong>
 	</p>

=== modified file 'templates/wlprofile/view_profile.html'
--- templates/wlprofile/view_profile.html	2015-02-18 22:30:08 +0000
+++ templates/wlprofile/view_profile.html	2016-06-28 17:58:37 +0000
@@ -10,7 +10,7 @@
 {% block content %}
 <div class="posRight small">
 	{% ifequal user profile.user %}
-		<a class="invertedColor" href="{% url profile_edit %}">Edit Profile</a>
+		<a class="invertedColor" href="{% url 'profile_edit' %}">Edit Profile</a>
 	{% endifequal %}
 </div>
 <h1>{{ profile.user.username }}'s Profile</h1>
@@ -25,7 +25,7 @@
 			</td>
 			<td>
 			{% ifnotequal user profile.user %}
-				<button onclick="window.location.href='{% url messages_compose_to profile.user %}';">
+				<button onclick="window.location.href='{% url 'messages_compose_to' profile.user %}';">
 					<img src="{{ MEDIA_URL }}forum/img/send_pm.png" alt ="" class="middle" />
 					<span class="middle">{% trans "Send PM" %}</span>
 				</button>
@@ -84,7 +84,7 @@
 <table class="bottom_line" width="100%">
 	<tr>
 		<td>
-    			<a href="{% url wlggz_userstats profile.user %}">View GGZ Statistics of this user</a>
+    			<a href="{% url 'wlggz_userstats' profile.user %}">View GGZ Statistics of this user</a>
 		</td>
 	</tr>
 </table>

=== modified file 'templates/wlsearch/search.html'
--- templates/wlsearch/search.html	2015-02-18 22:30:08 +0000
+++ templates/wlsearch/search.html	2016-06-28 17:58:37 +0000
@@ -50,7 +50,7 @@
 	<h4>Buildings</h4>
 	<ul>
 	{% for b in wlhelp_results_buildings %}
-		<li><a href="{% url wlhelp_building_details b.tribe.name b.name %}">{{ b.tribe.displayname }} &raquo; {{b.displayname}}</a></li>
+		<li><a href="{% url 'wlhelp_building_details' b.tribe.name b.name %}">{{ b.tribe.displayname }} &raquo; {{b.displayname}}</a></li>
 	{% endfor %}
 	</ul>
 	{% endif %}
@@ -59,7 +59,7 @@
 	<h4>Wares</h4>
 	<ul>
 	{% for w in wlhelp_results_wares %}
-		<li><a href="{% url wlhelp_ware_details w.tribe.name w.name %}">{{ w.tribe.displayname }} &raquo; {{w.displayname}}</a></li>
+		<li><a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}">{{ w.tribe.displayname }} &raquo; {{w.displayname}}</a></li>
 	{% endfor %}
 	</ul>
 	{% endif %}

=== added directory 'threadedcomments'
=== added file 'threadedcomments/LICENSE.txt'
--- threadedcomments/LICENSE.txt	1970-01-01 00:00:00 +0000
+++ threadedcomments/LICENSE.txt	2016-06-28 17:58:37 +0000
@@ -0,0 +1,28 @@
+Copyright (c) 2009, Eric Florenzano and Thejaswi Puthraya
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of the author nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file

=== added file 'threadedcomments/__init__.py'
=== added file 'threadedcomments/admin.py'
--- threadedcomments/admin.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/admin.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,31 @@
+from django.contrib import admin
+from django.utils.translation import ugettext_lazy as _
+from threadedcomments.models import ThreadedComment, FreeThreadedComment
+
+class ThreadedCommentAdmin(admin.ModelAdmin):
+    fieldsets = (
+        (None, {'fields': ('content_type', 'object_id')}),
+        (_('Parent'), {'fields' : ('parent',)}),
+        (_('Content'), {'fields': ('user', 'comment')}),
+        (_('Meta'), {'fields': ('is_public', 'date_submitted', 'date_modified', 'date_approved', 'is_approved', 'ip_address')}),
+    )
+    list_display = ('user', 'date_submitted', 'content_type', 'get_content_object', 'parent', '__unicode__')
+    list_filter = ('date_submitted',)
+    date_hierarchy = 'date_submitted'
+    search_fields = ('comment', 'user__username')
+
+class FreeThreadedCommentAdmin(admin.ModelAdmin):
+    fieldsets = (
+        (None, {'fields': ('content_type', 'object_id')}),
+        (_('Parent'), {'fields' : ('parent',)}),
+        (_('Content'), {'fields': ('name', 'website', 'email', 'comment')}),
+        (_('Meta'), {'fields': ('date_submitted', 'date_modified', 'date_approved', 'is_public', 'ip_address', 'is_approved')}),
+    )
+    list_display = ('name', 'date_submitted', 'content_type', 'get_content_object', 'parent', '__unicode__')
+    list_filter = ('date_submitted',)
+    date_hierarchy = 'date_submitted'
+    search_fields = ('comment', 'name', 'email', 'website')
+
+
+admin.site.register(ThreadedComment, ThreadedCommentAdmin)
+admin.site.register(FreeThreadedComment, FreeThreadedCommentAdmin)

=== added file 'threadedcomments/forms.py'
--- threadedcomments/forms.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/forms.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,41 @@
+from django import forms
+from threadedcomments.models import DEFAULT_MAX_COMMENT_LENGTH
+from threadedcomments.models import FreeThreadedComment, ThreadedComment
+from django.utils.translation import ugettext_lazy as _
+
+class ThreadedCommentForm(forms.ModelForm):
+    """
+    Form which can be used to validate data for a new ThreadedComment.
+    It consists of just two fields: ``comment``, and ``markup``.
+    
+    The ``comment`` field is the only one which is required.
+    """
+
+    comment = forms.CharField(
+        label = _('comment'),
+        max_length = DEFAULT_MAX_COMMENT_LENGTH,
+        widget = forms.Textarea
+    )
+
+    class Meta:
+        model = ThreadedComment
+        fields = ('comment', 'markup')
+
+class FreeThreadedCommentForm(forms.ModelForm):
+    """
+    Form which can be used to validate data for a new FreeThreadedComment.
+    It consists of just a few fields: ``comment``, ``name``, ``website``,
+    ``email``, and ``markup``.
+    
+    The fields ``comment``, and ``name`` are the only ones which are required.
+    """
+
+    comment = forms.CharField(
+        label = _('comment'),
+        max_length = DEFAULT_MAX_COMMENT_LENGTH,
+        widget = forms.Textarea
+    )
+
+    class Meta:
+        model = FreeThreadedComment
+        fields = ('comment', 'name', 'website', 'email', 'markup')
\ No newline at end of file

=== added directory 'threadedcomments/management'
=== added file 'threadedcomments/management/__init__.py'
=== added directory 'threadedcomments/management/commands'
=== added file 'threadedcomments/management/commands/__init__.py'
=== added file 'threadedcomments/management/commands/migratecomments.py'
--- threadedcomments/management/commands/migratecomments.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/management/commands/migratecomments.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,59 @@
+from django.core.management.base import BaseCommand
+from django.contrib.comments.models import Comment, FreeComment
+from threadedcomments.models import ThreadedComment, FreeThreadedComment
+
+class Command(BaseCommand):
+    help = "Migrates Django's built-in django.contrib.comments data to threadedcomments data"
+    
+    output_transaction = True
+    
+    def handle(self, *args, **options):
+        """
+        Converts all legacy ``Comment`` and ``FreeComment`` objects into 
+        ``ThreadedComment`` and ``FreeThreadedComment`` objects, respectively.
+        """
+        self.handle_free_comments()
+        self.handle_comments()
+    
+    def handle_free_comments(self):
+        """
+        Converts all legacy ``FreeComment`` objects into ``FreeThreadedComment``
+        objects.
+        """
+        comments = FreeComment.objects.all()
+        for c in comments:
+            new = FreeThreadedComment(
+                content_type = c.content_type,
+                object_id = c.object_id,
+                comment = c.comment,
+                name = c.person_name,
+                website = '',
+                email = '',
+                date_submitted = c.submit_date,
+                date_modified = c.submit_date,
+                date_approved = c.submit_date,
+                is_public = c.is_public,
+                ip_address = c.ip_address,
+                is_approved = c.approved
+            )
+            new.save()
+    
+    def handle_comments(self):
+        """
+        Converts all legacy ``Comment`` objects into ``ThreadedComment`` objects.
+        """
+        comments = Comment.objects.all()
+        for c in comments:
+            new = ThreadedComment(
+                content_type = c.content_type,
+                object_id = c.object_id,
+                comment = c.comment,
+                user = c.user,
+                date_submitted = c.submit_date,
+                date_modified = c.submit_date,
+                date_approved = c.submit_date,
+                is_public = c.is_public,
+                ip_address = c.ip_address,
+                is_approved = not c.is_removed
+            )
+            new.save()
\ No newline at end of file

=== added directory 'threadedcomments/migrations'
=== added file 'threadedcomments/migrations/0001_initial.py'
--- threadedcomments/migrations/0001_initial.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/migrations/0001_initial.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+import datetime
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contenttypes', '0002_remove_content_type_name'),
+        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='FreeThreadedComment',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField(verbose_name='object ID')),
+                ('name', models.CharField(max_length=128, verbose_name='name')),
+                ('website', models.URLField(verbose_name='site', blank=True)),
+                ('email', models.EmailField(max_length=254, verbose_name='e-mail address', blank=True)),
+                ('date_submitted', models.DateTimeField(default=datetime.datetime.now, verbose_name='date/time submitted')),
+                ('date_modified', models.DateTimeField(default=datetime.datetime.now, verbose_name='date/time modified')),
+                ('date_approved', models.DateTimeField(default=None, null=True, verbose_name='date/time approved', blank=True)),
+                ('comment', models.TextField(verbose_name='comment')),
+                ('markup', models.IntegerField(default=b'markdown', null=True, blank=True, choices=[(1, 'markdown'), (2, 'textile'), (3, 'restructuredtext'), (5, 'plaintext')])),
+                ('is_public', models.BooleanField(default=True, verbose_name='is public')),
+                ('is_approved', models.BooleanField(default=False, verbose_name='is approved')),
+                ('ip_address', models.GenericIPAddressField(null=True, verbose_name='IP address', blank=True)),
+                ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+                ('parent', models.ForeignKey(related_name='children', default=None, blank=True, to='threadedcomments.FreeThreadedComment', null=True)),
+            ],
+            options={
+                'ordering': ('-date_submitted',),
+                'get_latest_by': 'date_submitted',
+                'verbose_name': 'Free Threaded Comment',
+                'verbose_name_plural': 'Free Threaded Comments',
+            },
+        ),
+        migrations.CreateModel(
+            name='TestModel',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('name', models.CharField(max_length=5)),
+                ('is_public', models.BooleanField(default=True)),
+                ('date', models.DateTimeField(default=datetime.datetime.now)),
+            ],
+        ),
+        migrations.CreateModel(
+            name='ThreadedComment',
+            fields=[
+                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
+                ('object_id', models.PositiveIntegerField(verbose_name='object ID')),
+                ('date_submitted', models.DateTimeField(default=datetime.datetime.now, verbose_name='date/time submitted')),
+                ('date_modified', models.DateTimeField(default=datetime.datetime.now, verbose_name='date/time modified')),
+                ('date_approved', models.DateTimeField(default=None, null=True, verbose_name='date/time approved', blank=True)),
+                ('comment', models.TextField(verbose_name='comment')),
+                ('markup', models.IntegerField(default=b'markdown', null=True, blank=True, choices=[(1, 'markdown'), (2, 'textile'), (3, 'restructuredtext'), (5, 'plaintext')])),
+                ('is_public', models.BooleanField(default=True, verbose_name='is public')),
+                ('is_approved', models.BooleanField(default=False, verbose_name='is approved')),
+                ('ip_address', models.GenericIPAddressField(null=True, verbose_name='IP address', blank=True)),
+                ('content_type', models.ForeignKey(to='contenttypes.ContentType')),
+                ('parent', models.ForeignKey(related_name='children', default=None, blank=True, to='threadedcomments.ThreadedComment', null=True)),
+                ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
+            ],
+            options={
+                'ordering': ('-date_submitted',),
+                'get_latest_by': 'date_submitted',
+                'verbose_name': 'Threaded Comment',
+                'verbose_name_plural': 'Threaded Comments',
+            },
+        ),
+    ]

=== added file 'threadedcomments/migrations/__init__.py'
=== added file 'threadedcomments/models.py'
--- threadedcomments/models.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/models.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,348 @@
+from django.db import models
+from django.contrib.contenttypes.models import ContentType
+#from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.contrib.auth.models import User
+from datetime import datetime
+from django.db.models import Q
+from django.utils.translation import ugettext_lazy as _
+from django.conf import settings
+from django.utils.encoding import force_unicode
+
+DEFAULT_MAX_COMMENT_LENGTH = getattr(settings, 'DEFAULT_MAX_COMMENT_LENGTH', 1000)
+DEFAULT_MAX_COMMENT_DEPTH = getattr(settings, 'DEFAULT_MAX_COMMENT_DEPTH', 8)
+
+MARKDOWN = 1
+TEXTILE = 2
+REST = 3
+#HTML = 4
+PLAINTEXT = 5
+MARKUP_CHOICES = (
+    (MARKDOWN, _("markdown")),
+    (TEXTILE, _("textile")),
+    (REST, _("restructuredtext")),
+#    (HTML, _("html")),
+    (PLAINTEXT, _("plaintext")),
+)
+
+DEFAULT_MARKUP = getattr(settings, 'DEFAULT_MARKUP', PLAINTEXT)
+
+def dfs(node, all_nodes, depth):
+    """
+    Performs a recursive depth-first search starting at ``node``.  This function
+    also annotates an attribute, ``depth``, which is an integer that represents
+    how deeply nested this node is away from the original object.
+    """
+    node.depth = depth
+    to_return = [node,]
+    for subnode in all_nodes:
+        if subnode.parent and subnode.parent.id == node.id:
+            to_return.extend(dfs(subnode, all_nodes, depth+1))
+    return to_return
+
+class ThreadedCommentManager(models.Manager):
+    """
+    A ``Manager`` which will be attached to each comment model.  It helps to facilitate
+    the retrieval of comments in tree form and also has utility methods for
+    creating and retrieving objects related to a specific content object.
+    """
+    def get_tree(self, content_object, root=None):
+        """
+        Runs a depth-first search on all comments related to the given content_object.
+        This depth-first search adds a ``depth`` attribute to the comment which
+        signifies how how deeply nested the comment is away from the original object.
+        
+        If root is specified, it will start the tree from that comment's ID.
+        
+        Ideally, one would use this ``depth`` attribute in the display of the comment to
+        offset that comment by some specified length.
+        
+        The following is a (VERY) simple example of how the depth property might be used in a template:
+        
+            {% for comment in comment_tree %}
+                <p style="margin-left: {{ comment.depth }}em">{{ comment.comment }}</p>
+            {% endfor %}
+        """
+        content_type = ContentType.objects.get_for_model(content_object)
+        children = list(self.get_query_set().filter(
+            content_type = content_type,
+            object_id = getattr(content_object, 'pk', getattr(content_object, 'id')),
+        ).select_related().order_by('date_submitted'))
+        to_return = []
+        if root:
+            if isinstance(root, int):
+                root_id = root
+            else:
+                root_id = root.id
+            to_return = [c for c in children if c.id == root_id]
+            if to_return:
+                to_return[0].depth = 0
+                for child in children:
+                    if child.parent_id == root_id:
+                        to_return.extend(dfs(child, children, 1))
+        else:
+            for child in children:
+                if not child.parent:
+                    to_return.extend(dfs(child, children, 0))
+        return to_return
+
+    def _generate_object_kwarg_dict(self, content_object, **kwargs):
+        """
+        Generates the most comment keyword arguments for a given ``content_object``.
+        """
+        kwargs['content_type'] = ContentType.objects.get_for_model(content_object)
+        kwargs['object_id'] = getattr(content_object, 'pk', getattr(content_object, 'id'))
+        return kwargs
+
+    def create_for_object(self, content_object, **kwargs):
+        """
+        A simple wrapper around ``create`` for a given ``content_object``.
+        """
+        return self.create(**self._generate_object_kwarg_dict(content_object, **kwargs))
+    
+    def get_or_create_for_object(self, content_object, **kwargs):
+        """
+        A simple wrapper around ``get_or_create`` for a given ``content_object``.
+        """
+        return self.get_or_create(**self._generate_object_kwarg_dict(content_object, **kwargs))
+    
+    def get_for_object(self, content_object, **kwargs):
+        """
+        A simple wrapper around ``get`` for a given ``content_object``.
+        """
+        return self.get(**self._generate_object_kwarg_dict(content_object, **kwargs))
+
+    def all_for_object(self, content_object, **kwargs):
+        """
+        Prepopulates a QuerySet with all comments related to the given ``content_object``.
+        """
+        return self.filter(**self._generate_object_kwarg_dict(content_object, **kwargs))
+
+class PublicThreadedCommentManager(ThreadedCommentManager):
+    """
+    A ``Manager`` which borrows all of the same methods from ``ThreadedCommentManager``,
+    but which also restricts the queryset to only the published methods 
+    (in other words, ``is_public = True``).
+    """
+    def get_query_set(self):
+        return super(ThreadedCommentManager, self).get_queryset().filter(
+            Q(is_public = True) | Q(is_approved = True)
+        )
+
+class ThreadedComment(models.Model):
+    """
+    A threaded comment which must be associated with an instance of 
+    ``django.contrib.auth.models.User``.  It is given its hierarchy by
+    a nullable relationship back on itself named ``parent``.
+    
+    This ``ThreadedComment`` supports several kinds of markup languages,
+    including Textile, Markdown, and ReST.
+    
+    It also includes two Managers: ``objects``, which is the same as the normal
+    ``objects`` Manager with a few added utility functions (see above), and
+    ``public``, which has those same utility functions but limits the QuerySet to
+    only those values which are designated as public (``is_public=True``).
+    """
+    # Generic Foreign Key Fields
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField(_('object ID'))
+    content_object = GenericForeignKey()
+    
+    # Hierarchy Field
+    parent = models.ForeignKey('self', null=True, blank=True, default=None, related_name='children')
+    
+    # User Field
+    user = models.ForeignKey(User)
+    
+    # Date Fields
+    date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
+    date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
+    date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
+    
+    # Meat n' Potatoes
+    comment = models.TextField(_('comment'))
+    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
+    
+    # Status Fields
+    is_public = models.BooleanField(_('is public'), default = True)
+    is_approved = models.BooleanField(_('is approved'), default = False)
+    
+    # Extra Field
+    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
+    
+    objects = ThreadedCommentManager()
+    public = PublicThreadedCommentManager()
+    
+    def __unicode__(self):
+        if len(self.comment) > 50:
+            return self.comment[:50] + "..."
+        return self.comment[:50]
+    
+    def save(self, **kwargs):
+        if not self.markup:
+            self.markup = DEFAULT_MARKUP
+        self.date_modified = datetime.now()
+        if not self.date_approved and self.is_approved:
+            self.date_approved = datetime.now()
+        super(ThreadedComment, self).save(**kwargs)
+    
+    def get_content_object(self):
+        """
+        Wrapper around the GenericForeignKey due to compatibility reasons
+        and due to ``list_display`` limitations.
+        """
+        return self.content_object
+    
+    def get_base_data(self, show_dates=True):
+        """
+        Outputs a Python dictionary representing the most useful bits of
+        information about this particular object instance.
+        
+        This is mostly useful for testing purposes, as the output from the
+        serializer changes from run to run.  However, this may end up being
+        useful for JSON and/or XML data exchange going forward and as the
+        serializer system is changed.
+        """
+        markup = "plaintext"
+        for markup_choice in MARKUP_CHOICES:
+            if self.markup == markup_choice[0]:
+                markup = markup_choice[1]
+                break
+        to_return = {
+            'content_object' : self.content_object,
+            'parent' : self.parent,
+            'user' : self.user,
+            'comment' : self.comment,
+            'is_public' : self.is_public,
+            'is_approved' : self.is_approved,
+            'ip_address' : self.ip_address,
+            'markup' : force_unicode(markup),
+        }
+        if show_dates:
+            to_return['date_submitted'] = self.date_submitted
+            to_return['date_modified'] = self.date_modified
+            to_return['date_approved'] = self.date_approved
+        return to_return
+    
+    class Meta:
+        ordering = ('-date_submitted',)
+        verbose_name = _("Threaded Comment")
+        verbose_name_plural = _("Threaded Comments")
+        get_latest_by = "date_submitted"
+
+    
+class FreeThreadedComment(models.Model):
+    """
+    A threaded comment which need not be associated with an instance of 
+    ``django.contrib.auth.models.User``.  Instead, it requires minimally a name,
+    and maximally a name, website, and e-mail address.  It is given its hierarchy
+    by a nullable relationship back on itself named ``parent``.
+    
+    This ``FreeThreadedComment`` supports several kinds of markup languages,
+    including Textile, Markdown, and ReST.
+    
+    It also includes two Managers: ``objects``, which is the same as the normal
+    ``objects`` Manager with a few added utility functions (see above), and
+    ``public``, which has those same utility functions but limits the QuerySet to
+    only those values which are designated as public (``is_public=True``).
+    """
+    # Generic Foreign Key Fields
+    content_type = models.ForeignKey(ContentType)
+    object_id = models.PositiveIntegerField(_('object ID'))
+    content_object = GenericForeignKey()
+    
+    # Hierarchy Field
+    parent = models.ForeignKey('self', null = True, blank=True, default = None, related_name='children')
+    
+    # User-Replacement Fields
+    name = models.CharField(_('name'), max_length = 128)
+    website = models.URLField(_('site'), blank = True)
+    email = models.EmailField(_('e-mail address'), blank = True)
+    
+    # Date Fields
+    date_submitted = models.DateTimeField(_('date/time submitted'), default = datetime.now)
+    date_modified = models.DateTimeField(_('date/time modified'), default = datetime.now)
+    date_approved = models.DateTimeField(_('date/time approved'), default=None, null=True, blank=True)
+    
+    # Meat n' Potatoes
+    comment = models.TextField(_('comment'))
+    markup = models.IntegerField(choices=MARKUP_CHOICES, default=DEFAULT_MARKUP, null=True, blank=True)
+    
+    # Status Fields
+    is_public = models.BooleanField(_('is public'), default = True)
+    is_approved = models.BooleanField(_('is approved'), default = False)
+    
+    # Extra Field
+    ip_address = models.GenericIPAddressField(_('IP address'), null=True, blank=True)
+    
+    objects = ThreadedCommentManager()
+    public = PublicThreadedCommentManager()
+    
+    def __unicode__(self):
+        if len(self.comment) > 50:
+            return self.comment[:50] + "..."
+        return self.comment[:50]
+    
+    def save(self, **kwargs):
+        if not self.markup:
+            self.markup = DEFAULT_MARKUP
+        self.date_modified = datetime.now()
+        if not self.date_approved and self.is_approved:
+            self.date_approved = datetime.now()
+        super(FreeThreadedComment, self).save()
+    
+    def get_content_object(self, **kwargs):
+        """
+        Wrapper around the GenericForeignKey due to compatibility reasons
+        and due to ``list_display`` limitations.
+        """
+        return self.content_object
+    
+    def get_base_data(self, show_dates=True):
+        """
+        Outputs a Python dictionary representing the most useful bits of
+        information about this particular object instance.
+        
+        This is mostly useful for testing purposes, as the output from the
+        serializer changes from run to run.  However, this may end up being
+        useful for JSON and/or XML data exchange going forward and as the
+        serializer system is changed.
+        """
+        markup = "plaintext"
+        for markup_choice in MARKUP_CHOICES:
+            if self.markup == markup_choice[0]:
+                markup = markup_choice[1]
+                break
+        to_return = {
+            'content_object' : self.content_object,
+            'parent' : self.parent,
+            'name' : self.name,
+            'website' : self.website,
+            'email' : self.email,
+            'comment' : self.comment,
+            'is_public' : self.is_public,
+            'is_approved' : self.is_approved,
+            'ip_address' : self.ip_address,
+            'markup' : force_unicode(markup),
+        }
+        if show_dates:
+            to_return['date_submitted'] = self.date_submitted
+            to_return['date_modified'] = self.date_modified
+            to_return['date_approved'] = self.date_approved
+        return to_return
+    
+    class Meta:
+        ordering = ('-date_submitted',)
+        verbose_name = _("Free Threaded Comment")
+        verbose_name_plural = _("Free Threaded Comments")
+        get_latest_by = "date_submitted"
+
+
+class TestModel(models.Model):
+    """
+    This model is simply used by this application's test suite as a model to 
+    which to attach comments.
+    """
+    name = models.CharField(max_length=5)
+    is_public = models.BooleanField(default=True)
+    date = models.DateTimeField(default=datetime.now)

=== added file 'threadedcomments/moderation.py'
--- threadedcomments/moderation.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/moderation.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,53 @@
+from django.db.models import signals
+from threadedcomments.models import ThreadedComment, FreeThreadedComment, MARKUP_CHOICES
+from threadedcomments.models import DEFAULT_MAX_COMMENT_LENGTH, DEFAULT_MAX_COMMENT_DEPTH
+from comment_utils import moderation
+
+MARKUP_CHOICES_IDS = [c[0] for c in MARKUP_CHOICES]
+
+
+class CommentModerator(moderation.CommentModerator):
+    max_comment_length = DEFAULT_MAX_COMMENT_LENGTH
+    allowed_markup = MARKUP_CHOICES_IDS
+    max_depth = DEFAULT_MAX_COMMENT_DEPTH
+
+    def _is_past_max_depth(self, comment):
+        i = 1
+        c = comment.parent
+        while c != None:
+            c = c.parent
+            i = i + 1
+            if i > self.max_depth:
+                return True
+        return False
+
+    def allow(self, comment, content_object):
+        if self._is_past_max_depth(comment):
+            return False
+        if comment.markup not in self.allowed_markup:
+            return False
+        return super(CommentModerator, self).allow(comment, content_object)
+
+    def moderate(self, comment, content_object):
+        if len(comment.comment) > self.max_comment_length:
+            return True
+        return super(CommentModerator, self).moderate(comment, content_object)
+
+class Moderator(moderation.Moderator):
+    def connect(self):
+        for model in (ThreadedComment, FreeThreadedComment):
+            signals.pre_save.connect(self.pre_save_moderation, sender=model)
+            signals.post_save.connect(self.post_save_moderation, sender=model)
+    
+    ## THE FOLLOWING ARE HACKS UNTIL django-comment-utils GETS UPDATED SIGNALS ####
+    def pre_save_moderation(self, sender=None, instance=None, **kwargs):
+        return super(Moderator, self).pre_save_moderation(sender, instance)
+
+    def post_save_moderation(self, sender=None, instance=None, **kwargs):
+        return super(Moderator, self).post_save_moderation(sender, instance)
+
+
+# Instantiate the ``Moderator`` so that other modules can import and 
+# begin to register with it.
+
+moderator = Moderator()
\ No newline at end of file

=== added directory 'threadedcomments/templatetags'
=== added file 'threadedcomments/templatetags/__init__.py'
=== added file 'threadedcomments/templatetags/gravatar.py'
--- threadedcomments/templatetags/gravatar.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/templatetags/gravatar.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,118 @@
+from django import template
+from django.conf import settings
+from django.template.defaultfilters import stringfilter
+from django.utils.encoding import smart_str
+from django.utils.safestring import mark_safe
+from django.utils.hashcompat import md5_constructor
+import urllib
+
+GRAVATAR_MAX_RATING = getattr(settings, 'GRAVATAR_MAX_RATING', 'R')
+GRAVATAR_DEFAULT_IMG = getattr(settings, 'GRAVATAR_DEFAULT_IMG', 'img:blank')
+GRAVATAR_SIZE = getattr(settings, 'GRAVATAR_SIZE', 80)
+
+GRAVATAR_URL = u'http://www.gravatar.com/avatar.php?gravatar_id=%(hash)s&rating=%(rating)s&size=%(size)s&default=%(default)s'
+
+def get_gravatar_url(parser, token):
+    """
+    Generates a gravatar image URL based on the given parameters.
+        
+    Format is as follows (The square brackets indicate that those arguments are 
+    optional.)::
+    
+        {% get_gravatar_url for myemailvar [rating "R" size 80 default img:blank as gravatar_url] %}
+    
+    Rating, size, and default may be either literal values or template variables.
+    The template tag will attempt to resolve variables first, and on resolution
+    failure it will use the literal value.
+    
+    If ``as`` is not specified, the URL will be output to the template in place.
+    
+    For all other arguments that are not specified, the appropriate default 
+    settings attribute will be used instead.
+    """
+    words = token.contents.split()
+    tagname = words.pop(0)
+    if len(words) < 2:
+        raise template.TemplateSyntaxError, "%r tag: At least one argument should be provided." % tagname
+    if words.pop(0) != "for":
+        raise template.TemplateSyntaxError, "%r tag: Syntax is {% get_gravatar_url for myemailvar rating "R" size 80 default img:blank as gravatar_url %}, where everything after myemailvar is optional."
+    email = words.pop(0)
+    if len(words) % 2 != 0:
+        raise template.TemplateSyntaxError, "%r tag: Imbalanced number of arguments." % tagname
+    args = {
+        'email': email,
+        'rating': GRAVATAR_MAX_RATING,
+        'size': GRAVATAR_SIZE,
+        'default': GRAVATAR_DEFAULT_IMG,
+    }
+    for name, value in zip(words[::2], words[1::2]):
+        name = name.lower()
+        if name not in ('rating', 'size', 'default', 'as'):
+            raise template.TemplateSyntaxError, "%r tag: Invalid argument %r." % tagname, name
+        args[smart_str(name)] = value
+    return GravatarUrlNode(**args)
+
+class GravatarUrlNode(template.Node):
+    def __init__(self, email=None, rating=GRAVATAR_MAX_RATING, size=GRAVATAR_SIZE, 
+        default=GRAVATAR_DEFAULT_IMG, **other_kwargs):
+        self.email = template.Variable(email)
+        self.rating = template.Variable(rating)
+        try:
+            self.size = template.Variable(size)
+        except:
+            self.size = size
+        self.default = template.Variable(default)
+        self.other_kwargs = other_kwargs
+
+    def render(self, context):
+        # Try to resolve the variables.  If they are not resolve-able, then use
+        # the provided name itself.
+        try:
+            email = self.email.resolve(context)
+        except template.VariableDoesNotExist:
+            email = self.email.var
+        try:
+            rating = self.rating.resolve(context)
+        except template.VariableDoesNotExist:
+            rating = self.rating.var
+        try:
+            size = self.size.resolve(context)
+        except template.VariableDoesNotExist:
+            size = self.size.var
+        except AttributeError:
+            size = self.size
+        try:
+            default = self.default.resolve(context)
+        except template.VariableDoesNotExist:
+            default = self.default.var
+        
+        gravatargs = {
+            'hash': md5_constructor(email).hexdigest(),
+            'rating': rating,
+            'size': size,
+            'default': urllib.quote_plus(default),
+        }
+        url = GRAVATAR_URL % gravatargs
+        if 'as' in self.other_kwargs:
+            context[self.other_kwargs['as']] = mark_safe(url)
+            return ''
+        return url
+
+def gravatar(email):
+    """
+    Takes an e-mail address and returns a gravatar image URL, using properties
+    from the django settings file.
+    """
+    hashed_email = md5_constructor(email).hexdigest()
+    return mark_safe(GRAVATAR_URL % {
+        'hash': hashed_email,
+        'rating': GRAVATAR_MAX_RATING, 
+        'size': GRAVATAR_SIZE,
+        'default': urllib.quote_plus(GRAVATAR_DEFAULT_IMG),
+    })
+gravatar = stringfilter(gravatar)
+
+
+register = template.Library()
+register.filter('gravatar', gravatar)
+register.tag('get_gravatar_url', get_gravatar_url)

=== added file 'threadedcomments/templatetags/threadedcommentstags.py'
--- threadedcomments/templatetags/threadedcommentstags.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/templatetags/threadedcommentstags.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,438 @@
+import re
+from django import template
+from django.contrib.contenttypes.models import ContentType
+from django.core.urlresolvers import reverse
+from django.utils.encoding import force_unicode
+from django.utils.safestring import mark_safe
+from threadedcomments.models import ThreadedComment, FreeThreadedComment
+from threadedcomments.forms import ThreadedCommentForm, FreeThreadedCommentForm
+from mainpage.templatetags.wl_markdown import do_wl_markdown;
+
+# Regular expressions for getting rid of newlines and witespace
+inbetween = re.compile('>[ \r\n]+<')
+newlines = re.compile('\r|\n')
+
+def get_contenttype_kwargs(content_object):
+    """
+    Gets the basic kwargs necessary for almost all of the following tags.
+    """
+    kwargs = {
+        'content_type' : ContentType.objects.get_for_model(content_object).id,
+        'object_id' : getattr(content_object, 'pk', getattr(content_object, 'id')),
+    }
+    return kwargs
+
+def get_comment_url(content_object, parent=None):
+    """
+    Given an object and an optional parent, this tag gets the URL to POST to for the
+    creation of new ``ThreadedComment`` objects.
+    """
+    kwargs = get_contenttype_kwargs(content_object)
+    if parent:
+        if not isinstance(parent, ThreadedComment):
+            raise template.TemplateSyntaxError, "get_comment_url requires its parent object to be of type ThreadedComment"
+        kwargs.update({'parent_id' : getattr(parent, 'pk', getattr(parent, 'id'))})
+        return reverse('tc_comment_parent', kwargs=kwargs)
+    else:
+        return reverse('tc_comment', kwargs=kwargs)
+
+def get_comment_url_ajax(content_object, parent=None, ajax_type='json'):
+    """
+    Given an object and an optional parent, this tag gets the URL to POST to for the
+    creation of new ``ThreadedComment`` objects.  It returns the latest created object
+    in the AJAX form of the user's choosing (json or xml).
+    """
+    kwargs = get_contenttype_kwargs(content_object)
+    kwargs.update({'ajax' : ajax_type})
+    if parent:
+        if not isinstance(parent, ThreadedComment):
+            raise template.TemplateSyntaxError, "get_comment_url_ajax requires its parent object to be of type ThreadedComment"
+        kwargs.update({'parent_id' : getattr(parent, 'pk', getattr(parent, 'id'))})
+        return reverse('tc_comment_parent_ajax', kwargs=kwargs)
+    else:
+        return reverse('tc_comment_ajax', kwargs=kwargs)
+
+def get_comment_url_json(content_object, parent=None):
+    """
+    Wraps ``get_comment_url_ajax`` with ``ajax_type='json'``
+    """
+    try:
+        return get_comment_url_ajax(content_object, parent, ajax_type="json")
+    except template.TemplateSyntaxError:
+        raise template.TemplateSyntaxError, "get_comment_url_json requires its parent object to be of type ThreadedComment"
+    return ''
+
+def get_comment_url_xml(content_object, parent=None):
+    """
+    Wraps ``get_comment_url_ajax`` with ``ajax_type='xml'``
+    """
+    try:
+        return get_comment_url_ajax(content_object, parent, ajax_type="xml")
+    except template.TemplateSyntaxError:
+        raise template.TemplateSyntaxError, "get_comment_url_xml requires its parent object to be of type ThreadedComment"
+    return ''
+
+def get_free_comment_url(content_object, parent=None):
+    """
+    Given an object and an optional parent, this tag gets the URL to POST to for the
+    creation of new ``FreeThreadedComment`` objects.
+    """
+    kwargs = get_contenttype_kwargs(content_object)
+    if parent:
+        if not isinstance(parent, FreeThreadedComment):
+            raise template.TemplateSyntaxError, "get_free_comment_url requires its parent object to be of type FreeThreadedComment"
+        kwargs.update({'parent_id' : getattr(parent, 'pk', getattr(parent, 'id'))})
+        return reverse('tc_free_comment_parent', kwargs=kwargs)
+    else:
+        return reverse('tc_free_comment', kwargs=kwargs)
+
+def get_free_comment_url_ajax(content_object, parent=None, ajax_type='json'):
+    """
+    Given an object and an optional parent, this tag gets the URL to POST to for the
+    creation of new ``FreeThreadedComment`` objects.  It returns the latest created object
+    in the AJAX form of the user's choosing (json or xml).
+    """
+    kwargs = get_contenttype_kwargs(content_object)
+    kwargs.update({'ajax' : ajax_type})
+    if parent:
+        if not isinstance(parent, FreeThreadedComment):
+            raise template.TemplateSyntaxError, "get_free_comment_url_ajax requires its parent object to be of type FreeThreadedComment"
+        kwargs.update({'parent_id' : getattr(parent, 'pk', getattr(parent, 'id'))})
+        return reverse('tc_free_comment_parent_ajax', kwargs=kwargs)
+    else:
+        return reverse('tc_free_comment_ajax', kwargs=kwargs)
+
+def get_free_comment_url_json(content_object, parent=None):
+    """
+    Wraps ``get_free_comment_url_ajax`` with ``ajax_type='json'``
+    """
+    try:
+        return get_free_comment_url_ajax(content_object, parent, ajax_type="json")
+    except template.TemplateSyntaxError:
+        raise template.TemplateSyntaxError, "get_free_comment_url_json requires its parent object to be of type FreeThreadedComment"
+    return ''
+
+def get_free_comment_url_xml(content_object, parent=None):
+    """
+    Wraps ``get_free_comment_url_ajax`` with ``ajax_type='xml'``
+    """
+    try:
+        return get_free_comment_url_ajax(content_object, parent, ajax_type="xml")
+    except template.TemplateSyntaxError:
+        raise template.TemplateSyntaxError, "get_free_comment_url_xml requires its parent object to be of type FreeThreadedComment"
+    return ''
+
+def auto_transform_markup(comment):
+    """
+    Given a comment (``ThreadedComment`` or ``FreeThreadedComment``), this tag
+    looks up the markup type of the comment and formats the output accordingly.
+    
+    It can also output the formatted content to a context variable, if a context name is
+    specified.
+    """
+    #NOCOMM franku: django.contrib.markup doesn't exist anymore
+    try:
+        from django.utils.html import escape
+        from threadedcomments.models import MARKDOWN, TEXTILE, REST, PLAINTEXT
+        if comment.markup == MARKDOWN:
+            from django.contrib.markup.templatetags.markup import markdown
+            return markdown(comment.comment)
+        elif comment.markup == TEXTILE:
+            from django.contrib.markup.templatetags.markup import textile
+            return textile(comment.comment)
+        elif comment.markup == REST:
+            from django.contrib.markup.templatetags.markup import restructuredtext
+            return restructuredtext(comment.comment)
+#        elif comment.markup == HTML:
+#            return mark_safe(force_unicode(comment.comment))
+        elif comment.markup == PLAINTEXT:
+            return escape(comment.comment)
+    except ImportError:
+        # Not marking safe, in case tag fails and users input malicious code.
+        # NOCOMM franku: bleach the comment
+        return do_wl_markdown(comment.comment, 'bleachit')
+
+def do_auto_transform_markup(parser, token):
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, "%r tag must be of format {%% %r COMMENT %%} or of format {%% %r COMMENT as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0], token.contents.split()[0])
+    if len(split) == 2:
+        return AutoTransformMarkupNode(split[1])
+    elif len(split) == 4:
+        return AutoTransformMarkupNode(split[1], context_name=split[3])
+    else:
+        raise template.TemplateSyntaxError, "Invalid number of arguments for tag %r" % split[0]
+
+class AutoTransformMarkupNode(template.Node):
+    def __init__(self, comment, context_name=None):
+        self.comment = template.Variable(comment)
+        self.context_name = context_name
+    def render(self, context):
+        comment = self.comment.resolve(context)
+        if self.context_name:
+            context[self.context_name] = auto_transform_markup(comment)
+            return ''
+        else:
+            return auto_transform_markup(comment)
+
+def do_get_threaded_comment_tree(parser, token):
+    """
+    Gets a tree (list of objects ordered by preorder tree traversal, and with an
+    additional ``depth`` integer attribute annotated onto each ``ThreadedComment``.
+    """
+    error_string = "%r tag must be of format {%% get_threaded_comment_tree for OBJECT [TREE_ROOT] as CONTEXT_VARIABLE %%}" % token.contents.split()[0]
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(error_string)
+    if len(split) == 5:
+        return CommentTreeNode(split[2], split[4], split[3])
+    elif len(split) == 6:
+        return CommentTreeNode(split[2], split[5], split[3])
+    else:
+        raise template.TemplateSyntaxError(error_string)
+
+def do_get_free_threaded_comment_tree(parser, token):
+    """
+    Gets a tree (list of objects ordered by traversing tree in preorder, and with an
+    additional ``depth`` integer attribute annotated onto each ``FreeThreadedComment.``
+    """
+    error_string = "%r tag must be of format {%% get_free_threaded_comment_tree for OBJECT [TREE_ROOT] as CONTEXT_VARIABLE %%}" % token.contents.split()[0]
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError(error_string)
+    if len(split) == 5:
+        return FreeCommentTreeNode(split[2], split[4], split[3])
+    elif len(split) == 6:
+        return FreeCommentTreeNode(split[2], split[5], split[3])
+    else:
+        raise template.TemplateSyntaxError(error_string)
+
+class CommentTreeNode(template.Node):
+    def __init__(self, content_object, context_name, tree_root):
+        self.content_object = template.Variable(content_object)
+        self.tree_root = template.Variable(tree_root)
+        self.tree_root_str = tree_root
+        self.context_name = context_name
+    def render(self, context):
+        content_object = self.content_object.resolve(context)
+        try:
+            tree_root = self.tree_root.resolve(context)
+        except template.VariableDoesNotExist:
+            if self.tree_root_str == 'as':
+                tree_root = None
+            else:
+                try:
+                    tree_root = int(self.tree_root_str)
+                except ValueError:
+                    tree_root = self.tree_root_str
+        context[self.context_name] = ThreadedComment.public.get_tree(content_object, root=tree_root)
+        return ''
+
+class FreeCommentTreeNode(template.Node):
+    def __init__(self, content_object, context_name, tree_root):
+        self.content_object = template.Variable(content_object)
+        self.tree_root = template.Variable(tree_root)
+        self.tree_root_str = tree_root
+        self.context_name = context_name
+    def render(self, context):
+        content_object = self.content_object.resolve(context)
+        try:
+            tree_root = self.tree_root.resolve(context)
+        except template.VariableDoesNotExist:
+            if self.tree_root_str == 'as':
+                tree_root = None
+            else:
+                try:
+                    tree_root = int(self.tree_root_str)
+                except ValueError:
+                    tree_root = self.tree_root_str
+        context[self.context_name] = FreeThreadedComment.public.get_tree(content_object, root=tree_root)
+        return ''
+
+def do_get_comment_count(parser, token):
+    """
+    Gets a count of how many ThreadedComment objects are attached to the given
+    object.
+    """
+    error_message = "%r tag must be of format {%% %r for OBJECT as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if split[1] != 'for' or split[3] != 'as':
+        raise template.TemplateSyntaxError, error_message
+    return ThreadedCommentCountNode(split[2], split[4])
+
+class ThreadedCommentCountNode(template.Node):
+    def __init__(self, content_object, context_name):
+        self.content_object = template.Variable(content_object)
+        self.context_name = context_name
+    def render(self, context):
+        content_object = self.content_object.resolve(context)
+        context[self.context_name] = ThreadedComment.public.all_for_object(content_object).count()
+        return ''
+        
+def do_get_free_comment_count(parser, token):
+    """
+    Gets a count of how many FreeThreadedComment objects are attached to the 
+    given object.
+    """
+    error_message = "%r tag must be of format {%% %r for OBJECT as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if split[1] != 'for' or split[3] != 'as':
+        raise template.TemplateSyntaxError, error_message
+    return FreeThreadedCommentCountNode(split[2], split[4])
+
+class FreeThreadedCommentCountNode(template.Node):
+    def __init__(self, content_object, context_name):
+        self.content_object = template.Variable(content_object)
+        self.context_name = context_name
+    def render(self, context):
+        content_object = self.content_object.resolve(context)
+        context[self.context_name] = FreeThreadedComment.public.all_for_object(content_object).count()
+        return ''
+
+def oneline(value):
+    """
+    Takes some HTML and gets rid of newlines and spaces between tags, rendering
+    the result all on one line.
+    """
+    try:
+        return mark_safe(newlines.sub('', inbetween.sub('><', value)))
+    except:
+        return value
+
+def do_get_threaded_comment_form(parser, token):
+    """
+    Gets a FreeThreadedCommentForm and inserts it into the context.
+    """
+    error_message = "%r tag must be of format {%% %r as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if split[1] != 'as':
+        raise template.TemplateSyntaxError, error_message
+    if len(split) != 3:
+        raise template.TemplateSyntaxError, error_message
+    if "free" in split[0]:
+        is_free = True
+    else:
+        is_free = False
+    return ThreadedCommentFormNode(split[2], free=is_free)
+
+class ThreadedCommentFormNode(template.Node):
+    def __init__(self, context_name, free=False):
+        self.context_name = context_name
+        self.free = free
+    def render(self, context):
+        if self.free:
+            form = FreeThreadedCommentForm()
+        else:
+            form = ThreadedCommentForm()
+        context[self.context_name] = form
+        return ''
+
+def do_get_latest_comments(parser, token):
+    """
+    Gets the latest comments by date_submitted.
+    """
+    error_message = "%r tag must be of format {%% %r NUM_TO_GET as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if len(split) != 4:
+        raise template.TemplateSyntaxError, error_message
+    if split[2] != 'as':
+        raise template.TemplateSyntaxError, error_message
+    if "free" in split[0]:
+        is_free = True
+    else:
+        is_free = False
+    return LatestCommentsNode(split[1], split[3], free=is_free)
+
+class LatestCommentsNode(template.Node):
+    def __init__(self, num, context_name, free=False):
+        self.num = num
+        self.context_name = context_name
+        self.free = free
+    def render(self, context):
+        if self.free:
+            comments = FreeThreadedComment.objects.order_by('-date_submitted')[:self.num]
+        else:
+            comments = ThreadedComment.objects.order_by('-date_submitted')[:self.num]
+        context[self.context_name] = comments
+        return ''
+
+def do_get_user_comments(parser, token):
+    """
+    Gets all comments submitted by a particular user.
+    """
+    error_message = "%r tag must be of format {%% %r for OBJECT as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if len(split) != 5:
+        raise template.TemplateSyntaxError, error_message
+    return UserCommentsNode(split[2], split[4])
+
+class UserCommentsNode(template.Node):
+    def __init__(self, user, context_name):
+        self.user = template.Variable(user)
+        self.context_name = context_name
+    def render(self, context):
+        user = self.user.resolve(context)
+        context[self.context_name] = user.threadedcomment_set.all()
+        return ''
+
+def do_get_user_comment_count(parser, token):
+    """
+    Gets the count of all comments submitted by a particular user.
+    """
+    error_message = "%r tag must be of format {%% %r for OBJECT as CONTEXT_VARIABLE %%}" % (token.contents.split()[0], token.contents.split()[0])
+    try:
+        split = token.split_contents()
+    except ValueError:
+        raise template.TemplateSyntaxError, error_message
+    if len(split) != 5:
+        raise template.TemplateSyntaxError, error_message
+    return UserCommentCountNode(split[2], split[4])
+
+class UserCommentCountNode(template.Node):
+    def __init__(self, user, context_name):
+        self.user = template.Variable(user)
+        self.context_name = context_name
+    def render(self, context):
+        user = self.user.resolve(context)
+        context[self.context_name] = user.threadedcomment_set.all().count()
+        return ''
+
+register = template.Library()
+register.simple_tag(get_comment_url)
+register.simple_tag(get_comment_url_json)
+register.simple_tag(get_comment_url_xml)
+register.simple_tag(get_free_comment_url)
+register.simple_tag(get_free_comment_url_json)
+register.simple_tag(get_free_comment_url_xml)
+
+register.filter('oneline', oneline)
+
+register.tag('auto_transform_markup', do_auto_transform_markup)
+register.tag('get_threaded_comment_tree', do_get_threaded_comment_tree)
+register.tag('get_free_threaded_comment_tree', do_get_free_threaded_comment_tree)
+register.tag('get_comment_count', do_get_comment_count)
+register.tag('get_free_comment_count', do_get_free_comment_count)
+register.tag('get_free_threaded_comment_form', do_get_threaded_comment_form)
+register.tag('get_threaded_comment_form', do_get_threaded_comment_form)
+register.tag('get_latest_comments', do_get_latest_comments)
+register.tag('get_latest_free_comments', do_get_latest_comments)
+register.tag('get_user_comments', do_get_user_comments)
+register.tag('get_user_comment_count', do_get_user_comment_count)
\ No newline at end of file

=== added directory 'threadedcomments/tests'
=== added file 'threadedcomments/tests/__init__.py'
--- threadedcomments/tests/__init__.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/tests/__init__.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,8 @@
+from views_tests import *
+from templatetags_tests import *
+try:
+    import comment_utils
+except ImportError:
+    pass
+else:
+    from moderator_tests import *

=== added file 'threadedcomments/tests/moderator_tests.py'
--- threadedcomments/tests/moderator_tests.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/tests/moderator_tests.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,400 @@
+from django.core import mail
+from django.test import TestCase
+
+from django.contrib.auth.models import User
+
+from threadedcomments.moderation import moderator, CommentModerator
+from threadedcomments.models import FreeThreadedComment, ThreadedComment, TestModel
+from threadedcomments.models import MARKDOWN, TEXTILE, REST, PLAINTEXT
+
+
+__all__ = ("ModeratorTestCase",)
+
+
+class ModeratorTestCase(TestCase):
+    
+    def test_threadedcomment(self):
+        topic = TestModel.objects.create(name = "Test")
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        user2 = User.objects.create_user('user2', 'floguy@xxxxxxxxx', password='password')
+        
+        comment1 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = 'This is fun!  This is very fun!',
+        )
+        comment2 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = 'This is stupid!  I hate it!',
+        )
+        comment3 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment2,
+            comment = 'I agree, the first comment was wrong and you are right!',
+        )
+        comment4 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = 'What are we talking about?',
+        )
+        comment5 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment3,
+            comment = "I'm a fanboy!",
+        )
+        comment6 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment1,
+            comment = "What are you talking about?",
+        )
+        
+        class Moderator1(CommentModerator):
+            enable_field = 'is_public'
+            auto_close_field = 'date'
+            close_after = 15
+        moderator.register(TestModel, Moderator1)
+        
+        comment7 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = "Post moderator addition.  Does it still work?",
+        )
+        
+        topic.is_public = False
+        topic.save()
+        
+        comment8 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "This should not appear, due to enable_field",
+        )
+        
+        moderator.unregister(TestModel)
+        
+        comment9 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = "This should appear again, due to unregistration",
+        )
+
+        self.assertEquals(len(mail.outbox), 0)
+        
+        ##################
+        
+        class Moderator2(CommentModerator):
+            enable_field = 'is_public'
+            auto_close_field = 'date'
+            close_after = 15
+            akismet = False
+            email_notification = True
+        moderator.register(TestModel, Moderator2)
+        
+        comment10 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1',
+            comment = "This should not appear again, due to registration with a new manager.",
+        )
+        
+        topic.is_public = True
+        topic.save()
+        
+        comment11 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment1,
+            comment = "This should appear again.",
+        )
+        
+        self.assertEquals(len(mail.outbox), 1)
+        mail.outbox = []
+        
+        topic.date = topic.date - datetime.timedelta(days = 20)
+        topic.save()
+        
+        comment12 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "This shouldn't appear, due to close_after=15.",
+        )
+        
+        topic.date = topic.date + datetime.timedelta(days = 20)
+        topic.save()
+        
+        moderator.unregister(TestModel)
+        
+        class Moderator3(CommentModerator):
+            max_comment_length = 10
+        moderator.register(TestModel, Moderator3)
+        
+        comment13 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "This shouldn't appear because it has more than 10 chars.",
+        )
+        
+        comment14 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "<10chars",
+        )
+        
+        moderator.unregister(TestModel)
+        
+        class Moderator4(CommentModerator):
+            allowed_markup = [REST,]
+        moderator.register(TestModel, Moderator4)
+        
+        comment15 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "INVALID Markup.  Should not show up.", markup=TEXTILE
+        )
+        
+        comment16 = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', parent = comment7,
+            comment = "VALID Markup.  Should show up.", markup=REST
+        )
+        
+        moderator.unregister(TestModel)
+        
+        tree = ThreadedComment.public.get_tree(topic)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is fun!  This is very fun!
+    What are you talking about?
+    This should appear again.
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+What are we talking about?
+Post moderator addition.  Does it still work?
+    <10chars
+    VALID Markup.  Should show up.
+This should appear again, due to unregistration
+""".lstrip())
+
+        tree = ThreadedComment.objects.get_tree(topic)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is fun!  This is very fun!
+    What are you talking about?
+    This should appear again.
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+What are we talking about?
+Post moderator addition.  Does it still work?
+    This shouldn't appear because it has more than 10 chars.
+    <10chars
+    VALID Markup.  Should show up.
+This should appear again, due to unregistration
+""".lstrip())
+        
+        tree = ThreadedComment.objects.get_tree(topic, root=comment2)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+""".lstrip())
+        
+        tree = ThreadedComment.objects.get_tree(topic, root=comment2.id)
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+""".lstrip())
+        
+    def test_freethreadedcomment(self):
+        
+        ###########################
+        ### FreeThreadedComment ###
+        ###########################
+        
+        fcomment1 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1',
+            comment = 'This is fun!  This is very fun!',
+        )
+        fcomment2 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1',
+            comment = 'This is stupid!  I hate it!',
+        )
+        fcomment3 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment2,
+            comment = 'I agree, the first comment was wrong and you are right!',
+        )
+        fcomment4 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', 
+            website="http://www.eflorenzano.com/";, email="floguy@xxxxxxxxx",
+            comment = 'What are we talking about?',
+        )
+        fcomment5 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment3,
+            comment = "I'm a fanboy!",
+        )
+        fcomment6 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment1,
+            comment = "What are you talking about?",
+        )
+        
+        moderator.register(TestModel, Moderator1)
+        
+        fcomment7 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1',
+            comment = "Post moderator addition.  Does it still work?",
+        )
+        
+        topic.is_public = False
+        topic.save()
+        
+        fcomment8 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment7,
+            comment = "This should not appear, due to enable_field",
+        )
+        
+        moderator.unregister(TestModel)
+        
+        fcomment9 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1',
+            comment = "This should appear again, due to unregistration",
+        )
+        
+        self.assertEquals(len(mail.outbox), 0)
+
+        moderator.register(TestModel, Moderator2)
+        
+        fcomment10 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1',
+            comment = "This should not appear again, due to registration with a new manager.",
+        )
+        
+        topic.is_public = True
+        topic.save()
+        
+        fcomment11 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment1,
+            comment = "This should appear again.",
+        )
+        
+        self.assertEquals(len(mail.outbox), 1)
+        
+        mail.outbox = []
+        
+        topic.date = topic.date - datetime.timedelta(days = 20)
+        topic.save()
+        
+        fcomment12 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment7,
+            comment = "This shouldn't appear, due to close_after=15.",
+        )
+        
+        topic.date = topic.date + datetime.timedelta(days = 20)
+        topic.save()
+        
+        moderator.unregister(TestModel)
+        moderator.register(TestModel, Moderator3)
+        
+        fcomment13 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment7,
+            comment = "This shouldn't appear because it has more than 10 chars.",
+        )
+        
+        fcomment14 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment7,
+            comment = "<10chars",
+        )
+        
+        moderator.unregister(TestModel)
+        class Moderator5(CommentModerator):
+            allowed_markup = [REST,]
+            max_depth = 3
+        moderator.register(TestModel, Moderator5)
+        
+        fcomment15 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment7,
+            comment = "INVALID Markup.  Should not show up.", markup=TEXTILE
+        )
+        
+        fcomment16 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = None,
+            comment = "VALID Markup.  Should show up.", markup=REST
+        )
+        
+        fcomment17 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment16,
+            comment = "Building Depth...Should Show Up.", markup=REST
+        )
+        
+        fcomment18 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment17,
+            comment = "More Depth...Should Show Up.", markup=REST
+        )
+        
+        fcomment19 = FreeThreadedComment.objects.create_for_object(
+            topic, name = "Eric", ip_address = '127.0.0.1', parent = fcomment18,
+            comment = "Too Deep..Should NOT Show UP", markup=REST
+        )
+        
+        moderator.unregister(TestModel)
+        
+        tree = FreeThreadedComment.public.get_tree(topic)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is fun!  This is very fun!
+    What are you talking about?
+    This should appear again.
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+What are we talking about?
+Post moderator addition.  Does it still work?
+    <10chars
+This should appear again, due to unregistration
+VALID Markup.  Should show up.
+    Building Depth...Should Show Up.
+        More Depth...Should Show Up.
+""".lstrip())
+        
+        tree = FreeThreadedComment.objects.get_tree(topic)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is fun!  This is very fun!
+    What are you talking about?
+    This should appear again.
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+What are we talking about?
+Post moderator addition.  Does it still work?
+    This shouldn't appear because it has more than 10 chars.
+    <10chars
+This should appear again, due to unregistration
+VALID Markup.  Should show up.
+    Building Depth...Should Show Up.
+        More Depth...Should Show Up.
+""".lstrip())
+        
+        tree = FreeThreadedComment.objects.get_tree(topic, root=comment2)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+""".lstrip())
+        
+        tree = FreeThreadedComment.objects.get_tree(topic, root=comment2.id)
+        output = []
+        for comment in tree:
+            output.append("%s %s" % ("    " * comment.depth, comment.comment))
+        self.assertEquals("\n".join(output),
+"""
+This is stupid!  I hate it!
+    I agree, the first comment was wrong and you are right!
+        I'm a fanboy!
+""".lstrip())

=== added file 'threadedcomments/tests/templatetags_tests.py'
--- threadedcomments/tests/templatetags_tests.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/tests/templatetags_tests.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,519 @@
+import datetime
+
+from xml.dom.minidom import parseString
+
+from django.core import mail
+from django.core.urlresolvers import reverse
+from django.template import Context, Template
+from django.test import TestCase
+from django.utils.simplejson import loads
+
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+
+from threadedcomments.models import FreeThreadedComment, ThreadedComment, TestModel
+from threadedcomments.models import MARKDOWN, TEXTILE, REST, PLAINTEXT
+from threadedcomments.templatetags import threadedcommentstags as tags
+
+
+__all__ = ("TemplateTagTestCase",)
+
+
+class TemplateTagTestCase(TestCase):
+    urls = "threadedcomments.tests.threadedcomments_urls"
+    
+    def test_get_comment_url(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        c = Context({
+            'topic': topic,
+            'parent': comment
+        })
+        sc = {
+            "ct": content_type.pk,
+            "id": topic.pk,
+            "pid": comment.pk,
+        }
+        
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url topic %}').render(c), u'/comment/%(ct)s/%(id)s/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url topic parent %}').render(c), u'/comment/%(ct)s/%(id)s/%(pid)s/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url_json topic %}').render(c), u'/comment/%(ct)s/%(id)s/json/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url_xml topic %}').render(c), u'/comment/%(ct)s/%(id)s/xml/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url_json topic parent %}').render(c), u'/comment/%(ct)s/%(id)s/%(pid)s/json/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_comment_url_xml topic parent %}').render(c), u'/comment/%(ct)s/%(id)s/%(pid)s/xml/' % sc)
+    
+    def test_get_free_comment_url(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        c = Context({
+            'topic': topic,
+            'parent': comment,
+        })
+        sc = {
+            "ct": content_type.pk,
+            "id": topic.pk,
+            "pid": comment.pk,
+        }
+        
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url topic %}').render(c), u'/freecomment/%(ct)s/%(id)s/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url topic parent %}').render(c), u'/freecomment/%(ct)s/%(id)s/%(pid)s/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url_json topic %}').render(c), u'/freecomment/%(ct)s/%(id)s/json/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url_xml topic %}').render(c), u'/freecomment/%(ct)s/%(id)s/xml/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url_json topic parent %}').render(c), u'/freecomment/%(ct)s/%(id)s/%(pid)s/json/' % sc)
+        self.assertEquals(Template('{% load threadedcommentstags %}{% get_free_comment_url_xml topic parent %}').render(c), u'/freecomment/%(ct)s/%(id)s/%(pid)s/xml/' % sc)
+    
+    def test_get_comment_count(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        c = Context({
+            'topic': topic,
+        })
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_comment_count for topic as count %}{{ count }}').render(c),
+            u'1'
+        )
+    
+    def test_get_free_comment_count(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        c = Context({
+            'topic': topic,
+        })
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_free_comment_count for topic as count %}{{ count }}').render(c),
+            u'1'
+        )
+    
+    def test_get_threaded_comment_form(self):
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_threaded_comment_form as form %}{{ form }}').render(Context({})),
+            u'<tr><th><label for="id_comment">comment:</label></th><td><textarea id="id_comment" rows="10" cols="40" name="comment"></textarea></td></tr>\n<tr><th><label for="id_markup">Markup:</label></th><td><select name="markup" id="id_markup">\n<option value="">---------</option>\n<option value="1">markdown</option>\n<option value="2">textile</option>\n<option value="3">restructuredtext</option>\n<option value="5" selected="selected">plaintext</option>\n</select></td></tr>'
+        )
+    
+    def test_get_latest_comments(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        old_topic = topic
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "Test 1",
+        )
+        ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "Test 2",
+        )
+        ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "Test 3",
+        )
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_latest_comments 2 as comments %}{{ comments }}').render(Context({})),
+            u'[&lt;ThreadedComment: Test 3&gt;, &lt;ThreadedComment: Test 2&gt;]'
+        )
+    
+    def test_get_latest_free_comments(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "Test 1",
+        )
+        FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "Test 2",
+        )
+        FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "Test 3",
+        )
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_latest_free_comments 2 as comments %}{{ comments }}').render(Context({})),
+            u'[&lt;FreeThreadedComment: Test 3&gt;, &lt;FreeThreadedComment: Test 2&gt;]'
+        )
+    
+    def test_get_threaded_comment_tree(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        parent1 = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "test1",
+        )
+        ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "test2",
+            parent = parent1,
+        )
+        parent2 = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "test3",
+        )
+        ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = '127.0.0.1',
+            comment = "test4",
+            parent = parent2,
+        )
+        
+        c = Context({
+            'topic': topic,
+        })
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_threaded_comment_tree for topic as tree %}[{% for item in tree %}({{ item.depth }}){{ item.comment }},{% endfor %}]').render(c),
+            u'[(0)test1,(1)test2,(0)test3,(1)test4,]'
+        )
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_threaded_comment_tree for topic 3 as tree %}[{% for item in tree %}({{ item.depth }}){{ item.comment }},{% endfor %}]').render(c),
+            u'[(0)test3,(1)test4,]'
+        )
+    
+    def test_get_free_threaded_comment_tree(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        parent1 = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "test1",
+        )
+        FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "test2",
+            parent = parent1,
+        )
+        parent2 = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "test3",
+        )
+        FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "test4",
+            parent = parent2,
+        )
+        
+        c = Context({
+            'topic': topic,
+        })
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_free_threaded_comment_tree for topic as tree %}[{% for item in tree %}({{ item.depth }}){{ item.comment }},{% endfor %}]').render(c),
+            u'[(0)test1,(1)test2,(0)test3,(1)test4,]'
+        )
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_free_threaded_comment_tree for topic 3 as tree %}[{% for item in tree %}({{ item.depth }}){{ item.comment }},{% endfor %}]').render(c),
+            u'[(0)test3,(1)test4,]'
+        )
+    
+    def test_user_comment_tags(self):
+        
+        user1 = User.objects.create_user('eric', 'floguy@xxxxxxxxx', password='password')
+        user2 = User.objects.create_user('brian', 'brosner@xxxxxxxxx', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        ThreadedComment.objects.create_for_object(topic,
+            user = user1,
+            ip_address = '127.0.0.1',
+            comment = "Eric comment",
+        )
+        ThreadedComment.objects.create_for_object(topic,
+            user = user2,
+            ip_address = '127.0.0.1',
+            comment = "Brian comment",
+        )
+        
+        c = Context({
+            'user': user1,
+        })
+        
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_user_comments for user as comments %}{{ comments }}').render(c),
+            u'[&lt;ThreadedComment: Eric comment&gt;]'
+        )
+        self.assertEquals(
+            Template('{% load threadedcommentstags %}{% get_user_comment_count for user as comment_count %}{{ comment_count }}').render(c),
+            u'1',
+        )
+    
+    def test_markdown_comment(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        topic = TestModel.objects.create(name="Test2")
+        
+        markdown_txt = '''
+A First Level Header
+====================
+
+A Second Level Header
+---------------------
+
+Now is the time for all good men to come to
+the aid of their country. This is just a
+regular paragraph.
+
+The quick brown fox jumped over the lazy
+dog's back.
+
+### Header 3
+
+> This is a blockquote.
+> 
+> This is the second paragraph in the blockquote.
+>
+> ## This is an H2 in a blockquote
+'''
+
+        comment_markdown = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', markup = MARKDOWN,
+            comment = markdown_txt,
+        )
+
+        c = Context({
+            'comment': comment_markdown,
+        })
+        s = Template("{% load threadedcommentstags %}{% auto_transform_markup comment %}").render(c).replace('\\n', '')
+        self.assertEquals(s.startswith(u"<h1>"), True)
+    
+    def test_textile_comment(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        topic = TestModel.objects.create(name="Test2")
+        
+        textile_txt = '''
+h2{color:green}. This is a title
+
+h3. This is a subhead
+
+p{color:red}. This is some text of dubious character. Isn't the use of "quotes" just lazy ... writing -- and theft of 'intellectual property' besides? I think the time has come to see a block quote.
+
+bq[fr]. This is a block quote. I'll admit it's not the most exciting block quote ever devised.
+
+Simple list:
+
+#{color:blue} one
+# two
+# three
+
+Multi-level list:
+
+# one
+## aye
+## bee
+## see
+# two
+## x
+## y
+# three
+
+Mixed list:
+
+* Point one
+* Point two
+## Step 1
+## Step 2
+## Step 3
+* Point three
+** Sub point 1
+** Sub point 2
+
+
+Well, that went well. How about we insert an <a href="/" title="watch out">old-fashioned ... hypertext link</a>? Will the quote marks in the tags get messed up? No!
+
+"This is a link (optional title)":http://www.textism.com
+
+table{border:1px solid black}.
+|_. this|_. is|_. a|_. header|
+<{background:gray}. |\2. this is|{background:red;width:200px}. a|^<>{height:200px}. row|
+|this|<>{padding:10px}. is|^. another|(bob#bob). row|
+
+An image:
+
+!/common/textist.gif(optional alt text)!
+
+# Librarians rule
+# Yes they do
+# But you knew that
+
+Some more text of dubious character. Here is a noisome string of CAPITAL letters. Here is ... something we want to _emphasize_. 
+That was a linebreak. And something to indicate *strength*. Of course I could use <em>my ... own HTML tags</em> if I <strong>felt</strong> like it.
+
+h3. Coding
+
+This <code>is some code, "isn't it"</code>. Watch those quote marks! Now for some preformatted text:
+
+<pre>
+<code>
+	$text = str_replace("<p>%::%</p>","",$text);
+	$text = str_replace("%::%</p>","",$text);
+	$text = str_replace("%::%","",$text);
+
+</code>
+</pre>
+
+This isn't code.
+
+
+So you see, my friends:
+
+* The time is now
+* The time is not later
+* The time is not yesterday
+* We must act
+'''
+
+        comment_textile = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', markup = TEXTILE,
+            comment = textile_txt,
+        )
+        c = Context({
+            'comment': comment_textile
+        })
+        s = Template("{% load threadedcommentstags %}{% auto_transform_markup comment %}").render(c)
+        self.assertEquals("<h3>" in s, True)
+    
+    def test_rest_comment(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        topic = TestModel.objects.create(name="Test2")
+        
+        rest_txt = '''
+FooBar Header
+=============
+reStructuredText is **nice**. It has its own webpage_.
+
+A table:
+
+=====  =====  ======
+   Inputs     Output
+------------  ------
+  A      B    A or B
+=====  =====  ======
+False  False  False
+True   False  True
+False  True   True
+True   True   True
+=====  =====  ======
+
+RST TracLinks
+-------------
+
+See also ticket `#42`::.
+
+.. _webpage: http://docutils.sourceforge.net/rst.html
+'''
+
+        comment_rest = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', markup = REST,
+            comment = rest_txt,
+        )
+        c = Context({
+            'comment': comment_rest
+        })
+        s = Template("{% load threadedcommentstags %}{% auto_transform_markup comment %}").render(c)
+        self.assertEquals(s.startswith('<p>reStructuredText is'), True)
+    
+    def test_plaintext_comment(self):
+        
+        user = User.objects.create_user('user', 'floguy@xxxxxxxxx', password='password')
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment_plaintext = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', markup = PLAINTEXT,
+            comment = '<b>This is Funny</b>',
+        )
+        c = Context({
+            'comment': comment_plaintext
+        })
+        self.assertEquals(
+            Template("{% load threadedcommentstags %}{% auto_transform_markup comment %}").render(c),
+            u'&lt;b&gt;This is Funny&lt;/b&gt;'
+        )
+
+        comment_plaintext = ThreadedComment.objects.create_for_object(
+            topic, user = user, ip_address = '127.0.0.1', markup = PLAINTEXT,
+            comment = '<b>This is Funny</b>',
+        )
+        c = Context({
+            'comment': comment_plaintext
+        })
+        self.assertEquals(
+            Template("{% load threadedcommentstags %}{% auto_transform_markup comment as abc %}{{ abc }}").render(c),
+            u'&lt;b&gt;This is Funny&lt;/b&gt;'
+        )
+    
+    def test_gravatar_tags(self):
+        c = Context({
+            'email': "floguy@xxxxxxxxx",
+            'rating': "G",
+            'size': 30,
+            'default': 'overridectx',
+        })
+        self.assertEquals(
+            Template('{% load gravatar %}{% get_gravatar_url for email %}').render(c),
+            u'http://www.gravatar.com/avatar.php?gravatar_id=04d6b8e8d3c68899ac88eb8623392150&rating=R&size=80&default=img%3Ablank'
+        )
+        self.assertEquals(
+            Template('{% load gravatar %}{% get_gravatar_url for email as var %}Var: {{ var }}').render(c),
+            u'Var: http://www.gravatar.com/avatar.php?gravatar_id=04d6b8e8d3c68899ac88eb8623392150&rating=R&size=80&default=img%3Ablank'
+        )
+        self.assertEquals(
+            Template('{% load gravatar %}{% get_gravatar_url for email size 30 rating "G" default override as var %}Var: {{ var }}').render(c),
+            u'Var: http://www.gravatar.com/avatar.php?gravatar_id=04d6b8e8d3c68899ac88eb8623392150&rating=G&size=30&default=override'
+        )
+        self.assertEquals(
+            Template('{% load gravatar %}{% get_gravatar_url for email size size rating rating default default as var %}Var: {{ var }}').render(c),
+            u'Var: http://www.gravatar.com/avatar.php?gravatar_id=04d6b8e8d3c68899ac88eb8623392150&rating=G&size=30&default=overridectx'
+        )
+        self.assertEquals(
+            Template('{% load gravatar %}{{ email|gravatar }}').render(c),
+            u'http://www.gravatar.com/avatar.php?gravatar_id=04d6b8e8d3c68899ac88eb8623392150&rating=R&size=80&default=img%3Ablank'
+        )
\ No newline at end of file

=== added file 'threadedcomments/tests/threadedcomments_urls.py'
--- threadedcomments/tests/threadedcomments_urls.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/tests/threadedcomments_urls.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns("",
+    url(r"", include("threadedcomments.urls")),
+)
\ No newline at end of file

=== added file 'threadedcomments/tests/views_tests.py'
--- threadedcomments/tests/views_tests.py	1970-01-01 00:00:00 +0000
+++ threadedcomments/tests/views_tests.py	2016-06-28 17:58:37 +0000
@@ -0,0 +1,833 @@
+import datetime
+
+from xml.dom.minidom import parseString
+
+from django.core import mail
+from django.core.urlresolvers import reverse
+from django.template import Context, Template
+from django.test import TestCase
+from django.utils.simplejson import loads
+
+from django.contrib.auth.models import User
+from django.contrib.contenttypes.models import ContentType
+
+from threadedcomments.models import FreeThreadedComment, ThreadedComment, TestModel
+from threadedcomments.models import MARKDOWN, TEXTILE, REST, PLAINTEXT
+from threadedcomments.templatetags import threadedcommentstags as tags
+
+
+__all__ = ("ViewsTestCase",)
+
+
+class ViewsTestCase(TestCase):
+    urls = "threadedcomments.tests.threadedcomments_urls"
+    
+    def test_freecomment_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_free_comment', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id
+        })
+        response = self.client.post(url, {
+            'comment': 'test1',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx',
+            'next': '/'
+        })
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test1',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_preview(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_free_comment', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test1',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx',
+            'next': '/',
+            'preview' : 'True'
+        })
+        self.assertEquals(len(response.content) > 0, True)
+    
+    def test_freecomment_edit(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_edit', kwargs={
+            'edit_id': comment.pk
+        })
+        
+        response = self.client.post(url, {
+            'comment' : 'test1_edited',
+            'name' : 'eric',
+            'website' : 'http://www.eflorenzano.com/',
+            'email' : 'floguy@xxxxxxxxx',
+            'next' : '/'
+        })
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test1_edited',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_edit_with_preview(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            website = "http://oebfare.com/";,
+            comment = "My test free comment!",
+            ip_address = '127.0.0.1',
+        )
+        
+        url = reverse('tc_free_comment_edit', kwargs={
+            'edit_id': comment.pk
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test1_edited',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx',
+            'next': '/',
+            'preview': 'True'
+        })
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://oebfare.com/',
+            'comment': u'My test free comment!',
+            'name': u'',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'',
+            'is_approved': False
+        })
+        self.assertEquals(len(response.content) > 0, True)
+    
+    def test_freecomment_json_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_free_comment_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'ajax': 'json'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test2',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx'
+        })
+        tmp = loads(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test2',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_json_edit(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_edit_ajax',kwargs={
+            'edit_id': comment.pk,
+            'ajax': 'json'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test2_edited',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx'
+        })
+        tmp = loads(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test2_edited',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_xml_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_free_comment_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'ajax': 'xml'
+        })
+        
+        response = self.client.post(url, {'comment' : 'test3', 'name' : 'eric', 'website' : 'http://www.eflorenzano.com/', 'email' : 'floguy@xxxxxxxxx', 'next' : '/'})
+        tmp = parseString(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test3',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_xml_edit(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_edit_ajax', kwargs={
+            'edit_id': comment.pk,
+            'ajax': 'xml'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test2_edited',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx'
+        })
+        tmp = parseString(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test2_edited',
+            'name': u'eric',
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_child_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_parent', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'parent_id': parent.id
+        })
+        response = self.client.post(url, {
+            'comment': 'test4',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx',
+            'next' : '/'
+        })
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test4',
+            'name': u'eric',
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_child_json_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_parent_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id, 
+            'parent_id': parent.id,
+            'ajax': 'json'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test5',
+            'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx'
+        })
+        tmp = loads(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test5',
+            'name': u'eric',
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def test_freecomment_child_xml_create(self):
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = '127.0.0.1',
+            comment = "My test free comment!",
+        )
+        
+        url = reverse('tc_free_comment_parent_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id, 
+            'parent_id': parent.id,
+            'ajax': 'xml'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test6', 'name': 'eric',
+            'website': 'http://www.eflorenzano.com/',
+            'email': 'floguy@xxxxxxxxx'
+        })
+        tmp = parseString(response.content)
+        o = FreeThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'website': u'http://www.eflorenzano.com/',
+            'comment': u'test6',
+            'name': u'eric',
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+            'email': u'floguy@xxxxxxxxx',
+            'is_approved': False
+        })
+    
+    def create_user_and_login(self):
+        user = User.objects.create_user(
+            'testuser',
+            'testuser@xxxxxxxxx',
+            'password',
+        )
+        user.is_active = True
+        user.save()
+        self.client.login(username='testuser', password='password')
+        return user
+    
+    def test_comment_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_comment', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test7',
+            'next' : '/'
+        })
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test7',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_preview(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_comment', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test7',
+            'next' : '/',
+            'preview': 'True'
+        })
+        self.assertEquals(len(response.content) > 0, True)
+    
+    def test_comment_edit(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_edit', kwargs={
+            'edit_id': comment.pk,
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test7_edited',
+            'next' : '/',
+        })
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test7_edited',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_edit_with_preview(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_edit', kwargs={
+            'edit_id': comment.pk,
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test7_edited',
+            'next': '/',
+            'preview': 'True'
+        })
+        
+        self.assertEquals(len(response.content) > 0, True)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'My test comment!',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_json_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_comment_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'ajax': 'json'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test8'
+        })
+        tmp = loads(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test8',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_json_edit(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_edit_ajax', kwargs={
+            'edit_id': comment.pk,
+            'ajax': 'json',
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test8_edited'
+        })
+        tmp = loads(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test8_edited',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_xml_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        url = reverse('tc_comment_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'ajax': 'xml'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test9'
+        })
+        tmp = parseString(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test9',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_xml_edit(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        comment = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_edit_ajax', kwargs={
+            'edit_id': comment.pk,
+            'ajax': 'xml',
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test8_edited'
+        })
+        tmp = parseString(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test8_edited',
+            'is_approved': False,
+            'parent': None,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_child_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_parent', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'parent_id': parent.id
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test10',
+            'next' : '/'
+        })
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test10',
+            'is_approved': False,
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_child_json_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_parent_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id, 
+            'parent_id': parent.id,
+            'ajax' : 'json'
+        })
+        
+        response = self.client.post(url, {
+            'comment' : 'test11'
+        })
+        tmp = loads(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test11',
+            'is_approved': False,
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_comment_child_xml_create(self):
+        
+        user = self.create_user_and_login()
+        
+        topic = TestModel.objects.create(name="Test2")
+        content_type = ContentType.objects.get_for_model(topic)
+        
+        parent = ThreadedComment.objects.create_for_object(topic,
+            user = user,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        
+        url = reverse('tc_comment_parent_ajax', kwargs={
+            'content_type': content_type.id,
+            'object_id': topic.id,
+            'parent_id': parent.id,
+            'ajax' : 'xml'
+        })
+        
+        response = self.client.post(url, {
+            'comment': 'test12'
+        })
+        tmp = parseString(response.content)
+        o = ThreadedComment.objects.latest('date_submitted').get_base_data(show_dates=False)
+        self.assertEquals(o, {
+            'comment': u'test12',
+            'is_approved': False,
+            'parent': parent,
+            'markup': u'plaintext',
+            'content_object': topic,
+            'user': user,
+            'is_public': True,
+            'ip_address': u'127.0.0.1',
+        })
+    
+    def test_freecomment_delete(self):
+        
+        user = User.objects.create_user(
+            'testuser',
+            'testuser@xxxxxxxxx',
+            'password',
+        )
+        user.is_active = True
+        user.save()
+        self.client.login(username='testuser', password='password')
+        
+        topic = TestModel.objects.create(name="Test2")
+        
+        comment = FreeThreadedComment.objects.create_for_object(topic,
+            ip_address = u'127.0.0.1',
+            comment = "My test comment!",
+        )
+        deleted_id = comment.id
+        
+        url = reverse('tc_free_comment_delete', kwargs={
+            'object_id': comment.id,
+        })
+        
+        response = self.client.post(url, {'next': '/'})
+        o = response['Location'].split('?')[-1] == 'next=/freecomment/%d/delete/' % deleted_id
+        self.assertEquals(o, True)
+        
+        # become super user and try deleting comment
+        user.is_superuser = True
+        user.save()
+        
+        response = self.client.post(url, {'next': '/'})
+        self.assertEquals(response['Location'], 'http://testserver/')
+        self.assertRaises(
+            FreeThreadedComment.DoesNotExist,
+            lambda: FreeThreadedComment.objects.get(id=deleted_id)
+        )
+        
+        # re-create comment
+        comment.save()
+        
+        response = self.client.get(url, {'next' : '/'})
+        self.assertEquals(len(response.content) > 0, True)
+        
+        o = FreeThreadedComment.objects.get(id=deleted_id) != None
+        self.assertEquals(o, True)
+    
+    def test_comment_

Follow ups