widelands-dev team mailing list archive
-
widelands-dev team
-
Mailing list archive
-
Message #07817
[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. > to &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"),
- (">:-)", "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
+ ('>:-)', '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 }}&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="">
</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 © 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 II™ (© 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 & Music Development</a></li>
- <li><a href="{% url pybb_forum 11 %}">Homepage</a></li>
- <li><a href="{% url pybb_forum 12 %}">Translations & 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 & Music Development</a></li>
+ <li><a href="{% url 'pybb_forum' 11 %}">Homepage</a></li>
+ <li><a href="{% url 'pybb_forum' 12 %}">Translations & 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> »
-<a href="{% url news_index %}{{ month|date:"Y" }}/" class="invertedColor">{{ month|date:"Y" }}</a> »
-<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> »
+<a href="{% url 'news_index' %}{{ month|date:"Y" }}/" class="invertedColor">{{ month|date:"Y" }}</a> »
+<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> »
-<a href="{% url news_index %}{{ year }}" class="invertedColor">{{ year }}</a>:
+<a href="{% url 'news_index' %}" class="invertedColor">News Archiv</a> »
+<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 }}">« {{ 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> »
+ <a href="{% url 'pybb_index' %}">Forums</a> »
{% if forum %}
<a href="{{ forum.category.get_absolute_url }}">{{ forum.category.name }}</a> »
{{ 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> » {{category.name}}
+ <a href="{% url 'pybb_index' %}">Forums</a> » {{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> »
+ <a href="{% url 'pybb_index' %}">Forums</a> »
{% pybb_link post.topic.forum.category %} »
<a href="{{ post.topic.forum.get_absolute_url }}">{{ post.topic.forum.name }}</a> »
{{ 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> »
+ <a href="{% url 'pybb_index' %}">Forums</a> »
{% pybb_link forum.category %} »
{{ 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> »
+ <a href="{% url 'pybb_index' %}">Forums</a> »
{% pybb_link topic.forum.category %} »
<a href="{{ topic.forum.get_absolute_url }}">{{ topic.forum.name }}</a> »
{{ 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 />
- <a href="{% url wlggz_matches %}">Last matches</a><br />
- <a href="{% url wlggz_ranking %}">GGZ ranking</a><br />
+ <a href="{% url 'wlggz_matches' %}">Last matches</a><br />
+ <a href="{% url 'wlggz_ranking' %}">GGZ ranking</a><br />
<br />
{% if user.is_authenticated %}
- <a href="{% url wlggz_userstats %}">View your ggz statistics</a><br />
- <a href="{% url wlggz_userinfo %}">About your ggz account</a><br />
- <a href="{% url wlggz_changepw %}">Change your ggz password</a><br />
+ <a href="{% url 'wlggz_userstats' %}">View your ggz statistics</a><br />
+ <a href="{% url 'wlggz_userinfo' %}">About your ggz account</a><br />
+ <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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
- <a href="{% url wlhelp_buildings tribe.name %}">Buildings</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_buildings' tribe.name %}">Buildings</a> »
{{ 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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
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> »
+<a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
{{ 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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
- <a href="{% url wlhelp_wares tribe.name %}">Wares</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_wares' tribe.name %}">Wares</a> »
{{ 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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
- <a href="{% url wlhelp_workers tribe.name %}">Workers</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_workers' tribe.name %}">Workers</a> »
{{ 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> »
- <a href="{% url wlhelp_tribe_details tribe.name %}">{{ tribe.displayname }}</a> »
+ <a href="{% url 'wlhelp_index' %}">Encyclopedia</a> »
+ <a href="{% url 'wlhelp_tribe_details' tribe.name %}">{{ tribe.displayname }}</a> »
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> » {{ map.name }}
+ <a href="{% url 'wlmaps_index' %}">Maps</a> » {{ 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> » Upload
+ <a href="{% url 'wlmaps_index' %}">Maps</a> » 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 }} » {{b.displayname}}</a></li>
+ <li><a href="{% url 'wlhelp_building_details' b.tribe.name b.name %}">{{ b.tribe.displayname }} » {{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 }} » {{w.displayname}}</a></li>
+ <li><a href="{% url 'wlhelp_ware_details' w.tribe.name w.name %}">{{ w.tribe.displayname }} » {{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'[<ThreadedComment: Test 3>, <ThreadedComment: Test 2>]'
+ )
+
+ 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'[<FreeThreadedComment: Test 3>, <FreeThreadedComment: Test 2>]'
+ )
+
+ 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'[<ThreadedComment: Eric comment>]'
+ )
+ 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'<b>This is Funny</b>'
+ )
+
+ 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'<b>This is Funny</b>'
+ )
+
+ 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