← Back to team overview

widelands-dev team mailing list archive

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

 

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

Requested reviews:
  Widelands Developers (widelands-dev)
Related bugs:
  Bug #1637632 in Widelands Website: "Cleanup notification_notice"
  https://bugs.launchpad.net/widelands-website/+bug/1637632
  Bug #1663155 in Widelands Website: "Forum: Speed up creating of new topics"
  https://bugs.launchpad.net/widelands-website/+bug/1663155

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

Since it seems nobody misses the old notifications:

This branch contains following changes:

- Remove table Notice from notifications (including removing related code). The 
  remaining notification app is now used to enable e-mailing for particular 
  things by each user. 
- Removing code of notification feed. This was never used afaik.
- Add e-mail notification for new uploaded maps (has to be explicitly enabled 
  by the user)
- Some Notification e-mails are deferred to speed up website response and get sent by 
  running the django command 'emit_notices'. 
  This command is already used in a daily cron job (obviously useless). I 
  would suggest to run it in a hourly cron job. The emails for notice types 
  which are deferred are:
  - Forum new Topic
  - Forum new Post
  - A new map is available (new notice type)
  We have to watch how speedup creating of new topics will affect creating of 
  spam topics. Maybe we should consider adding a timeout for creating new 
  topics (yes i know SirVer doesn't like that)
- Rework the users view of his notification settings to show the settings 
  per app
- The notice type "wiki_article_edited" is removed, instead the already existing 
  type "Observed Article Changed" is used
- The admin page of Notice settings gives now the possibility to get the 
  settings of a particular user. Just search for the username to see his 
  settings. If there are not all possible settings shown, the user has never 
  entered his notification page, because notice settings for a user are 
  automatically created if the user calls the notifications page
- The admin page of pybb topics shows now all subscribers (users who observe a 
  topic) in a list

When merging this the follwing notice types could be deleted (has to be done 
over the admin page):

messages_recovered
messages_deleted
messages_replied
messages_sent
wiki_article_edited

Removing those over the admin page ensures also removing the corresponding notice_type 
settings for each user.

There is one remaining Django warning when the django command 'emit_notices' 
is executed, which i could not solve:

/home/kaputtnik/wlwebsite/code/notifications_cleanup/notification/engine.py:49: RuntimeWarning: Pickled model instance's Django version is not specified.
  str(queued_batch.pickled_data).decode('base64'))

All i found in the web seems to be unrelated. It is just a warning and think it 
is not really importand for us...

After merging './manage.py migrate' has to be run to apply the database related changes.

I have prepaired the alpha site for testing. There is the database used which i created for testing the upgrade onto django 1.8, so there is not much content. I did activate that database because there are only a few 'new topic subscribers' so other people will not be spammed with new topics from the alpha site.

For testing purposes be sure to enable all available notifications in http://alpha.widelands.org/notification/

Then create a topic, answer to an existing topic or upload a map. Also pm's should be testet. Sending of deferred e-mails (new topics, posts, maps) is currently done by running the command './manage.py emit_notices' by hand. I will run it this evening, so then you will receive some messages from alpha.widelands.org if you have done some testing over there.
-- 
Your team Widelands Developers is requested to review the proposed merge of lp:~widelands-dev/widelands-website/notifications_cleanup into lp:widelands-website.
=== modified file 'media/css/notice.css'
--- media/css/notice.css	2012-05-08 21:52:15 +0000
+++ media/css/notice.css	2017-05-01 10:49:01 +0000
@@ -12,7 +12,6 @@
 .notifications th {
 	border: none;
 	padding: 4px;
-	padding-top: 20px;
 	text-align: left;
 	font-weight: normal;
 }

=== added file 'news/migrations/0002_auto_20170417_1857.py'
--- news/migrations/0002_auto_20170417_1857.py	1970-01-01 00:00:00 +0000
+++ news/migrations/0002_auto_20170417_1857.py	2017-05-01 10:49:01 +0000
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('news', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='post',
+            name='body',
+            field=models.TextField(help_text=b'Text entered here will be rendered using Markdown', verbose_name='body'),
+        ),
+    ]

=== modified file 'notification/README'
--- notification/README	2016-05-17 19:28:38 +0000
+++ notification/README	2017-05-01 10:49:01 +0000
@@ -1,7 +1,12 @@
 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 
+It is included as a widelands app because the new version is 
 incompatible with our old data.
 
+Year 2017:
+The ability to store notices is removed and therefor it acts only as an app
+to send e-mails for observed items.
+
+
 See the file LICENSE for Copyright notice.
 
 Original Description:

=== modified file 'notification/admin.py'
--- notification/admin.py	2016-12-13 18:28:51 +0000
+++ notification/admin.py	2017-05-01 10:49:01 +0000
@@ -1,5 +1,6 @@
 from django.contrib import admin
-from notification.models import NoticeType, NoticeSetting, Notice, ObservedItem
+from notification.models import NoticeType, NoticeSetting, ObservedItem
+from django.utils.translation import ugettext_lazy as _
 
 
 class NoticeTypeAdmin(admin.ModelAdmin):
@@ -7,15 +8,20 @@
 
 
 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')
-
+    search_fields = ['user__username',]
+    list_display = ('user', 'notice_type', 'medium', 'send')
+
+
+class ObserverdItemAdmin(admin.ModelAdmin):
+    readonly_fields = ('observed_object', 'content_type', 'object_id')
+    search_fields = ['user__username', 'notice_type__label']
+    list_display = ('user', 'notice_type', 'content_type', 'get_content_object')
+    fieldsets = (
+            (None, {'fields': ('user',)}),
+            (_('Observed object'), {'fields': ('observed_object', 'content_type', 'object_id')}),
+            (_('Settings'), {'fields': ('added', 'notice_type', 'signal')}),
+            )
 
 admin.site.register(NoticeType, NoticeTypeAdmin)
 admin.site.register(NoticeSetting, NoticeSettingAdmin)
-admin.site.register(Notice, NoticeAdmin)
-admin.site.register(ObservedItem)
+admin.site.register(ObservedItem, ObserverdItemAdmin)

=== removed file 'notification/atomformat.py'
--- notification/atomformat.py	2016-12-13 18:28:51 +0000
+++ notification/atomformat.py	1970-01-01 00:00:00 +0000
@@ -1,551 +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:
-            # @@@ really we should allow a feed to define its "start" for this case
-            return datetime.now()
-
-    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})
-                # write unescaped -- it had better be well-formed XML
-                handler._write(text)
-                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)
-                # write unescaped -- it had better be well-formed XML
-                handler._write(text)
-                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)

=== removed file 'notification/context_processors.py'
--- notification/context_processors.py	2016-12-13 18:28:51 +0000
+++ notification/context_processors.py	1970-01-01 00:00:00 +0000
@@ -1,10 +0,0 @@
-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 {}

=== removed file 'notification/decorators.py'
--- notification/decorators.py	2016-12-13 18:28:51 +0000
+++ notification/decorators.py	1970-01-01 00:00:00 +0000
@@ -1,65 +0,0 @@
-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

=== modified file 'notification/engine.py'
--- notification/engine.py	2016-12-13 18:28:51 +0000
+++ notification/engine.py	2017-05-01 10:49:01 +0000
@@ -49,7 +49,10 @@
                     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)
+                    # FrankU: commented, because not all users get e-mailed
+                    # and to supress useless logging
+                    # 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(

=== removed file 'notification/feeds.py'
--- notification/feeds.py	2016-12-13 18:28:51 +0000
+++ notification/feeds.py	1970-01-01 00:00:00 +0000
@@ -1,75 +0,0 @@
-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]

=== modified file 'notification/management/commands/emit_notices.py'
--- notification/management/commands/emit_notices.py	2016-12-13 18:28:51 +0000
+++ notification/management/commands/emit_notices.py	2017-05-01 10:49:01 +0000
@@ -10,6 +10,7 @@
     help = 'Emit queued notices.'
 
     def handle_noargs(self, **options):
-        logging.basicConfig(level=logging.DEBUG, format='%(message)s')
+        # Franku: Uncomment for debugging purposes
+        # logging.basicConfig(level=logging.DEBUG, format='%(message)s')
         logging.info('-' * 72)
         send_all()

=== added file 'notification/migrations/0002_auto_20170417_1857.py'
--- notification/migrations/0002_auto_20170417_1857.py	1970-01-01 00:00:00 +0000
+++ notification/migrations/0002_auto_20170417_1857.py	2017-05-01 10:49:01 +0000
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('notification', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='notice',
+            name='notice_type',
+        ),
+        migrations.RemoveField(
+            model_name='notice',
+            name='user',
+        ),
+        migrations.DeleteModel(
+            name='Notice',
+        ),
+    ]

=== modified file 'notification/models.py'
--- notification/models.py	2016-12-13 18:28:51 +0000
+++ notification/models.py	2017-05-01 10:49:01 +0000
@@ -68,7 +68,11 @@
 
 class NoticeSetting(models.Model):
     """Indicates, for a given user, whether to send notifications of a given
-    type to a given medium."""
+    type to a given medium.
+
+    Notice types for each user are added if he/she enters the notification page.
+
+    """
 
     user = models.ForeignKey(User, verbose_name=_('user'))
     notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
@@ -82,6 +86,12 @@
 
 
 def get_notification_setting(user, notice_type, medium):
+    """Return NotceSetting for a specific user. If a NoticeSetting of
+    given NoticeType didn't exist for given user, a NoticeSetting is created.
+
+    If a new NoticeSetting is created, the field 'default' of a NoticeType
+    decides whether NoticeSetting.send is True or False as default.
+    """
     try:
         return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
     except NoticeSetting.DoesNotExist:
@@ -91,80 +101,17 @@
         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)
+def get_observers_for(notice_type):
+    """ Returns the list of users which wants to get a message (email) for this
+    type of notice."""
+    settings = NoticeSetting.objects.filter(notice_type__label=notice_type).filter(send=True)
+    users = []
+    for s in settings:
+        users.append(s.user)
+    return users
 
 
 class NoticeQueueBatch(models.Model):
@@ -262,66 +209,65 @@
     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)
-
+    # FrankU: This try statement is added to pass notice types
+    # which are deleted but used by third party apps to create a notice
+    try:
+        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',
+        )  # 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,
+                '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)
+
+            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)
+    except NoticeType.DoesNotExist:
+        pass
 
 def send(*args, **kwargs):
     """A basic interface around both queue and send_now.
@@ -409,6 +355,15 @@
     def send_notice(self):
         send([self.user], self.notice_type.label,
              {'observed': self.observed_object})
+        
+    def get_content_object(self):
+        """
+        taken from threadedcomments:
+
+        Wrapper around the GenericForeignKey due to compatibility reasons
+        and due to ``list_display`` limitations.
+        """
+        return self.observed_object
 
 
 def observe(observed, observer, notice_type_label, signal='post_save'):

=== modified file 'notification/urls.py'
--- notification/urls.py	2016-12-13 18:28:51 +0000
+++ notification/urls.py	2017-05-01 10:49:01 +0000
@@ -1,10 +1,7 @@
 from django.conf.urls import url
 
-from notification.views import notices, mark_all_seen, feed_for_user, single
+from notification.views import notice_settings
 
 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'),
+    url(r'^$', notice_settings, name='notification_notices'),
 ]

=== modified file 'notification/views.py'
--- notification/views.py	2016-12-13 18:28:51 +0000
+++ notification/views.py	2017-05-01 10:49:01 +0000
@@ -1,30 +1,19 @@
-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.shortcuts import render_to_response
 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 = []
+def notice_settings(request):
+    app_tables = {}
+    for notice_type in NoticeType.objects.all().order_by('label'):
+        # Assuming each notice_type.label begins with the name of the app
+        # followed by an underscore:
+        app = notice_type.label.partition('_')[0]
+        app_tables.setdefault(app, [])
+        checkbox_values = []
         for medium_id, medium_display in NOTICE_MEDIA:
             form_label = '%s_%s' % (notice_type.label, medium_id)
             setting = get_notification_setting(
@@ -35,65 +24,11 @@
                 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 = {
+            checkbox_values.append((form_label, setting.send))
+
+        app_tables[app].append({'notice_type': notice_type, 'html_values': checkbox_values})
+
+    return render_to_response('notification/notice_settings.html', {
         '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,
+        'app_tables': app_tables,
     }, 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'))

=== modified file 'pybb/admin.py'
--- pybb/admin.py	2016-12-15 10:43:41 +0000
+++ pybb/admin.py	2017-05-01 10:49:01 +0000
@@ -48,7 +48,9 @@
         }
         ),
     )
-
+    
+class SubscribersInline(admin.TabularInline):
+    model = Topic.subscribers.through
 
 class TopicAdmin(admin.ModelAdmin):
     list_display = ['name', 'forum', 'created', 'head', 'is_hidden']
@@ -59,14 +61,13 @@
     fieldsets = (
         (None, {
             'fields': ('forum', 'name', 'user', ('created', 'updated'))
-        }
-        ),
+        }),
         (_('Additional options'), {
             'classes': ('collapse',),
-            'fields': (('views',), ('sticky', 'closed'), 'subscribers')
-        }
-        ),
+            'fields': (('views',), ('sticky', 'closed'),)
+        }),
     )
+    inlines = [ SubscribersInline, ]
 
 
 class PostAdmin(admin.ModelAdmin):

=== modified file 'pybb/forms.py'
--- pybb/forms.py	2017-01-23 13:01:31 +0000
+++ pybb/forms.py	2017-05-01 10:49:01 +0000
@@ -10,9 +10,7 @@
 from pybb.models import Topic, Post, PrivateMessage, Attachment
 from pybb import settings as pybb_settings
 from django.conf import settings
-from notification.models import send
-from django.core.mail import send_mail
-from django.contrib.sites.models import Site
+from notification.models import send, get_observers_for
 
 
 class AddPostForm(forms.ModelForm):
@@ -94,11 +92,11 @@
 
         if not hidden:
             if topic_is_new:
-                send(User.objects.all(), 'forum_new_topic',
-                     {'topic': topic, 'post': post, 'user': topic.user})
+                send(get_observers_for('forum_new_topic'), 'forum_new_topic',
+                     {'topic': topic, 'post': post, 'user': topic.user}, queue = True)
             else:
                 send(self.topic.subscribers.all(), 'forum_new_post',
-                     {'post': post, 'topic': topic, 'user': post.user})
+                     {'post': post, 'topic': topic, 'user': post.user}, queue = True)
 
         return post
 

=== modified file 'pybb/management/pybb_notifications.py'
--- pybb/management/pybb_notifications.py	2016-12-13 18:28:51 +0000
+++ pybb/management/pybb_notifications.py	2017-05-01 10:49:01 +0000
@@ -14,6 +14,8 @@
                                         _('Forum New Post'),
                                         _('a new comment has been posted to a topic you observe'))
 
+    # TODO (Franku): post_syncdb is deprecated since Django 1.7
+    # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
     signals.post_syncdb.connect(create_notice_types,
                                 sender=notification)
 except ImportError:

=== modified file 'templates/notification/email_body.txt'
--- templates/notification/email_body.txt	2015-03-15 20:05:49 +0000
+++ templates/notification/email_body.txt	2017-05-01 10:49:01 +0000
@@ -1,5 +1,5 @@
 {% load i18n %}{% blocktrans %}You have received the following notice from {{ current_site }}:
 
 {{ message }}
-To see other notices or change how you receive notifications, please go to {{ notices_url }}.
-{% endblocktrans %}
\ No newline at end of file
+To change how you receive notifications, please go to {{ notices_url }}.
+{% endblocktrans %}

=== removed file 'templates/notification/forum_new_post/notice.html'
--- templates/notification/forum_new_post/notice.html	2016-06-06 18:26:47 +0000
+++ templates/notification/forum_new_post/notice.html	1970-01-01 00:00:00 +0000
@@ -1,5 +0,0 @@
-{% 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 %}

=== removed file 'templates/notification/forum_new_topic/notice.html'
--- templates/notification/forum_new_topic/notice.html	2016-06-06 18:26:47 +0000
+++ templates/notification/forum_new_topic/notice.html	1970-01-01 00:00:00 +0000
@@ -1,4 +0,0 @@
-{% 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 %}

=== removed file 'templates/notification/full.html'
--- templates/notification/full.html	2009-02-20 16:46:21 +0000
+++ templates/notification/full.html	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
\ No newline at end of file

=== removed directory 'templates/notification/messages_deleted'
=== removed file 'templates/notification/messages_deleted/full.txt'
--- templates/notification/messages_deleted/full.txt	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_deleted/full.txt	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans %}You have deleted the message '{{ message }}'.{% endblocktrans %}

=== removed file 'templates/notification/messages_deleted/notice.html'
--- templates/notification/messages_deleted/notice.html	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_deleted/notice.html	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have deleted the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}

=== removed file 'templates/notification/messages_received/notice.html'
--- templates/notification/messages_received/notice.html	2016-03-02 21:02:38 +0000
+++ templates/notification/messages_received/notice.html	1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
-{% load i18n %}
-{% 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 %}

=== removed directory 'templates/notification/messages_recovered'
=== removed file 'templates/notification/messages_recovered/full.txt'
--- templates/notification/messages_recovered/full.txt	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_recovered/full.txt	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans %}You have recovered the message '{{ message }}'.{% endblocktrans %}

=== removed file 'templates/notification/messages_recovered/notice.html'
--- templates/notification/messages_recovered/notice.html	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_recovered/notice.html	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have recovered the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}

=== removed directory 'templates/notification/messages_replied'
=== removed file 'templates/notification/messages_replied/full.txt'
--- templates/notification/messages_replied/full.txt	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_replied/full.txt	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans with message.parent_msg as message_parent_msg and message.recipient as message_recipient %}You have replied to '{{ message_parent_msg }}' from {{ message_recipient }}.{% endblocktrans %}

=== removed file 'templates/notification/messages_replied/notice.html'
--- templates/notification/messages_replied/notice.html	2016-03-02 21:02:38 +0000
+++ templates/notification/messages_replied/notice.html	1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
-{% load i18n %}
-{% 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 %}

=== removed file 'templates/notification/messages_reply_received/notice.html'
--- templates/notification/messages_reply_received/notice.html	2016-03-02 21:02:38 +0000
+++ templates/notification/messages_reply_received/notice.html	1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
-{% load i18n %}
-{% 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 %}

=== removed directory 'templates/notification/messages_sent'
=== removed file 'templates/notification/messages_sent/full.txt'
--- templates/notification/messages_sent/full.txt	2009-02-26 11:32:18 +0000
+++ templates/notification/messages_sent/full.txt	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans with message.recipient as message_recipient %}You have sent the message '{{ message }}' to {{ message_recipient }}.{% endblocktrans %}

=== removed file 'templates/notification/messages_sent/notice.html'
--- templates/notification/messages_sent/notice.html	2016-03-02 21:02:38 +0000
+++ templates/notification/messages_sent/notice.html	1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
-{% load i18n %}
-{% 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 %}

=== removed file 'templates/notification/notice.html'
--- templates/notification/notice.html	2009-02-20 16:46:21 +0000
+++ templates/notification/notice.html	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
\ No newline at end of file

=== renamed file 'templates/notification/notices.html' => 'templates/notification/notice_settings.html'
--- templates/notification/notices.html	2017-03-06 20:15:27 +0000
+++ templates/notification/notice_settings.html	2017-05-01 10:49:01 +0000
@@ -9,39 +9,7 @@
 {% endblock %}
 
 {% block content %}
-<h1>{% trans "Notifications" %}</h1>
-{% comment "Testing if this is used by users" %}
-<div class="blogEntry">
-{% autopaginate notices %}
-
-{% if notices %}
-	<a href="{% url 'notification_mark_all_seen' %}" class="posRight small">{% trans "Mark all as seen" %}</a>
-	{% paginate %}
-
-	{# TODO: get timezone support working with regroup #}
-	{% regroup notices by added.date as notices_by_date %}
-	{% for date in notices_by_date %}
-		<br />
-		<h3>{{ date.grouper|naturalday:_("MONTH_DAY_FORMAT")|title }}</h3>
-		<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="text">{{ notice.message|safe }}</td>
-				<td class="date">{{ notice.added|custom_date:user }}</td>
-			</tr>
-		{% endfor %}
-		</table>
-	{% endfor %}
-	<br />
-	{% paginate %}
-{% else %}
-	<p>
-		{% trans "No notifications." %}
-	</p>
-{% endif %}
-</div>
-{% endcomment %}
+<h1>{% trans "Notification Settings" %}</h1>
 
 <div class="blogEntry">
 	<h2>{% trans "Settings" %}</h2>
@@ -57,32 +25,34 @@
 		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 %}
-
-	<form method="POST" action="."> {# doubt this easy to do in uni-form #}
+	<form method="POST" action=".">
+	{% for app, settings in app_tables.items %}
+		<h3>{{ app|capfirst }} </h3>
 		<table class="notifications">
 			<tr>
 				<th class="small">{% trans "Notification Type" %}</th>
-			{% for header in notice_settings.column_headers %}
-				<th class="small">{{ header }}</th>
-			{% endfor %}
+				{% for header in column_headers %}
+					<th class="small">{{ header }}</th>
+				{% endfor %}
 			</tr>
-		{% for row in notice_settings.rows %}
+			{% for setting in settings %}
 			<tr class="{% cycle "odd" "even" %}">
 				<td>
-					{% trans row.notice_type.display %}<br />
-					{% trans row.notice_type.description %}
-				</td>
-			{% for cell in row.cells %}
-				<td>
-					<input type="checkbox" name="{{ cell.0 }}" {% if cell.1 %}checked="checked" {% endif %}/>
-				</td>
+					{% trans setting.notice_type.display %}<br />
+					{% trans setting.notice_type.description %}
+				</td>
+				<td style="width: 5em;">
+					{% for html_value in setting.html_values %}
+						<input type="checkbox" name="{{ html_value.0 }}" {% if html_value.1 %}checked="checked" {% endif %}/>
+					{% endfor %}
+				</td>
+			</tr>
 			{% endfor %}
-			</tr>
-		{% endfor %}
 		</table>
 		<br />
-		{% csrf_token %}
-		<input type="submit" value="{% trans "Change" %}" />
+	{% endfor %}
+	{% csrf_token %}
+	<input class="posRight" type="submit" value="{% trans "Change" %}" />
 	</form>
 </div>
 {% endblock %}

=== removed file 'templates/notification/single.html'
--- templates/notification/single.html	2012-05-08 21:52:15 +0000
+++ templates/notification/single.html	1970-01-01 00:00:00 +0000
@@ -1,40 +0,0 @@
-{% extends "notification/base.html" %}
-
-{% load humanize i18n %}
-{% load custom_date %}
-
-{% block title %}
-{% trans "Notification "%} - {{ block.super }}
-{% endblock %}
-
-{% block content %}
-<h1>{% trans "Notification" %}: {{ notice.notice_type.display }}</h1>
-<div class="blogEntry">
-	<table>
-		<tr>
-			<td class="grey">Notice:</td>
-			<td>{{ notice.message|safe }}</td>
-		</tr>
-		<tr>
-			<td class="grey">Notice Type:</td>
-			<td>{{ notice.notice_type.display }}</td>
-		</tr>
-		<tr>
-			<td class="grey">Date of Notice:</td>
-			<td>{{ notice.added }}</td>
-		</tr>
-		<tr>
-			<td class="grey">Notice Unseen:</td>
-			<td>{{ notice.unseen }}</td>
-		</tr>
-		<tr>
-			<td class="grey">Notice Archiv:</td>
-			<td>{{ notice.archived }}</td>
-		</tr>
-		<tr>
-			<td class="grey">Notice Site:</td>
-			<td>{{ notice.on_site }}</td>
-		</tr>
-	</table>
-</div>
-{% endblock %}

=== removed file 'templates/notification/wiki_article_edited/notice.html'
--- templates/notification/wiki_article_edited/notice.html	2016-06-06 18:26:47 +0000
+++ templates/notification/wiki_article_edited/notice.html	1970-01-01 00:00:00 +0000
@@ -1,4 +0,0 @@
-{% 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 %}

=== removed file 'templates/notification/wiki_observed_article_changed/notice.html'
--- templates/notification/wiki_observed_article_changed/notice.html	2009-02-20 10:11:49 +0000
+++ templates/notification/wiki_observed_article_changed/notice.html	1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
-{% load i18n %}{% blocktrans with observed.get_absolute_url as article_url %}The wiki article <a href="{{ article_url }}">{{ observed }}</a>, that you observe, has changed.{% endblocktrans %}

=== removed file 'templates/notification/wiki_revision_reverted/notice.html'
--- templates/notification/wiki_revision_reverted/notice.html	2009-02-20 10:11:49 +0000
+++ templates/notification/wiki_revision_reverted/notice.html	1970-01-01 00:00:00 +0000
@@ -1,2 +0,0 @@
-{% load i18n %}
-{% blocktrans with revision.get_absolute_url as revision_url and article.get_absolute_url as article_url %}Your revision <a href="{{ revision_url }}">{{ revision }}</a> on <a href="{{ article_url }}">{{ article }}</a> has been reverted.{% endblocktrans %}

=== added directory 'templates/notification/wlmaps_new_map'
=== added file 'templates/notification/wlmaps_new_map/full.txt'
--- templates/notification/wlmaps_new_map/full.txt	1970-01-01 00:00:00 +0000
+++ templates/notification/wlmaps_new_map/full.txt	2017-05-01 10:49:01 +0000
@@ -0,0 +1,7 @@
+{% load i18n %}A new map was uploaded to the Website by {{ user }}:
+{% blocktrans %}
+Mapname: {{ mapname }}
+Description: {{ uploader_comment }}
+–––––––––––
+Link to map: http://{{ current_site }}{{ url }}
+{% endblocktrans %}
\ No newline at end of file

=== modified file 'wiki/forms.py'
--- wiki/forms.py	2017-02-24 19:15:17 +0000
+++ wiki/forms.py	2017-05-01 10:49:01 +0000
@@ -8,6 +8,11 @@
 from wiki.models import Article
 from wiki.models import ChangeSet
 from settings import WIKI_WORD_RE
+try:
+    from notification import models as notification
+except:
+    notification = None
+
 
 wikiword_pattern = re.compile('^' + WIKI_WORD_RE + '$')
 
@@ -102,6 +107,8 @@
                 article.creator = editor
                 article.group = group
             article.save(*args, **kwargs)
+            if notification:
+                notification.observe(article, editor, 'wiki_observed_article_changed')
 
         # 4 - Create new revision
         changeset = article.new_revision(

=== modified file 'wiki/management.py'
--- wiki/management.py	2016-12-13 18:28:51 +0000
+++ wiki/management.py	2017-05-01 10:49:01 +0000
@@ -6,9 +6,6 @@
     from notification import models as notification
 
     def create_notice_types(app, created_models, verbosity, **kwargs):
-        notification.create_notice_type('wiki_article_edited',
-                                        _('Article Edited'),
-                                        _('your article has been edited'))
         notification.create_notice_type('wiki_revision_reverted',
                                         _('Article Revision Reverted'),
                                         _('your revision has been reverted'))
@@ -16,6 +13,8 @@
                                         _('Observed Article Changed'),
                                         _('an article you observe has changed'))
 
+    # TODO (Franku): post_syncdb is deprecated since Django 1.7
+    # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
     signals.post_syncdb.connect(create_notice_types,
                                 sender=notification)
 except ImportError:

=== added file 'wlmaps/management.py'
--- wlmaps/management.py	1970-01-01 00:00:00 +0000
+++ wlmaps/management.py	2017-05-01 10:49:01 +0000
@@ -0,0 +1,18 @@
+from django.db.models import signals
+
+from django.utils.translation import ugettext_noop as _
+
+try:
+    from notification import models as notification
+
+    def create_notice_types(app, created_models, verbosity, **kwargs):
+        notification.create_notice_type('wlmaps_new_map',
+                                        _('A new Map is available'),
+                                        _('a new map is available for download'),1)
+
+    # TODO (Franku): post_syncdb is deprecated since Django 1.7
+    # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
+    signals.post_syncdb.connect(create_notice_types,
+                                sender=notification)
+except ImportError:
+    print 'Skipping creation of NoticeTypes as notification app not found'

=== modified file 'wlmaps/models.py'
--- wlmaps/models.py	2016-08-11 18:13:59 +0000
+++ wlmaps/models.py	2017-05-01 10:49:01 +0000
@@ -5,6 +5,10 @@
 from django.contrib.auth.models import User
 from django.template.defaultfilters import slugify
 import datetime
+try:
+    from notification import models as notification
+except ImportError:
+    notification = None
 
 import settings
 if settings.USE_SPHINX:
@@ -64,4 +68,8 @@
             self.slug = slugify(self.name)
 
         map = super(Map, self).save(*args, **kwargs)
-        return map
+        if notification:
+            notification.send(notification.get_observers_for('wlmaps_new_map'), 'wlmaps_new_map',
+                              {'mapname': self.name, 'url': self.get_absolute_url(), 'user': self.uploader, 'uploader_comment': self.uploader_comment}, queue=True)
+
+        return map    


Follow ups