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

Proposed by kaputtnik
Status: Merged
Merged at revision: 456
Proposed branch: lp:~widelands-dev/widelands-website/notifications_cleanup
Merge into: lp:widelands-website
Diff against target: 1740 lines (+255/-1078)
42 files modified
media/css/notice.css (+0/-1)
news/migrations/0002_auto_20170417_1857.py (+19/-0)
notification/README (+6/-1)
notification/admin.py (+16/-10)
notification/atomformat.py (+0/-551)
notification/context_processors.py (+0/-10)
notification/decorators.py (+0/-65)
notification/engine.py (+4/-1)
notification/feeds.py (+0/-75)
notification/management/commands/emit_notices.py (+2/-1)
notification/migrations/0002_auto_20170417_1857.py (+25/-0)
notification/models.py (+87/-133)
notification/urls.py (+2/-5)
notification/views.py (+16/-81)
pybb/admin.py (+7/-6)
pybb/forms.py (+4/-6)
pybb/management/pybb_notifications.py (+2/-0)
templates/notification/email_body.txt (+2/-2)
templates/notification/forum_new_post/notice.html (+0/-5)
templates/notification/forum_new_topic/notice.html (+0/-4)
templates/notification/full.html (+0/-1)
templates/notification/maps_new_map/full.txt (+7/-0)
templates/notification/messages_deleted/full.txt (+0/-1)
templates/notification/messages_deleted/notice.html (+0/-1)
templates/notification/messages_received/notice.html (+0/-3)
templates/notification/messages_recovered/full.txt (+0/-1)
templates/notification/messages_recovered/notice.html (+0/-1)
templates/notification/messages_replied/full.txt (+0/-1)
templates/notification/messages_replied/notice.html (+0/-3)
templates/notification/messages_reply_received/notice.html (+0/-3)
templates/notification/messages_sent/full.txt (+0/-1)
templates/notification/messages_sent/notice.html (+0/-3)
templates/notification/notice.html (+0/-1)
templates/notification/notice_settings.html (+20/-50)
templates/notification/single.html (+0/-40)
templates/notification/wiki_article_edited/notice.html (+0/-4)
templates/notification/wiki_observed_article_changed/notice.html (+0/-1)
templates/notification/wiki_revision_reverted/notice.html (+0/-2)
wiki/forms.py (+7/-0)
wiki/management.py (+2/-3)
wlmaps/management.py (+18/-0)
wlmaps/models.py (+9/-1)
To merge this branch: bzr merge lp:~widelands-dev/widelands-website/notifications_cleanup
Reviewer Review Type Date Requested Status
SirVer Approve
Review via email: mp+323457@code.launchpad.net

Description of the change

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.

To post a comment you must log in.
Revision history for this message
SirVer (sirver) wrote :

Will review and test this this week. Thanks for continuing to bring the complexity of the homepage down!

Revision history for this message
kaputtnik (franku) wrote :
Revision history for this message
SirVer (sirver) wrote :

Some comments while testing:

- On http://alpha.widelands.org/notification/, the maps heading says Wlmaps. That should probably just read Maps.
- I sent myself a PM and got an email immediately.
- I subscribed to the forum and created a new topic.
- When uploading a map I got a 504 gateway timeout the first time. When trying again I got an "Internal server error" after a while, but the map was apparently successful uploaded.

After activating the alpha repo and running /var/www/django_projects/alpha/wlwebsite/code/widelands/manage.py emit_notices I got the two emails I was expecting (map upload and new topic). So it works! Great :)

[pickling]
I think the warning is fine and can be ignored here. Pickling is a way to serialize (i.e. convert to a string representation) a living Python object so that it can be stored somewhere. We store the notices in some sort of on-disk queue for later sending as email (in emit_notices). The design seems interesting, I would expect to save the data into the database using djangos normal means, but apparently another approach was taken here.

The potential problem is that if we update django, the models living representation might change, since the code changed. Unpickling from this queue will then give half-correct representations that will cause problems. However, this is easily avoided by making sure that the emit queue is empty before we update anything. This should be the case after emit_notices runs successfully.

Code lgtm - 2 nits inlined.

review: Approve
Revision history for this message
kaputtnik (franku) wrote :

Thanks for testing and review :-)

Got also all emails (except for the server error).

Deferred vs. Immediate mails: I have tried to use deferred mails for things where 'maybe' much people get informed. I just guessed that most people are interested in getting e-mails for new maps, topics and forum-posts. When sending a message or on changes in wiki there are only a few people involved and a few mails are send immediately then.

Wlmaps -> Maps: This string is cut from the label of a notice type. So wlmaps_new_map is turned into 'Wlmaps' and forum_new_topic get 'Forum'. I thought it is intended to start the label with the name of the app which it relates to. But i am fine with 'maps_new_map' as the label for the notice type. I want also to reverse the ordering so we have an alphabetical order of headings: 1. Forum, 2. Maps, 3. Messages, 4. Wiki

Map upload: The process of uploading the map take a very long time. I think this is because of wl_map_info needs long time to process the map. The timeout you had is maybe because you uploaded a big map? No idea for the server error though.

Pickling: The pickled object is already stored in the database. The model for this is 'NoticeQueueBatch'

http://bazaar.launchpad.net/~widelands-dev/widelands-website/notifications_cleanup/view/head:/notification/models.py#L117

Are you fine with running emit_notices hourly?

470. By kaputtnik

adressed code review; sorted notice settings

Revision history for this message
SirVer (sirver) wrote :

> I think this is because of wl_map_info needs long time to process the map.

I agree, this is probably the problem. It takes so long because it needs to load all of Widelands graphics assets into memory and on a cold cache this takes probably too long. Not much we can do about this except of changing the tool into a python library that is always loaded in the website and stays in memory.

> Pickling: The pickled object is already stored in the database. The model for this is 'NoticeQueueBatch'

This I find interesting. I would expect that the objects in the database to be always stored using djangos ORM, i.e. in proper tables. Instead here they are pickled and saved as a string. I wonder what informed this design - it seems more brittle to me. But it is probably without consequence for us either way.

> Are you fine with running emit_notices hourly?

Yes, sure. There is also /etc/cron.20minutesly/ to run it 3 times per hour. I would trigger the script there.

review: Approve
Revision history for this message
kaputtnik (franku) wrote :

Saving the pickled objects in the database is an easy way to store the data, imho. No need for special rights is needed here. In fact storing in the database is nearly the same as storing on disk, because mysql stores his databases and tables on disk.
From my understanding Djangos ORM is used for table relationships, e.g. tables with foreign keys. Using Django makes database actions easy, e.g. creating of proper tables with or without relations. But it does not mean that every table 'must' use ORM. A standalone table, like NoticeQueueBatch is easily created through django, but only used by the app. Just my understanding though...

I have stopped alpha now.

Don't know when i can deploy this. Maybe on Sunday.

Thanks for your input :-)

471. By kaputtnik

renamed template folder, to find the e-mail template for new maps

472. By kaputtnik

modified a comment to explain why a try statement is needed

Revision history for this message
SirVer (sirver) wrote :

I think your reasoning and arguments are correct. My thoughts are more like this: Using django's ORM makes the schema of your messages explicit in the code (as compared to pickling them) and allows for transformations forward of the schema (using south). It is also resilient to updates of django and allows for quicker access and filtering (through the database).

It just seems interesting that they decided to pickle them instead. It just seems the wrong decision to me. But in our case I do not expect problems with either approach anyways.

Revision history for this message
kaputtnik (franku) wrote :

Another approach is to send emails asynchronously (as an own thread). Most people talk about using celery or other third party apps for this approach. Since the current solution works, i didn't spend much time to investigate asynchronous (threaded) emailing.

Example: http://stackoverflow.com/questions/4447081/how-to-send-asynchronous-email-using-django#4447147

Revision history for this message
SirVer (sirver) wrote :

I agree, that seems overkill and the current solution is much simpler.

Revision history for this message
kaputtnik (franku) wrote :

Merged and deployed. I have created a cron job /etc/cron.20minutesly/django_commands.

First i forgot to make the script executable, so the first emailing didn't work. Lets see if it works after making the script executable.

The command used in /etc/cron.daily/django_regular_commands had a comment:

# Was unable to quiet the next command - we will likely never seen when it fails
/var/www/django_projects/wlwebsite/bin/python manage.py emit_notices 2> /dev/null

I removed the complete part from there and put in /etc/cron.20minutesly/django_commands, and use it in there without the redirect (>2 /dev/null). I am not sure where the output of this command will be written. I checked /var/log/upstart/wl.log and /var/log/nginx/wlwebsite.log but cant see anything in there.
Since i have adjusted the code to make this command not so chattering, it may be ok as it is?

Revision history for this message
SirVer (sirver) wrote :

> I am not sure where the output of this command will be written.

It will be silently dropped on success (i.e. exit code 0) (by cronic). On error, cronic will output an error report to stderr which will trigger cron to send it to me by email. I forgot where to configure the recipient of the emails for cron though...

Thanks for merging!

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'media/css/notice.css'
2--- media/css/notice.css 2012-05-08 21:52:15 +0000
3+++ media/css/notice.css 2017-05-07 09:51:42 +0000
4@@ -12,7 +12,6 @@
5 .notifications th {
6 border: none;
7 padding: 4px;
8- padding-top: 20px;
9 text-align: left;
10 font-weight: normal;
11 }
12
13=== added file 'news/migrations/0002_auto_20170417_1857.py'
14--- news/migrations/0002_auto_20170417_1857.py 1970-01-01 00:00:00 +0000
15+++ news/migrations/0002_auto_20170417_1857.py 2017-05-07 09:51:42 +0000
16@@ -0,0 +1,19 @@
17+# -*- coding: utf-8 -*-
18+from __future__ import unicode_literals
19+
20+from django.db import models, migrations
21+
22+
23+class Migration(migrations.Migration):
24+
25+ dependencies = [
26+ ('news', '0001_initial'),
27+ ]
28+
29+ operations = [
30+ migrations.AlterField(
31+ model_name='post',
32+ name='body',
33+ field=models.TextField(help_text=b'Text entered here will be rendered using Markdown', verbose_name='body'),
34+ ),
35+ ]
36
37=== modified file 'notification/README'
38--- notification/README 2016-05-17 19:28:38 +0000
39+++ notification/README 2017-05-07 09:51:42 +0000
40@@ -1,7 +1,12 @@
41 This is the old version (0.1.4) of the notification app by James Tauber.
42-I have included it as a widelands app because the new version is
43+It is included as a widelands app because the new version is
44 incompatible with our old data.
45
46+Year 2017:
47+The ability to store notices is removed and therefor it acts only as an app
48+to send e-mails for observed items.
49+
50+
51 See the file LICENSE for Copyright notice.
52
53 Original Description:
54
55=== modified file 'notification/admin.py'
56--- notification/admin.py 2016-12-13 18:28:51 +0000
57+++ notification/admin.py 2017-05-07 09:51:42 +0000
58@@ -1,5 +1,6 @@
59 from django.contrib import admin
60-from notification.models import NoticeType, NoticeSetting, Notice, ObservedItem
61+from notification.models import NoticeType, NoticeSetting, ObservedItem
62+from django.utils.translation import ugettext_lazy as _
63
64
65 class NoticeTypeAdmin(admin.ModelAdmin):
66@@ -7,15 +8,20 @@
67
68
69 class NoticeSettingAdmin(admin.ModelAdmin):
70- list_display = ('id', 'user', 'notice_type', 'medium', 'send')
71-
72-
73-class NoticeAdmin(admin.ModelAdmin):
74- list_display = ('message', 'user', 'notice_type',
75- 'added', 'unseen', 'archived')
76-
77+ search_fields = ['user__username',]
78+ list_display = ('user', 'notice_type', 'medium', 'send')
79+
80+
81+class ObserverdItemAdmin(admin.ModelAdmin):
82+ readonly_fields = ('observed_object', 'content_type', 'object_id')
83+ search_fields = ['user__username', 'notice_type__label']
84+ list_display = ('user', 'notice_type', 'content_type', 'get_content_object')
85+ fieldsets = (
86+ (None, {'fields': ('user',)}),
87+ (_('Observed object'), {'fields': ('observed_object', 'content_type', 'object_id')}),
88+ (_('Settings'), {'fields': ('added', 'notice_type', 'signal')}),
89+ )
90
91 admin.site.register(NoticeType, NoticeTypeAdmin)
92 admin.site.register(NoticeSetting, NoticeSettingAdmin)
93-admin.site.register(Notice, NoticeAdmin)
94-admin.site.register(ObservedItem)
95+admin.site.register(ObservedItem, ObserverdItemAdmin)
96
97=== removed file 'notification/atomformat.py'
98--- notification/atomformat.py 2016-12-13 18:28:51 +0000
99+++ notification/atomformat.py 1970-01-01 00:00:00 +0000
100@@ -1,551 +0,0 @@
101-#
102-# django-atompub by James Tauber <http://jtauber.com/>
103-# http://code.google.com/p/django-atompub/
104-# An implementation of the Atom format and protocol for Django
105-#
106-# For instructions on how to use this module to generate Atom feeds,
107-# see http://code.google.com/p/django-atompub/wiki/UserGuide
108-#
109-#
110-# Copyright (c) 2007, James Tauber
111-#
112-# Permission is hereby granted, free of charge, to any person obtaining a copy
113-# of this software and associated documentation files (the "Software"), to deal
114-# in the Software without restriction, including without limitation the rights
115-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
116-# copies of the Software, and to permit persons to whom the Software is
117-# furnished to do so, subject to the following conditions:
118-#
119-# The above copyright notice and this permission notice shall be included in
120-# all copies or substantial portions of the Software.
121-#
122-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
123-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
124-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
125-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
126-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
127-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
128-# THE SOFTWARE.
129-#
130-
131-from xml.sax.saxutils import XMLGenerator
132-from datetime import datetime
133-
134-
135-GENERATOR_TEXT = 'django-atompub'
136-GENERATOR_ATTR = {
137- 'uri': 'http://code.google.com/p/django-atompub/',
138- 'version': 'r33'
139-}
140-
141-
142-# based on django.utils.xmlutils.SimplerXMLGenerator
143-class SimplerXMLGenerator(XMLGenerator):
144-
145- def addQuickElement(self, name, contents=None, attrs=None):
146- """Convenience method for adding an element with no children."""
147- if attrs is None:
148- attrs = {}
149- self.startElement(name, attrs)
150- if contents is not None:
151- self.characters(contents)
152- self.endElement(name)
153-
154-
155-# based on django.utils.feedgenerator.rfc3339_date
156-def rfc3339_date(date):
157- return date.strftime('%Y-%m-%dT%H:%M:%SZ')
158-
159-
160-# based on django.utils.feedgenerator.get_tag_uri
161-def get_tag_uri(url, date):
162- """Creates a TagURI.
163-
164- See http://diveintomark.org/archives/2004/05/28/howto-atom-id
165-
166- """
167- tag = re.sub('^http://', '', url)
168- if date is not None:
169- tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
170- tag = re.sub('#', '/', tag)
171- return 'tag:' + tag
172-
173-
174-# based on django.contrib.syndication.feeds.Feed
175-class Feed(object):
176-
177- VALIDATE = True
178-
179- def __init__(self, slug, feed_url):
180- # @@@ slug and feed_url are not used yet
181- pass
182-
183- def __get_dynamic_attr(self, attname, obj, default=None):
184- try:
185- attr = getattr(self, attname)
186- except AttributeError:
187- return default
188- if callable(attr):
189- # Check func_code.co_argcount rather than try/excepting the
190- # function and catching the TypeError, because something inside
191- # the function may raise the TypeError. This technique is more
192- # accurate.
193- if hasattr(attr, 'func_code'):
194- argcount = attr.func_code.co_argcount
195- else:
196- argcount = attr.__call__.func_code.co_argcount
197- if argcount == 2: # one argument is 'self'
198- return attr(obj)
199- else:
200- return attr()
201- return attr
202-
203- def get_feed(self, extra_params=None):
204-
205- if extra_params:
206- try:
207- obj = self.get_object(extra_params.split('/'))
208- except (AttributeError, LookupError):
209- raise LookupError('Feed does not exist')
210- else:
211- obj = None
212-
213- feed = AtomFeed(
214- atom_id=self.__get_dynamic_attr('feed_id', obj),
215- title=self.__get_dynamic_attr('feed_title', obj),
216- updated=self.__get_dynamic_attr('feed_updated', obj),
217- icon=self.__get_dynamic_attr('feed_icon', obj),
218- logo=self.__get_dynamic_attr('feed_logo', obj),
219- rights=self.__get_dynamic_attr('feed_rights', obj),
220- subtitle=self.__get_dynamic_attr('feed_subtitle', obj),
221- authors=self.__get_dynamic_attr('feed_authors', obj, default=[]),
222- categories=self.__get_dynamic_attr(
223- 'feed_categories', obj, default=[]),
224- contributors=self.__get_dynamic_attr(
225- 'feed_contributors', obj, default=[]),
226- links=self.__get_dynamic_attr('feed_links', obj, default=[]),
227- extra_attrs=self.__get_dynamic_attr('feed_extra_attrs', obj),
228- hide_generator=self.__get_dynamic_attr(
229- 'hide_generator', obj, default=False)
230- )
231-
232- items = self.__get_dynamic_attr('items', obj)
233- if items is None:
234- raise LookupError('Feed has no items field')
235-
236- for item in items:
237- feed.add_item(
238- atom_id=self.__get_dynamic_attr('item_id', item),
239- title=self.__get_dynamic_attr('item_title', item),
240- updated=self.__get_dynamic_attr('item_updated', item),
241- content=self.__get_dynamic_attr('item_content', item),
242- published=self.__get_dynamic_attr('item_published', item),
243- rights=self.__get_dynamic_attr('item_rights', item),
244- source=self.__get_dynamic_attr('item_source', item),
245- summary=self.__get_dynamic_attr('item_summary', item),
246- authors=self.__get_dynamic_attr(
247- 'item_authors', item, default=[]),
248- categories=self.__get_dynamic_attr(
249- 'item_categories', item, default=[]),
250- contributors=self.__get_dynamic_attr(
251- 'item_contributors', item, default=[]),
252- links=self.__get_dynamic_attr('item_links', item, default=[]),
253- extra_attrs=self.__get_dynamic_attr(
254- 'item_extra_attrs', None, default={}),
255- )
256-
257- if self.VALIDATE:
258- feed.validate()
259- return feed
260-
261-
262-class ValidationError(Exception):
263- pass
264-
265-
266-# based on django.utils.feedgenerator.SyndicationFeed and
267-# django.utils.feedgenerator.Atom1Feed
268-class AtomFeed(object):
269-
270- mime_type = 'application/atom+xml'
271- ns = u'http://www.w3.org/2005/Atom'
272-
273- def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
274- authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
275- if atom_id is None:
276- raise LookupError('Feed has no feed_id field')
277- if title is None:
278- raise LookupError('Feed has no feed_title field')
279- # if updated == None, we'll calculate it
280- self.feed = {
281- 'id': atom_id,
282- 'title': title,
283- 'updated': updated,
284- 'icon': icon,
285- 'logo': logo,
286- 'rights': rights,
287- 'subtitle': subtitle,
288- 'authors': authors,
289- 'categories': categories,
290- 'contributors': contributors,
291- 'links': links,
292- 'extra_attrs': extra_attrs,
293- 'hide_generator': hide_generator,
294- }
295- self.items = []
296-
297- def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
298- authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
299- if atom_id is None:
300- raise LookupError('Feed has no item_id method')
301- if title is None:
302- raise LookupError('Feed has no item_title method')
303- if updated is None:
304- raise LookupError('Feed has no item_updated method')
305- self.items.append({
306- 'id': atom_id,
307- 'title': title,
308- 'updated': updated,
309- 'content': content,
310- 'published': published,
311- 'rights': rights,
312- 'source': source,
313- 'summary': summary,
314- 'authors': authors,
315- 'categories': categories,
316- 'contributors': contributors,
317- 'links': links,
318- 'extra_attrs': extra_attrs,
319- })
320-
321- def latest_updated(self):
322- """Returns the latest item's updated or the current time if there are
323- no items."""
324- updates = [item['updated'] for item in self.items]
325- if len(updates) > 0:
326- updates.sort()
327- return updates[-1]
328- else:
329- # @@@ really we should allow a feed to define its "start" for this case
330- return datetime.now()
331-
332- def write_text_construct(self, handler, element_name, data):
333- if isinstance(data, tuple):
334- text_type, text = data
335- if text_type == 'xhtml':
336- handler.startElement(element_name, {'type': text_type})
337- # write unescaped -- it had better be well-formed XML
338- handler._write(text)
339- handler.endElement(element_name)
340- else:
341- handler.addQuickElement(
342- element_name, text, {'type': text_type})
343- else:
344- handler.addQuickElement(element_name, data)
345-
346- def write_person_construct(self, handler, element_name, person):
347- handler.startElement(element_name, {})
348- handler.addQuickElement(u'name', person['name'])
349- if 'uri' in person:
350- handler.addQuickElement(u'uri', person['uri'])
351- if 'email' in person:
352- handler.addQuickElement(u'email', person['email'])
353- handler.endElement(element_name)
354-
355- def write_link_construct(self, handler, link):
356- if 'length' in link:
357- link['length'] = str(link['length'])
358- handler.addQuickElement(u'link', None, link)
359-
360- def write_category_construct(self, handler, category):
361- handler.addQuickElement(u'category', None, category)
362-
363- def write_source(self, handler, data):
364- handler.startElement(u'source', {})
365- if data.get('id'):
366- handler.addQuickElement(u'id', data['id'])
367- if data.get('title'):
368- self.write_text_construct(handler, u'title', data['title'])
369- if data.get('subtitle'):
370- self.write_text_construct(handler, u'subtitle', data['subtitle'])
371- if data.get('icon'):
372- handler.addQuickElement(u'icon', data['icon'])
373- if data.get('logo'):
374- handler.addQuickElement(u'logo', data['logo'])
375- if data.get('updated'):
376- handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
377- for category in data.get('categories', []):
378- self.write_category_construct(handler, category)
379- for link in data.get('links', []):
380- self.write_link_construct(handler, link)
381- for author in data.get('authors', []):
382- self.write_person_construct(handler, u'author', author)
383- for contributor in data.get('contributors', []):
384- self.write_person_construct(handler, u'contributor', contributor)
385- if data.get('rights'):
386- self.write_text_construct(handler, u'rights', data['rights'])
387- handler.endElement(u'source')
388-
389- def write_content(self, handler, data):
390- if isinstance(data, tuple):
391- content_dict, text = data
392- if content_dict.get('type') == 'xhtml':
393- handler.startElement(u'content', content_dict)
394- # write unescaped -- it had better be well-formed XML
395- handler._write(text)
396- handler.endElement(u'content')
397- else:
398- handler.addQuickElement(u'content', text, content_dict)
399- else:
400- handler.addQuickElement(u'content', data)
401-
402- def write(self, outfile, encoding):
403- handler = SimplerXMLGenerator(outfile, encoding)
404- handler.startDocument()
405- feed_attrs = {u'xmlns': self.ns}
406- if self.feed.get('extra_attrs'):
407- feed_attrs.update(self.feed['extra_attrs'])
408- handler.startElement(u'feed', feed_attrs)
409- handler.addQuickElement(u'id', self.feed['id'])
410- self.write_text_construct(handler, u'title', self.feed['title'])
411- if self.feed.get('subtitle'):
412- self.write_text_construct(
413- handler, u'subtitle', self.feed['subtitle'])
414- if self.feed.get('icon'):
415- handler.addQuickElement(u'icon', self.feed['icon'])
416- if self.feed.get('logo'):
417- handler.addQuickElement(u'logo', self.feed['logo'])
418- if self.feed['updated']:
419- handler.addQuickElement(
420- u'updated', rfc3339_date(self.feed['updated']))
421- else:
422- handler.addQuickElement(
423- u'updated', rfc3339_date(self.latest_updated()))
424- for category in self.feed['categories']:
425- self.write_category_construct(handler, category)
426- for link in self.feed['links']:
427- self.write_link_construct(handler, link)
428- for author in self.feed['authors']:
429- self.write_person_construct(handler, u'author', author)
430- for contributor in self.feed['contributors']:
431- self.write_person_construct(handler, u'contributor', contributor)
432- if self.feed.get('rights'):
433- self.write_text_construct(handler, u'rights', self.feed['rights'])
434- if not self.feed.get('hide_generator'):
435- handler.addQuickElement(
436- u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
437-
438- self.write_items(handler)
439-
440- handler.endElement(u'feed')
441-
442- def write_items(self, handler):
443- for item in self.items:
444- entry_attrs = item.get('extra_attrs', {})
445- handler.startElement(u'entry', entry_attrs)
446-
447- handler.addQuickElement(u'id', item['id'])
448- self.write_text_construct(handler, u'title', item['title'])
449- handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
450- if item.get('published'):
451- handler.addQuickElement(
452- u'published', rfc3339_date(item['published']))
453- if item.get('rights'):
454- self.write_text_construct(handler, u'rights', item['rights'])
455- if item.get('source'):
456- self.write_source(handler, item['source'])
457-
458- for author in item['authors']:
459- self.write_person_construct(handler, u'author', author)
460- for contributor in item['contributors']:
461- self.write_person_construct(
462- handler, u'contributor', contributor)
463- for category in item['categories']:
464- self.write_category_construct(handler, category)
465- for link in item['links']:
466- self.write_link_construct(handler, link)
467- if item.get('summary'):
468- self.write_text_construct(handler, u'summary', item['summary'])
469- if item.get('content'):
470- self.write_content(handler, item['content'])
471-
472- handler.endElement(u'entry')
473-
474- def validate(self):
475-
476- def validate_text_construct(obj):
477- if isinstance(obj, tuple):
478- if obj[0] not in ['text', 'html', 'xhtml']:
479- return False
480- # @@@ no validation is done that 'html' text constructs are valid HTML
481- # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
482-
483- return True
484-
485- if not validate_text_construct(self.feed['title']):
486- raise ValidationError('feed title has invalid type')
487- if self.feed.get('subtitle'):
488- if not validate_text_construct(self.feed['subtitle']):
489- raise ValidationError('feed subtitle has invalid type')
490- if self.feed.get('rights'):
491- if not validate_text_construct(self.feed['rights']):
492- raise ValidationError('feed rights has invalid type')
493-
494- alternate_links = {}
495- for link in self.feed.get('links'):
496- if link.get('rel') == 'alternate' or link.get('rel') == None:
497- key = (link.get('type'), link.get('hreflang'))
498- if key in alternate_links:
499- raise ValidationError(
500- 'alternate links must have unique type/hreflang')
501- alternate_links[key] = link
502-
503- if self.feed.get('authors'):
504- feed_author = True
505- else:
506- feed_author = False
507-
508- for item in self.items:
509- if not feed_author and not item.get('authors'):
510- if item.get('source') and item['source'].get('authors'):
511- pass
512- else:
513- raise ValidationError(
514- 'if no feed author, all entries must have author (possibly in source)')
515-
516- if not validate_text_construct(item['title']):
517- raise ValidationError('entry title has invalid type')
518- if item.get('rights'):
519- if not validate_text_construct(item['rights']):
520- raise ValidationError('entry rights has invalid type')
521- if item.get('summary'):
522- if not validate_text_construct(item['summary']):
523- raise ValidationError('entry summary has invalid type')
524- source = item.get('source')
525- if source:
526- if source.get('title'):
527- if not validate_text_construct(source['title']):
528- raise ValidationError('source title has invalid type')
529- if source.get('subtitle'):
530- if not validate_text_construct(source['subtitle']):
531- raise ValidationError(
532- 'source subtitle has invalid type')
533- if source.get('rights'):
534- if not validate_text_construct(source['rights']):
535- raise ValidationError('source rights has invalid type')
536-
537- alternate_links = {}
538- for link in item.get('links'):
539- if link.get('rel') == 'alternate' or link.get('rel') == None:
540- key = (link.get('type'), link.get('hreflang'))
541- if key in alternate_links:
542- raise ValidationError(
543- 'alternate links must have unique type/hreflang')
544- alternate_links[key] = link
545-
546- if not item.get('content'):
547- if not alternate_links:
548- raise ValidationError(
549- 'if no content, entry must have alternate link')
550-
551- if item.get('content') and isinstance(item.get('content'), tuple):
552- content_type = item.get('content')[0].get('type')
553- if item.get('content')[0].get('src'):
554- if item.get('content')[1]:
555- raise ValidationError(
556- 'content with src should be empty')
557- if not item.get('summary'):
558- raise ValidationError(
559- 'content with src requires a summary too')
560- if content_type in ['text', 'html', 'xhtml']:
561- raise ValidationError(
562- 'content with src cannot have type of text, html or xhtml')
563- if content_type:
564- if '/' in content_type and \
565- not content_type.startswith('text/') and \
566- not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
567- not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
568- # @@@ check content is Base64
569- if not item.get('summary'):
570- raise ValidationError(
571- 'content in Base64 requires a summary too')
572- if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
573- raise ValidationError(
574- 'content type does not appear to be valid')
575-
576- # @@@ no validation is done that 'html' text constructs are valid HTML
577- # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
578-
579- return
580-
581- return
582-
583-
584-class LegacySyndicationFeed(AtomFeed):
585- """
586- Provides an SyndicationFeed-compatible interface in its __init__ and
587- add_item but is really a new AtomFeed object.
588- """
589-
590- def __init__(self, title, link, description, language=None, author_email=None,
591- author_name=None, author_link=None, subtitle=None, categories=[],
592- feed_url=None, feed_copyright=None):
593-
594- atom_id = link
595- title = title
596- updated = None # will be calculated
597- rights = feed_copyright
598- subtitle = subtitle
599- author_dict = {'name': author_name}
600- if author_link:
601- author_dict['uri'] = author_uri
602- if author_email:
603- author_dict['email'] = author_email
604- authors = [author_dict]
605- if categories:
606- categories = [{'term': term} for term in categories]
607- links = [{'rel': 'alternate', 'href': link}]
608- if feed_url:
609- links.append({'rel': 'self', 'href': feed_url})
610- if language:
611- extra_attrs = {'xml:lang': language}
612- else:
613- extra_attrs = {}
614-
615- # description ignored (as with Atom1Feed)
616-
617- AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
618- authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
619-
620- def add_item(self, title, link, description, author_email=None,
621- author_name=None, author_link=None, pubdate=None, comments=None,
622- unique_id=None, enclosure=None, categories=[], item_copyright=None):
623-
624- if unique_id:
625- atom_id = unique_id
626- else:
627- atom_id = get_tag_uri(link, pubdate)
628- title = title
629- updated = pubdate
630- if item_copyright:
631- rights = item_copyright
632- else:
633- rights = None
634- if description:
635- summary = 'html', description
636- else:
637- summary = None
638- author_dict = {'name': author_name}
639- if author_link:
640- author_dict['uri'] = author_uri
641- if author_email:
642- author_dict['email'] = author_email
643- authors = [author_dict]
644- categories = [{'term': term} for term in categories]
645- links = [{'rel': 'alternate', 'href': link}]
646- if enclosure:
647- links.append({'rel': 'enclosure', 'href': enclosure.url,
648- 'length': enclosure.length, 'type': enclosure.mime_type})
649-
650- AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
651- authors=authors, categories=categories, links=links)
652
653=== removed file 'notification/context_processors.py'
654--- notification/context_processors.py 2016-12-13 18:28:51 +0000
655+++ notification/context_processors.py 1970-01-01 00:00:00 +0000
656@@ -1,10 +0,0 @@
657-from notification.models import Notice
658-
659-
660-def notification(request):
661- if request.user.is_authenticated():
662- return {
663- 'notice_unseen_count': Notice.objects.unseen_count_for(request.user, on_site=True),
664- }
665- else:
666- return {}
667
668=== removed file 'notification/decorators.py'
669--- notification/decorators.py 2016-12-13 18:28:51 +0000
670+++ notification/decorators.py 1970-01-01 00:00:00 +0000
671@@ -1,65 +0,0 @@
672-from django.utils.translation import ugettext as _
673-from django.http import HttpResponse
674-from django.contrib.auth import authenticate, login
675-from django.conf import settings
676-
677-
678-def simple_basic_auth_callback(request, user, *args, **kwargs):
679- """Simple callback to automatically login the given user after a successful
680- basic authentication."""
681- login(request, user)
682- request.user = user
683-
684-
685-def basic_auth_required(realm=None, test_func=None, callback_func=None):
686- """This decorator should be used with views that need simple authentication
687- against Django's authentication framework.
688-
689- The ``realm`` string is shown during the basic auth query.
690-
691- It takes a ``test_func`` argument that is used to validate the given
692- credentials and return the decorated function if successful.
693-
694- If unsuccessful the decorator will try to authenticate and checks if the
695- user has the ``is_active`` field set to True.
696-
697- In case of a successful authentication the ``callback_func`` will be
698- called by passing the ``request`` and the ``user`` object. After that the
699- actual view function will be called.
700-
701- If all of the above fails a "Authorization Required" message will be shown.
702-
703- """
704- if realm is None:
705- realm = getattr(settings, 'HTTP_AUTHENTICATION_REALM',
706- _('Restricted Access'))
707- if test_func is None:
708- test_func = lambda u: u.is_authenticated()
709-
710- def decorator(view_func):
711- def basic_auth(request, *args, **kwargs):
712- # Just return the original view because already logged in
713- if test_func(request.user):
714- return view_func(request, *args, **kwargs)
715-
716- # Not logged in, look if login credentials are provided
717- if 'HTTP_AUTHORIZATION' in request.META:
718- auth_method, auth = request.META[
719- 'HTTP_AUTHORIZATION'].split(' ', 1)
720- if 'basic' == auth_method.lower():
721- auth = auth.strip().decode('base64')
722- username, password = auth.split(':', 1)
723- user = authenticate(username=username, password=password)
724- if user is not None:
725- if user.is_active:
726- if callback_func is not None and callable(callback_func):
727- callback_func(request, user, *args, **kwargs)
728- return view_func(request, *args, **kwargs)
729-
730- response = HttpResponse(
731- _('Authorization Required'), mimetype='text/plain')
732- response.status_code = 401
733- response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
734- return response
735- return basic_auth
736- return decorator
737
738=== modified file 'notification/engine.py'
739--- notification/engine.py 2016-12-13 18:28:51 +0000
740+++ notification/engine.py 2017-05-07 09:51:42 +0000
741@@ -49,7 +49,10 @@
742 str(queued_batch.pickled_data).decode('base64'))
743 for user, label, extra_context, on_site in notices:
744 user = User.objects.get(pk=user)
745- logging.info('emitting notice to %s' % user)
746+ # FrankU: commented, because not all users get e-mailed
747+ # and to supress useless logging
748+ # logging.info('emitting notice to %s' % user)
749+
750 # call this once per user to be atomic and allow for logging to
751 # accurately show how long each takes.
752 notification.send_now(
753
754=== removed file 'notification/feeds.py'
755--- notification/feeds.py 2016-12-13 18:28:51 +0000
756+++ notification/feeds.py 1970-01-01 00:00:00 +0000
757@@ -1,75 +0,0 @@
758-from datetime import datetime
759-
760-from django.core.urlresolvers import reverse
761-from django.conf import settings
762-from django.contrib.sites.models import Site
763-from django.contrib.auth.models import User
764-from django.shortcuts import get_object_or_404
765-from django.template.defaultfilters import linebreaks, escape, striptags
766-from django.utils.translation import ugettext_lazy as _
767-
768-from notification.models import Notice
769-from notification.atomformat import Feed
770-
771-ITEMS_PER_FEED = getattr(settings, 'ITEMS_PER_FEED', 20)
772-
773-
774-class BaseNoticeFeed(Feed):
775-
776- def item_id(self, notification):
777- return 'http://%s%s' % (
778- Site.objects.get_current().domain,
779- notification.get_absolute_url(),
780- )
781-
782- def item_title(self, notification):
783- return striptags(notification.message)
784-
785- def item_updated(self, notification):
786- return notification.added
787-
788- def item_published(self, notification):
789- return notification.added
790-
791- def item_content(self, notification):
792- return {'type': 'html', }, linebreaks(escape(notification.message))
793-
794- def item_links(self, notification):
795- return [{'href': self.item_id(notification)}]
796-
797- def item_authors(self, notification):
798- return [{'name': notification.user.username}]
799-
800-
801-class NoticeUserFeed(BaseNoticeFeed):
802-
803- def get_object(self, params):
804- return get_object_or_404(User, username=params[0].lower())
805-
806- def feed_id(self, user):
807- return 'http://%s%s' % (
808- Site.objects.get_current().domain,
809- reverse('notification_feed_for_user'),
810- )
811-
812- def feed_title(self, user):
813- return _('Notices Feed')
814-
815- def feed_updated(self, user):
816- qs = Notice.objects.filter(user=user)
817- # We return an arbitrary date if there are no results, because there
818- # must be a feed_updated field as per the Atom specifications, however
819- # there is no real data to go by, and an arbitrary date can be static.
820- if qs.count() == 0:
821- return datetime(year=2008, month=7, day=1)
822- return qs.latest('added').added
823-
824- def feed_links(self, user):
825- complete_url = 'http://%s%s' % (
826- Site.objects.get_current().domain,
827- reverse('notification_notices'),
828- )
829- return ({'href': complete_url},)
830-
831- def items(self, user):
832- return Notice.objects.notices_for(user).order_by('-added')[:ITEMS_PER_FEED]
833
834=== modified file 'notification/management/commands/emit_notices.py'
835--- notification/management/commands/emit_notices.py 2016-12-13 18:28:51 +0000
836+++ notification/management/commands/emit_notices.py 2017-05-07 09:51:42 +0000
837@@ -10,6 +10,7 @@
838 help = 'Emit queued notices.'
839
840 def handle_noargs(self, **options):
841- logging.basicConfig(level=logging.DEBUG, format='%(message)s')
842+ # Franku: Uncomment for debugging purposes
843+ # logging.basicConfig(level=logging.DEBUG, format='%(message)s')
844 logging.info('-' * 72)
845 send_all()
846
847=== added file 'notification/migrations/0002_auto_20170417_1857.py'
848--- notification/migrations/0002_auto_20170417_1857.py 1970-01-01 00:00:00 +0000
849+++ notification/migrations/0002_auto_20170417_1857.py 2017-05-07 09:51:42 +0000
850@@ -0,0 +1,25 @@
851+# -*- coding: utf-8 -*-
852+from __future__ import unicode_literals
853+
854+from django.db import models, migrations
855+
856+
857+class Migration(migrations.Migration):
858+
859+ dependencies = [
860+ ('notification', '0001_initial'),
861+ ]
862+
863+ operations = [
864+ migrations.RemoveField(
865+ model_name='notice',
866+ name='notice_type',
867+ ),
868+ migrations.RemoveField(
869+ model_name='notice',
870+ name='user',
871+ ),
872+ migrations.DeleteModel(
873+ name='Notice',
874+ ),
875+ ]
876
877=== modified file 'notification/models.py'
878--- notification/models.py 2016-12-13 18:28:51 +0000
879+++ notification/models.py 2017-05-07 09:51:42 +0000
880@@ -68,7 +68,11 @@
881
882 class NoticeSetting(models.Model):
883 """Indicates, for a given user, whether to send notifications of a given
884- type to a given medium."""
885+ type to a given medium.
886+
887+ Notice types for each user are added if he/she enters the notification page.
888+
889+ """
890
891 user = models.ForeignKey(User, verbose_name=_('user'))
892 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
893@@ -82,6 +86,12 @@
894
895
896 def get_notification_setting(user, notice_type, medium):
897+ """Return NotceSetting for a specific user. If a NoticeSetting of
898+ given NoticeType didn't exist for given user, a NoticeSetting is created.
899+
900+ If a new NoticeSetting is created, the field 'default' of a NoticeType
901+ decides whether NoticeSetting.send is True or False as default.
902+ """
903 try:
904 return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
905 except NoticeSetting.DoesNotExist:
906@@ -91,81 +101,14 @@
907 setting.save()
908 return setting
909
910-
911 def should_send(user, notice_type, medium):
912 return get_notification_setting(user, notice_type, medium).send
913
914-
915-class NoticeManager(models.Manager):
916-
917- def notices_for(self, user, archived=False, unseen=None, on_site=None):
918- """returns Notice objects for the given user.
919-
920- If archived=False, it only include notices not archived.
921- If archived=True, it returns all notices for that user.
922-
923- If unseen=None, it includes all notices.
924- If unseen=True, return only unseen notices.
925- If unseen=False, return only seen notices.
926-
927- """
928- if archived:
929- qs = self.filter(user=user)
930- else:
931- qs = self.filter(user=user, archived=archived)
932- if unseen is not None:
933- qs = qs.filter(unseen=unseen)
934- if on_site is not None:
935- qs = qs.filter(on_site=on_site)
936- return qs
937-
938- def unseen_count_for(self, user, **kwargs):
939- """returns the number of unseen notices for the given user but does not
940- mark them seen."""
941- return self.notices_for(user, unseen=True, **kwargs).count()
942-
943-
944-class Notice(models.Model):
945-
946- user = models.ForeignKey(User, verbose_name=_('user'))
947- message = models.TextField(_('message'))
948- notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
949- added = models.DateTimeField(_('added'), default=datetime.datetime.now)
950- unseen = models.BooleanField(_('unseen'), default=True)
951- archived = models.BooleanField(_('archived'), default=False)
952- on_site = models.BooleanField(_('on site'))
953-
954- objects = NoticeManager()
955-
956- def __unicode__(self):
957- return self.message
958-
959- def archive(self):
960- self.archived = True
961- self.save()
962-
963- def is_unseen(self):
964- """returns value of self.unseen but also changes it to false.
965-
966- Use this in a template to mark an unseen notice differently the
967- first time it is shown.
968-
969- """
970- unseen = self.unseen
971- if unseen:
972- self.unseen = False
973- self.save()
974- return unseen
975-
976- class Meta:
977- ordering = ['-added']
978- verbose_name = _('notice')
979- verbose_name_plural = _('notices')
980-
981- def get_absolute_url(self):
982- return ('notification_notice', [str(self.pk)])
983- get_absolute_url = models.permalink(get_absolute_url)
984-
985+def get_observers_for(notice_type):
986+ """ Returns the list of users which wants to get a message (email) for this
987+ type of notice."""
988+ settings = NoticeSetting.objects.filter(notice_type__label=notice_type).filter(send=True)
989+ return [s.user for s in NoticeSetting.objects.filter(notice_type__label=notice_type).filter(send=True)]
990
991 class NoticeQueueBatch(models.Model):
992 """A queued notice.
993@@ -262,66 +205,68 @@
994 if extra_context is None:
995 extra_context = {}
996
997- notice_type = NoticeType.objects.get(label=label)
998-
999- current_site = Site.objects.get_current()
1000- notices_url = u"http://%s%s" % (
1001- unicode(current_site),
1002- reverse('notification_notices'),
1003- )
1004-
1005- current_language = get_language()
1006-
1007- formats = (
1008- 'short.txt',
1009- 'full.txt',
1010- 'notice.html',
1011- 'full.html',
1012- ) # TODO make formats configurable
1013-
1014- for user in users:
1015- recipients = []
1016- # get user language for user from language store defined in
1017- # NOTIFICATION_LANGUAGE_MODULE setting
1018- try:
1019- language = get_notification_language(user)
1020- except LanguageStoreNotAvailable:
1021- language = None
1022-
1023- if language is not None:
1024- # activate the user's language
1025- activate(language)
1026-
1027- # update context with user specific translations
1028- context = Context({
1029- 'user': user,
1030- 'notice': ugettext(notice_type.display),
1031- 'notices_url': notices_url,
1032- 'current_site': current_site,
1033- })
1034- context.update(extra_context)
1035-
1036- # get prerendered format messages
1037- messages = get_formatted_messages(formats, label, context)
1038-
1039- # Strip newlines from subject
1040- subject = ''.join(render_to_string('notification/email_subject.txt', {
1041- 'message': messages['short.txt'],
1042- }, context).splitlines())
1043-
1044- body = render_to_string('notification/email_body.txt', {
1045- 'message': messages['full.txt'],
1046- }, context)
1047-
1048- notice = Notice.objects.create(user=user, message=messages['notice.html'],
1049- notice_type=notice_type, on_site=on_site)
1050- if should_send(user, notice_type, '1') and user.email: # Email
1051- recipients.append(user.email)
1052- send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
1053-
1054- # reset environment to original language
1055- activate(current_language)
1056-
1057+ # FrankU: This try statement is added to pass notice types
1058+ # which are deleted but used by third party apps to create a notice
1059+ # e.g. django-messages installed some notice-types which are superfluous
1060+ # because they just create a notice (which is not used anymore), but not
1061+ # used for sending email, like: 'message deleted' or 'message recovered'
1062+ try:
1063+ notice_type = NoticeType.objects.get(label=label)
1064+
1065+ current_site = Site.objects.get_current()
1066+ notices_url = u"http://%s%s" % (
1067+ unicode(current_site),
1068+ reverse('notification_notices'),
1069+ )
1070+
1071+ current_language = get_language()
1072+
1073+ formats = (
1074+ 'short.txt',
1075+ 'full.txt',
1076+ ) # TODO make formats configurable
1077+
1078+ for user in users:
1079+ recipients = []
1080+ # get user language for user from language store defined in
1081+ # NOTIFICATION_LANGUAGE_MODULE setting
1082+ try:
1083+ language = get_notification_language(user)
1084+ except LanguageStoreNotAvailable:
1085+ language = None
1086+
1087+ if language is not None:
1088+ # activate the user's language
1089+ activate(language)
1090+
1091+ # update context with user specific translations
1092+ context = Context({
1093+ 'user': user,
1094+ 'notices_url': notices_url,
1095+ 'current_site': current_site,
1096+ })
1097+ context.update(extra_context)
1098+
1099+ # get prerendered format messages
1100+ messages = get_formatted_messages(formats, label, context)
1101+
1102+ # Strip newlines from subject
1103+ subject = ''.join(render_to_string('notification/email_subject.txt', {
1104+ 'message': messages['short.txt'],
1105+ }, context).splitlines())
1106+
1107+ body = render_to_string('notification/email_body.txt', {
1108+ 'message': messages['full.txt'],
1109+ }, context)
1110+
1111+ if should_send(user, notice_type, '1') and user.email: # Email
1112+ recipients.append(user.email)
1113+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
1114+
1115+ # reset environment to original language
1116+ activate(current_language)
1117+ except NoticeType.DoesNotExist:
1118+ pass
1119
1120 def send(*args, **kwargs):
1121 """A basic interface around both queue and send_now.
1122@@ -409,6 +354,15 @@
1123 def send_notice(self):
1124 send([self.user], self.notice_type.label,
1125 {'observed': self.observed_object})
1126+
1127+ def get_content_object(self):
1128+ """
1129+ taken from threadedcomments:
1130+
1131+ Wrapper around the GenericForeignKey due to compatibility reasons
1132+ and due to ``list_display`` limitations.
1133+ """
1134+ return self.observed_object
1135
1136
1137 def observe(observed, observer, notice_type_label, signal='post_save'):
1138
1139=== modified file 'notification/urls.py'
1140--- notification/urls.py 2016-12-13 18:28:51 +0000
1141+++ notification/urls.py 2017-05-07 09:51:42 +0000
1142@@ -1,10 +1,7 @@
1143 from django.conf.urls import url
1144
1145-from notification.views import notices, mark_all_seen, feed_for_user, single
1146+from notification.views import notice_settings
1147
1148 urlpatterns = [
1149- url(r'^$', notices, name='notification_notices'),
1150- url(r'^(\d+)/$', single, name='notification_notice'),
1151- #url(r'^feed/$', feed_for_user, name="notification_feed_for_user"),
1152- url(r'^mark_all_seen/$', mark_all_seen, name='notification_mark_all_seen'),
1153+ url(r'^$', notice_settings, name='notification_notices'),
1154 ]
1155
1156=== modified file 'notification/views.py'
1157--- notification/views.py 2016-12-13 18:28:51 +0000
1158+++ notification/views.py 2017-05-07 09:51:42 +0000
1159@@ -1,30 +1,19 @@
1160-from django.core.urlresolvers import reverse
1161-from django.shortcuts import render_to_response, get_object_or_404
1162-from django.http import HttpResponseRedirect, Http404
1163+from django.shortcuts import render_to_response
1164 from django.template import RequestContext
1165 from django.contrib.auth.decorators import login_required
1166-from django.contrib.syndication.views import Feed
1167-
1168+from collections import OrderedDict
1169 from notification.models import *
1170-from notification.decorators import basic_auth_required, simple_basic_auth_callback
1171-from notification.feeds import NoticeUserFeed
1172-
1173-
1174-@basic_auth_required(realm='Notices Feed', callback_func=simple_basic_auth_callback)
1175-def feed_for_user(request):
1176- url = 'feed/%s' % request.user.username
1177- return Feed(request, url, {
1178- 'feed': NoticeUserFeed,
1179- })
1180
1181
1182 @login_required
1183-def notices(request):
1184- notice_types = NoticeType.objects.all()
1185- notices = Notice.objects.notices_for(request.user, on_site=True)
1186- settings_table = []
1187- for notice_type in NoticeType.objects.all():
1188- settings_row = []
1189+def notice_settings(request):
1190+ app_tables = {}
1191+ for notice_type in NoticeType.objects.all().order_by('label'):
1192+ # Assuming each notice_type.label begins with the name of the app
1193+ # followed by an underscore:
1194+ app = notice_type.label.partition('_')[0]
1195+ app_tables.setdefault(app, [])
1196+ checkbox_values = []
1197 for medium_id, medium_display in NOTICE_MEDIA:
1198 form_label = '%s_%s' % (notice_type.label, medium_id)
1199 setting = get_notification_setting(
1200@@ -35,65 +24,11 @@
1201 else:
1202 setting.send = False
1203 setting.save()
1204- settings_row.append((form_label, setting.send))
1205- settings_table.append(
1206- {'notice_type': notice_type, 'cells': settings_row})
1207-
1208- notice_settings = {
1209+ checkbox_values.append((form_label, setting.send))
1210+
1211+ app_tables[app].append({'notice_type': notice_type, 'html_values': checkbox_values})
1212+
1213+ return render_to_response('notification/notice_settings.html', {
1214 'column_headers': [medium_display for medium_id, medium_display in NOTICE_MEDIA],
1215- 'rows': settings_table,
1216- }
1217-
1218- return render_to_response('notification/notices.html', {
1219- 'notices': notices,
1220- 'notice_types': notice_types,
1221- 'notice_settings': notice_settings,
1222+ 'app_tables': OrderedDict(sorted(app_tables.items(), key=lambda t: t[0]))
1223 }, context_instance=RequestContext(request))
1224-
1225-
1226-@login_required
1227-def single(request, id):
1228- notice = get_object_or_404(Notice, id=id)
1229- if request.user == notice.user:
1230- return render_to_response('notification/single.html', {
1231- 'notice': notice,
1232- }, context_instance=RequestContext(request))
1233- raise Http404
1234-
1235-
1236-@login_required
1237-def archive(request, noticeid=None, next_page=None):
1238- if noticeid:
1239- try:
1240- notice = Notice.objects.get(id=noticeid)
1241- if request.user == notice.user or request.user.is_superuser:
1242- notice.archive()
1243- else: # you can archive other users' notices
1244- # only if you are superuser.
1245- return HttpResponseRedirect(next_page)
1246- except Notice.DoesNotExist:
1247- return HttpResponseRedirect(next_page)
1248- return HttpResponseRedirect(next_page)
1249-
1250-
1251-@login_required
1252-def delete(request, noticeid=None, next_page=None):
1253- if noticeid:
1254- try:
1255- notice = Notice.objects.get(id=noticeid)
1256- if request.user == notice.user or request.user.is_superuser:
1257- notice.delete()
1258- else: # you can delete other users' notices
1259- # only if you are superuser.
1260- return HttpResponseRedirect(next_page)
1261- except Notice.DoesNotExist:
1262- return HttpResponseRedirect(next_page)
1263- return HttpResponseRedirect(next_page)
1264-
1265-
1266-@login_required
1267-def mark_all_seen(request):
1268- for notice in Notice.objects.notices_for(request.user, unseen=True):
1269- notice.unseen = False
1270- notice.save()
1271- return HttpResponseRedirect(reverse('notification_notices'))
1272
1273=== modified file 'pybb/admin.py'
1274--- pybb/admin.py 2016-12-15 10:43:41 +0000
1275+++ pybb/admin.py 2017-05-07 09:51:42 +0000
1276@@ -48,7 +48,9 @@
1277 }
1278 ),
1279 )
1280-
1281+
1282+class SubscribersInline(admin.TabularInline):
1283+ model = Topic.subscribers.through
1284
1285 class TopicAdmin(admin.ModelAdmin):
1286 list_display = ['name', 'forum', 'created', 'head', 'is_hidden']
1287@@ -59,14 +61,13 @@
1288 fieldsets = (
1289 (None, {
1290 'fields': ('forum', 'name', 'user', ('created', 'updated'))
1291- }
1292- ),
1293+ }),
1294 (_('Additional options'), {
1295 'classes': ('collapse',),
1296- 'fields': (('views',), ('sticky', 'closed'), 'subscribers')
1297- }
1298- ),
1299+ 'fields': (('views',), ('sticky', 'closed'),)
1300+ }),
1301 )
1302+ inlines = [ SubscribersInline, ]
1303
1304
1305 class PostAdmin(admin.ModelAdmin):
1306
1307=== modified file 'pybb/forms.py'
1308--- pybb/forms.py 2017-01-23 13:01:31 +0000
1309+++ pybb/forms.py 2017-05-07 09:51:42 +0000
1310@@ -10,9 +10,7 @@
1311 from pybb.models import Topic, Post, PrivateMessage, Attachment
1312 from pybb import settings as pybb_settings
1313 from django.conf import settings
1314-from notification.models import send
1315-from django.core.mail import send_mail
1316-from django.contrib.sites.models import Site
1317+from notification.models import send, get_observers_for
1318
1319
1320 class AddPostForm(forms.ModelForm):
1321@@ -94,11 +92,11 @@
1322
1323 if not hidden:
1324 if topic_is_new:
1325- send(User.objects.all(), 'forum_new_topic',
1326- {'topic': topic, 'post': post, 'user': topic.user})
1327+ send(get_observers_for('forum_new_topic'), 'forum_new_topic',
1328+ {'topic': topic, 'post': post, 'user': topic.user}, queue = True)
1329 else:
1330 send(self.topic.subscribers.all(), 'forum_new_post',
1331- {'post': post, 'topic': topic, 'user': post.user})
1332+ {'post': post, 'topic': topic, 'user': post.user}, queue = True)
1333
1334 return post
1335
1336
1337=== modified file 'pybb/management/pybb_notifications.py'
1338--- pybb/management/pybb_notifications.py 2016-12-13 18:28:51 +0000
1339+++ pybb/management/pybb_notifications.py 2017-05-07 09:51:42 +0000
1340@@ -14,6 +14,8 @@
1341 _('Forum New Post'),
1342 _('a new comment has been posted to a topic you observe'))
1343
1344+ # TODO (Franku): post_syncdb is deprecated since Django 1.7
1345+ # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
1346 signals.post_syncdb.connect(create_notice_types,
1347 sender=notification)
1348 except ImportError:
1349
1350=== modified file 'templates/notification/email_body.txt'
1351--- templates/notification/email_body.txt 2015-03-15 20:05:49 +0000
1352+++ templates/notification/email_body.txt 2017-05-07 09:51:42 +0000
1353@@ -1,5 +1,5 @@
1354 {% load i18n %}{% blocktrans %}You have received the following notice from {{ current_site }}:
1355
1356 {{ message }}
1357-To see other notices or change how you receive notifications, please go to {{ notices_url }}.
1358-{% endblocktrans %}
1359\ No newline at end of file
1360+To change how you receive notifications, please go to {{ notices_url }}.
1361+{% endblocktrans %}
1362
1363=== removed file 'templates/notification/forum_new_post/notice.html'
1364--- templates/notification/forum_new_post/notice.html 2016-06-06 18:26:47 +0000
1365+++ templates/notification/forum_new_post/notice.html 1970-01-01 00:00:00 +0000
1366@@ -1,5 +0,0 @@
1367-{% load i18n %}{% url 'profile_view' user.username as user_url %}
1368-{% blocktrans with topic.get_absolute_url as topic_url and post.get_absolute_url as post_url%}
1369-<a href="{{ user_url }}">{{ user }}</a>
1370-has <a href="{{ post_url }}">replied</a>
1371-to the forum topic <a href="{{ topic_url }}">{{ topic }}</a>.{% endblocktrans %}
1372
1373=== removed file 'templates/notification/forum_new_topic/notice.html'
1374--- templates/notification/forum_new_topic/notice.html 2016-06-06 18:26:47 +0000
1375+++ templates/notification/forum_new_topic/notice.html 1970-01-01 00:00:00 +0000
1376@@ -1,4 +0,0 @@
1377-{% load i18n %}{% url 'profile_view' user.username as user_url %}
1378-{% blocktrans with topic.get_absolute_url as topic_url %}
1379-A new forum topic has be created under <a href="{{ topic_url }}">{{ topic }}</a>
1380-by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
1381
1382=== removed file 'templates/notification/full.html'
1383--- templates/notification/full.html 2009-02-20 16:46:21 +0000
1384+++ templates/notification/full.html 1970-01-01 00:00:00 +0000
1385@@ -1,1 +0,0 @@
1386-{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
1387\ No newline at end of file
1388
1389=== added directory 'templates/notification/maps_new_map'
1390=== added file 'templates/notification/maps_new_map/full.txt'
1391--- templates/notification/maps_new_map/full.txt 1970-01-01 00:00:00 +0000
1392+++ templates/notification/maps_new_map/full.txt 2017-05-07 09:51:42 +0000
1393@@ -0,0 +1,7 @@
1394+{% load i18n %}A new map was uploaded to the Website by {{ user }}:
1395+{% blocktrans %}
1396+Mapname: {{ mapname }}
1397+Description: {{ uploader_comment }}
1398+–––––––––––
1399+Link to map: http://{{ current_site }}{{ url }}
1400+{% endblocktrans %}
1401\ No newline at end of file
1402
1403=== removed directory 'templates/notification/messages_deleted'
1404=== removed file 'templates/notification/messages_deleted/full.txt'
1405--- templates/notification/messages_deleted/full.txt 2009-02-26 11:32:18 +0000
1406+++ templates/notification/messages_deleted/full.txt 1970-01-01 00:00:00 +0000
1407@@ -1,1 +0,0 @@
1408-{% load i18n %}{% blocktrans %}You have deleted the message '{{ message }}'.{% endblocktrans %}
1409
1410=== removed file 'templates/notification/messages_deleted/notice.html'
1411--- templates/notification/messages_deleted/notice.html 2009-02-26 11:32:18 +0000
1412+++ templates/notification/messages_deleted/notice.html 1970-01-01 00:00:00 +0000
1413@@ -1,1 +0,0 @@
1414-{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have deleted the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}
1415
1416=== removed file 'templates/notification/messages_received/notice.html'
1417--- templates/notification/messages_received/notice.html 2016-03-02 21:02:38 +0000
1418+++ templates/notification/messages_received/notice.html 1970-01-01 00:00:00 +0000
1419@@ -1,3 +0,0 @@
1420-{% load i18n %}
1421-{% load wlprofile_extras %}
1422-{% 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 %}
1423
1424=== removed directory 'templates/notification/messages_recovered'
1425=== removed file 'templates/notification/messages_recovered/full.txt'
1426--- templates/notification/messages_recovered/full.txt 2009-02-26 11:32:18 +0000
1427+++ templates/notification/messages_recovered/full.txt 1970-01-01 00:00:00 +0000
1428@@ -1,1 +0,0 @@
1429-{% load i18n %}{% blocktrans %}You have recovered the message '{{ message }}'.{% endblocktrans %}
1430
1431=== removed file 'templates/notification/messages_recovered/notice.html'
1432--- templates/notification/messages_recovered/notice.html 2009-02-26 11:32:18 +0000
1433+++ templates/notification/messages_recovered/notice.html 1970-01-01 00:00:00 +0000
1434@@ -1,1 +0,0 @@
1435-{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have recovered the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}
1436
1437=== removed directory 'templates/notification/messages_replied'
1438=== removed file 'templates/notification/messages_replied/full.txt'
1439--- templates/notification/messages_replied/full.txt 2009-02-26 11:32:18 +0000
1440+++ templates/notification/messages_replied/full.txt 1970-01-01 00:00:00 +0000
1441@@ -1,1 +0,0 @@
1442-{% 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 %}
1443
1444=== removed file 'templates/notification/messages_replied/notice.html'
1445--- templates/notification/messages_replied/notice.html 2016-03-02 21:02:38 +0000
1446+++ templates/notification/messages_replied/notice.html 1970-01-01 00:00:00 +0000
1447@@ -1,3 +0,0 @@
1448-{% load i18n %}
1449-{% load wlprofile_extras %}
1450-{% 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 %}
1451
1452=== removed file 'templates/notification/messages_reply_received/notice.html'
1453--- templates/notification/messages_reply_received/notice.html 2016-03-02 21:02:38 +0000
1454+++ templates/notification/messages_reply_received/notice.html 1970-01-01 00:00:00 +0000
1455@@ -1,3 +0,0 @@
1456-{% load i18n %}
1457-{% load wlprofile_extras %}
1458-{% 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 %}
1459
1460=== removed directory 'templates/notification/messages_sent'
1461=== removed file 'templates/notification/messages_sent/full.txt'
1462--- templates/notification/messages_sent/full.txt 2009-02-26 11:32:18 +0000
1463+++ templates/notification/messages_sent/full.txt 1970-01-01 00:00:00 +0000
1464@@ -1,1 +0,0 @@
1465-{% load i18n %}{% blocktrans with message.recipient as message_recipient %}You have sent the message '{{ message }}' to {{ message_recipient }}.{% endblocktrans %}
1466
1467=== removed file 'templates/notification/messages_sent/notice.html'
1468--- templates/notification/messages_sent/notice.html 2016-03-02 21:02:38 +0000
1469+++ templates/notification/messages_sent/notice.html 1970-01-01 00:00:00 +0000
1470@@ -1,3 +0,0 @@
1471-{% load i18n %}
1472-{% load wlprofile_extras %}
1473-{% 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 %}
1474
1475=== removed file 'templates/notification/notice.html'
1476--- templates/notification/notice.html 2009-02-20 16:46:21 +0000
1477+++ templates/notification/notice.html 1970-01-01 00:00:00 +0000
1478@@ -1,1 +0,0 @@
1479-{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
1480\ No newline at end of file
1481
1482=== renamed file 'templates/notification/notices.html' => 'templates/notification/notice_settings.html'
1483--- templates/notification/notices.html 2017-03-06 20:15:27 +0000
1484+++ templates/notification/notice_settings.html 2017-05-07 09:51:42 +0000
1485@@ -9,39 +9,7 @@
1486 {% endblock %}
1487
1488 {% block content %}
1489-<h1>{% trans "Notifications" %}</h1>
1490-{% comment "Testing if this is used by users" %}
1491-<div class="blogEntry">
1492-{% autopaginate notices %}
1493-
1494-{% if notices %}
1495- <a href="{% url 'notification_mark_all_seen' %}" class="posRight small">{% trans "Mark all as seen" %}</a>
1496- {% paginate %}
1497-
1498- {# TODO: get timezone support working with regroup #}
1499- {% regroup notices by added.date as notices_by_date %}
1500- {% for date in notices_by_date %}
1501- <br />
1502- <h3>{{ date.grouper|naturalday:_("MONTH_DAY_FORMAT")|title }}</h3>
1503- <table class="notifications">
1504- {% for notice in date.list %}
1505- <tr class="{% cycle "odd" "even" %} {% if notice.is_unseen %}italic{% endif %}">
1506- <td class="type"><a href="{% url 'notification_notice' notice.pk %} ">{% trans notice.notice_type.display %}</a></td>
1507- <td class="text">{{ notice.message|safe }}</td>
1508- <td class="date">{{ notice.added|custom_date:user }}</td>
1509- </tr>
1510- {% endfor %}
1511- </table>
1512- {% endfor %}
1513- <br />
1514- {% paginate %}
1515-{% else %}
1516- <p>
1517- {% trans "No notifications." %}
1518- </p>
1519-{% endif %}
1520-</div>
1521-{% endcomment %}
1522+<h1>{% trans "Notification Settings" %}</h1>
1523
1524 <div class="blogEntry">
1525 <h2>{% trans "Settings" %}</h2>
1526@@ -57,32 +25,34 @@
1527 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>.
1528 </p>
1529 {% endif %}
1530-
1531- <form method="POST" action="."> {# doubt this easy to do in uni-form #}
1532+ <form method="POST" action=".">
1533+ {% for app, settings in app_tables.items %}
1534+ <h3>{{ app|capfirst }} </h3>
1535 <table class="notifications">
1536 <tr>
1537 <th class="small">{% trans "Notification Type" %}</th>
1538- {% for header in notice_settings.column_headers %}
1539- <th class="small">{{ header }}</th>
1540- {% endfor %}
1541+ {% for header in column_headers %}
1542+ <th class="small">{{ header }}</th>
1543+ {% endfor %}
1544 </tr>
1545- {% for row in notice_settings.rows %}
1546+ {% for setting in settings %}
1547 <tr class="{% cycle "odd" "even" %}">
1548 <td>
1549- {% trans row.notice_type.display %}<br />
1550- {% trans row.notice_type.description %}
1551- </td>
1552- {% for cell in row.cells %}
1553- <td>
1554- <input type="checkbox" name="{{ cell.0 }}" {% if cell.1 %}checked="checked" {% endif %}/>
1555- </td>
1556+ {% trans setting.notice_type.display %}<br />
1557+ {% trans setting.notice_type.description %}
1558+ </td>
1559+ <td style="width: 5em;">
1560+ {% for html_value in setting.html_values %}
1561+ <input type="checkbox" name="{{ html_value.0 }}" {% if html_value.1 %}checked="checked" {% endif %}/>
1562+ {% endfor %}
1563+ </td>
1564+ </tr>
1565 {% endfor %}
1566- </tr>
1567- {% endfor %}
1568 </table>
1569 <br />
1570- {% csrf_token %}
1571- <input type="submit" value="{% trans "Change" %}" />
1572+ {% endfor %}
1573+ {% csrf_token %}
1574+ <input class="posRight" type="submit" value="{% trans "Change" %}" />
1575 </form>
1576 </div>
1577 {% endblock %}
1578
1579=== removed file 'templates/notification/single.html'
1580--- templates/notification/single.html 2012-05-08 21:52:15 +0000
1581+++ templates/notification/single.html 1970-01-01 00:00:00 +0000
1582@@ -1,40 +0,0 @@
1583-{% extends "notification/base.html" %}
1584-
1585-{% load humanize i18n %}
1586-{% load custom_date %}
1587-
1588-{% block title %}
1589-{% trans "Notification "%} - {{ block.super }}
1590-{% endblock %}
1591-
1592-{% block content %}
1593-<h1>{% trans "Notification" %}: {{ notice.notice_type.display }}</h1>
1594-<div class="blogEntry">
1595- <table>
1596- <tr>
1597- <td class="grey">Notice:</td>
1598- <td>{{ notice.message|safe }}</td>
1599- </tr>
1600- <tr>
1601- <td class="grey">Notice Type:</td>
1602- <td>{{ notice.notice_type.display }}</td>
1603- </tr>
1604- <tr>
1605- <td class="grey">Date of Notice:</td>
1606- <td>{{ notice.added }}</td>
1607- </tr>
1608- <tr>
1609- <td class="grey">Notice Unseen:</td>
1610- <td>{{ notice.unseen }}</td>
1611- </tr>
1612- <tr>
1613- <td class="grey">Notice Archiv:</td>
1614- <td>{{ notice.archived }}</td>
1615- </tr>
1616- <tr>
1617- <td class="grey">Notice Site:</td>
1618- <td>{{ notice.on_site }}</td>
1619- </tr>
1620- </table>
1621-</div>
1622-{% endblock %}
1623
1624=== removed file 'templates/notification/wiki_article_edited/notice.html'
1625--- templates/notification/wiki_article_edited/notice.html 2016-06-06 18:26:47 +0000
1626+++ templates/notification/wiki_article_edited/notice.html 1970-01-01 00:00:00 +0000
1627@@ -1,4 +0,0 @@
1628-{% load i18n %}{% url 'profile_view' user.username as user_url %}
1629-{% blocktrans with article.get_absolute_url as article_url %}
1630-The wiki article <a href="{{ article_url }}">{{ article }}</a>
1631-has been edited by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
1632
1633=== removed file 'templates/notification/wiki_observed_article_changed/notice.html'
1634--- templates/notification/wiki_observed_article_changed/notice.html 2009-02-20 10:11:49 +0000
1635+++ templates/notification/wiki_observed_article_changed/notice.html 1970-01-01 00:00:00 +0000
1636@@ -1,1 +0,0 @@
1637-{% 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 %}
1638
1639=== removed file 'templates/notification/wiki_revision_reverted/notice.html'
1640--- templates/notification/wiki_revision_reverted/notice.html 2009-02-20 10:11:49 +0000
1641+++ templates/notification/wiki_revision_reverted/notice.html 1970-01-01 00:00:00 +0000
1642@@ -1,2 +0,0 @@
1643-{% load i18n %}
1644-{% 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 %}
1645
1646=== modified file 'wiki/forms.py'
1647--- wiki/forms.py 2017-02-24 19:15:17 +0000
1648+++ wiki/forms.py 2017-05-07 09:51:42 +0000
1649@@ -8,6 +8,11 @@
1650 from wiki.models import Article
1651 from wiki.models import ChangeSet
1652 from settings import WIKI_WORD_RE
1653+try:
1654+ from notification import models as notification
1655+except:
1656+ notification = None
1657+
1658
1659 wikiword_pattern = re.compile('^' + WIKI_WORD_RE + '$')
1660
1661@@ -102,6 +107,8 @@
1662 article.creator = editor
1663 article.group = group
1664 article.save(*args, **kwargs)
1665+ if notification:
1666+ notification.observe(article, editor, 'wiki_observed_article_changed')
1667
1668 # 4 - Create new revision
1669 changeset = article.new_revision(
1670
1671=== modified file 'wiki/management.py'
1672--- wiki/management.py 2016-12-13 18:28:51 +0000
1673+++ wiki/management.py 2017-05-07 09:51:42 +0000
1674@@ -6,9 +6,6 @@
1675 from notification import models as notification
1676
1677 def create_notice_types(app, created_models, verbosity, **kwargs):
1678- notification.create_notice_type('wiki_article_edited',
1679- _('Article Edited'),
1680- _('your article has been edited'))
1681 notification.create_notice_type('wiki_revision_reverted',
1682 _('Article Revision Reverted'),
1683 _('your revision has been reverted'))
1684@@ -16,6 +13,8 @@
1685 _('Observed Article Changed'),
1686 _('an article you observe has changed'))
1687
1688+ # TODO (Franku): post_syncdb is deprecated since Django 1.7
1689+ # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
1690 signals.post_syncdb.connect(create_notice_types,
1691 sender=notification)
1692 except ImportError:
1693
1694=== added file 'wlmaps/management.py'
1695--- wlmaps/management.py 1970-01-01 00:00:00 +0000
1696+++ wlmaps/management.py 2017-05-07 09:51:42 +0000
1697@@ -0,0 +1,18 @@
1698+from django.db.models import signals
1699+
1700+from django.utils.translation import ugettext_noop as _
1701+
1702+try:
1703+ from notification import models as notification
1704+
1705+ def create_notice_types(app, created_models, verbosity, **kwargs):
1706+ notification.create_notice_type('maps_new_map',
1707+ _('A new Map is available'),
1708+ _('a new map is available for download'),1)
1709+
1710+ # TODO (Franku): post_syncdb is deprecated since Django 1.7
1711+ # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
1712+ signals.post_syncdb.connect(create_notice_types,
1713+ sender=notification)
1714+except ImportError:
1715+ print 'Skipping creation of NoticeTypes as notification app not found'
1716
1717=== modified file 'wlmaps/models.py'
1718--- wlmaps/models.py 2016-08-11 18:13:59 +0000
1719+++ wlmaps/models.py 2017-05-07 09:51:42 +0000
1720@@ -5,6 +5,10 @@
1721 from django.contrib.auth.models import User
1722 from django.template.defaultfilters import slugify
1723 import datetime
1724+try:
1725+ from notification import models as notification
1726+except ImportError:
1727+ notification = None
1728
1729 import settings
1730 if settings.USE_SPHINX:
1731@@ -64,4 +68,8 @@
1732 self.slug = slugify(self.name)
1733
1734 map = super(Map, self).save(*args, **kwargs)
1735- return map
1736+ if notification:
1737+ notification.send(notification.get_observers_for('wlmaps_new_map'), 'wlmaps_new_map',
1738+ {'mapname': self.name, 'url': self.get_absolute_url(), 'user': self.uploader, 'uploader_comment': self.uploader_comment}, queue=True)
1739+
1740+ return map

Subscribers

People subscribed via source and target branches