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

Proposed by kaputtnik on 2017-05-01
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 2017-05-01 Approve on 2017-05-04
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.
SirVer (sirver) wrote :

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

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
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 on 2017-05-03

adressed code review; sorted notice settings

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
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 on 2017-05-05

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

472. By kaputtnik on 2017-05-07

modified a comment to explain why a try statement is needed

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.

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

SirVer (sirver) wrote :

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

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?

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
=== modified file 'media/css/notice.css'
--- media/css/notice.css 2012-05-08 21:52:15 +0000
+++ media/css/notice.css 2017-05-07 09:51:42 +0000
@@ -12,7 +12,6 @@
12.notifications th {12.notifications th {
13 border: none;13 border: none;
14 padding: 4px;14 padding: 4px;
15 padding-top: 20px;
16 text-align: left;15 text-align: left;
17 font-weight: normal;16 font-weight: normal;
18}17}
1918
=== 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-07 09:51:42 +0000
@@ -0,0 +1,19 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import models, migrations
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('news', '0001_initial'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='post',
16 name='body',
17 field=models.TextField(help_text=b'Text entered here will be rendered using Markdown', verbose_name='body'),
18 ),
19 ]
020
=== modified file 'notification/README'
--- notification/README 2016-05-17 19:28:38 +0000
+++ notification/README 2017-05-07 09:51:42 +0000
@@ -1,7 +1,12 @@
1This is the old version (0.1.4) of the notification app by James Tauber.1This is the old version (0.1.4) of the notification app by James Tauber.
2I have included it as a widelands app because the new version is 2It is included as a widelands app because the new version is
3incompatible with our old data.3incompatible with our old data.
44
5Year 2017:
6The ability to store notices is removed and therefor it acts only as an app
7to send e-mails for observed items.
8
9
5See the file LICENSE for Copyright notice.10See the file LICENSE for Copyright notice.
611
7Original Description:12Original Description:
813
=== modified file 'notification/admin.py'
--- notification/admin.py 2016-12-13 18:28:51 +0000
+++ notification/admin.py 2017-05-07 09:51:42 +0000
@@ -1,5 +1,6 @@
1from django.contrib import admin1from django.contrib import admin
2from notification.models import NoticeType, NoticeSetting, Notice, ObservedItem2from notification.models import NoticeType, NoticeSetting, ObservedItem
3from django.utils.translation import ugettext_lazy as _
34
45
5class NoticeTypeAdmin(admin.ModelAdmin):6class NoticeTypeAdmin(admin.ModelAdmin):
@@ -7,15 +8,20 @@
78
89
9class NoticeSettingAdmin(admin.ModelAdmin):10class NoticeSettingAdmin(admin.ModelAdmin):
10 list_display = ('id', 'user', 'notice_type', 'medium', 'send')11 search_fields = ['user__username',]
1112 list_display = ('user', 'notice_type', 'medium', 'send')
1213
13class NoticeAdmin(admin.ModelAdmin):14
14 list_display = ('message', 'user', 'notice_type',15class ObserverdItemAdmin(admin.ModelAdmin):
15 'added', 'unseen', 'archived')16 readonly_fields = ('observed_object', 'content_type', 'object_id')
1617 search_fields = ['user__username', 'notice_type__label']
18 list_display = ('user', 'notice_type', 'content_type', 'get_content_object')
19 fieldsets = (
20 (None, {'fields': ('user',)}),
21 (_('Observed object'), {'fields': ('observed_object', 'content_type', 'object_id')}),
22 (_('Settings'), {'fields': ('added', 'notice_type', 'signal')}),
23 )
1724
18admin.site.register(NoticeType, NoticeTypeAdmin)25admin.site.register(NoticeType, NoticeTypeAdmin)
19admin.site.register(NoticeSetting, NoticeSettingAdmin)26admin.site.register(NoticeSetting, NoticeSettingAdmin)
20admin.site.register(Notice, NoticeAdmin)27admin.site.register(ObservedItem, ObserverdItemAdmin)
21admin.site.register(ObservedItem)
2228
=== 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 @@
1#
2# django-atompub by James Tauber <http://jtauber.com/>
3# http://code.google.com/p/django-atompub/
4# An implementation of the Atom format and protocol for Django
5#
6# For instructions on how to use this module to generate Atom feeds,
7# see http://code.google.com/p/django-atompub/wiki/UserGuide
8#
9#
10# Copyright (c) 2007, James Tauber
11#
12# Permission is hereby granted, free of charge, to any person obtaining a copy
13# of this software and associated documentation files (the "Software"), to deal
14# in the Software without restriction, including without limitation the rights
15# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16# copies of the Software, and to permit persons to whom the Software is
17# furnished to do so, subject to the following conditions:
18#
19# The above copyright notice and this permission notice shall be included in
20# all copies or substantial portions of the Software.
21#
22# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
28# THE SOFTWARE.
29#
30
31from xml.sax.saxutils import XMLGenerator
32from datetime import datetime
33
34
35GENERATOR_TEXT = 'django-atompub'
36GENERATOR_ATTR = {
37 'uri': 'http://code.google.com/p/django-atompub/',
38 'version': 'r33'
39}
40
41
42# based on django.utils.xmlutils.SimplerXMLGenerator
43class SimplerXMLGenerator(XMLGenerator):
44
45 def addQuickElement(self, name, contents=None, attrs=None):
46 """Convenience method for adding an element with no children."""
47 if attrs is None:
48 attrs = {}
49 self.startElement(name, attrs)
50 if contents is not None:
51 self.characters(contents)
52 self.endElement(name)
53
54
55# based on django.utils.feedgenerator.rfc3339_date
56def rfc3339_date(date):
57 return date.strftime('%Y-%m-%dT%H:%M:%SZ')
58
59
60# based on django.utils.feedgenerator.get_tag_uri
61def get_tag_uri(url, date):
62 """Creates a TagURI.
63
64 See http://diveintomark.org/archives/2004/05/28/howto-atom-id
65
66 """
67 tag = re.sub('^http://', '', url)
68 if date is not None:
69 tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1)
70 tag = re.sub('#', '/', tag)
71 return 'tag:' + tag
72
73
74# based on django.contrib.syndication.feeds.Feed
75class Feed(object):
76
77 VALIDATE = True
78
79 def __init__(self, slug, feed_url):
80 # @@@ slug and feed_url are not used yet
81 pass
82
83 def __get_dynamic_attr(self, attname, obj, default=None):
84 try:
85 attr = getattr(self, attname)
86 except AttributeError:
87 return default
88 if callable(attr):
89 # Check func_code.co_argcount rather than try/excepting the
90 # function and catching the TypeError, because something inside
91 # the function may raise the TypeError. This technique is more
92 # accurate.
93 if hasattr(attr, 'func_code'):
94 argcount = attr.func_code.co_argcount
95 else:
96 argcount = attr.__call__.func_code.co_argcount
97 if argcount == 2: # one argument is 'self'
98 return attr(obj)
99 else:
100 return attr()
101 return attr
102
103 def get_feed(self, extra_params=None):
104
105 if extra_params:
106 try:
107 obj = self.get_object(extra_params.split('/'))
108 except (AttributeError, LookupError):
109 raise LookupError('Feed does not exist')
110 else:
111 obj = None
112
113 feed = AtomFeed(
114 atom_id=self.__get_dynamic_attr('feed_id', obj),
115 title=self.__get_dynamic_attr('feed_title', obj),
116 updated=self.__get_dynamic_attr('feed_updated', obj),
117 icon=self.__get_dynamic_attr('feed_icon', obj),
118 logo=self.__get_dynamic_attr('feed_logo', obj),
119 rights=self.__get_dynamic_attr('feed_rights', obj),
120 subtitle=self.__get_dynamic_attr('feed_subtitle', obj),
121 authors=self.__get_dynamic_attr('feed_authors', obj, default=[]),
122 categories=self.__get_dynamic_attr(
123 'feed_categories', obj, default=[]),
124 contributors=self.__get_dynamic_attr(
125 'feed_contributors', obj, default=[]),
126 links=self.__get_dynamic_attr('feed_links', obj, default=[]),
127 extra_attrs=self.__get_dynamic_attr('feed_extra_attrs', obj),
128 hide_generator=self.__get_dynamic_attr(
129 'hide_generator', obj, default=False)
130 )
131
132 items = self.__get_dynamic_attr('items', obj)
133 if items is None:
134 raise LookupError('Feed has no items field')
135
136 for item in items:
137 feed.add_item(
138 atom_id=self.__get_dynamic_attr('item_id', item),
139 title=self.__get_dynamic_attr('item_title', item),
140 updated=self.__get_dynamic_attr('item_updated', item),
141 content=self.__get_dynamic_attr('item_content', item),
142 published=self.__get_dynamic_attr('item_published', item),
143 rights=self.__get_dynamic_attr('item_rights', item),
144 source=self.__get_dynamic_attr('item_source', item),
145 summary=self.__get_dynamic_attr('item_summary', item),
146 authors=self.__get_dynamic_attr(
147 'item_authors', item, default=[]),
148 categories=self.__get_dynamic_attr(
149 'item_categories', item, default=[]),
150 contributors=self.__get_dynamic_attr(
151 'item_contributors', item, default=[]),
152 links=self.__get_dynamic_attr('item_links', item, default=[]),
153 extra_attrs=self.__get_dynamic_attr(
154 'item_extra_attrs', None, default={}),
155 )
156
157 if self.VALIDATE:
158 feed.validate()
159 return feed
160
161
162class ValidationError(Exception):
163 pass
164
165
166# based on django.utils.feedgenerator.SyndicationFeed and
167# django.utils.feedgenerator.Atom1Feed
168class AtomFeed(object):
169
170 mime_type = 'application/atom+xml'
171 ns = u'http://www.w3.org/2005/Atom'
172
173 def __init__(self, atom_id, title, updated=None, icon=None, logo=None, rights=None, subtitle=None,
174 authors=[], categories=[], contributors=[], links=[], extra_attrs={}, hide_generator=False):
175 if atom_id is None:
176 raise LookupError('Feed has no feed_id field')
177 if title is None:
178 raise LookupError('Feed has no feed_title field')
179 # if updated == None, we'll calculate it
180 self.feed = {
181 'id': atom_id,
182 'title': title,
183 'updated': updated,
184 'icon': icon,
185 'logo': logo,
186 'rights': rights,
187 'subtitle': subtitle,
188 'authors': authors,
189 'categories': categories,
190 'contributors': contributors,
191 'links': links,
192 'extra_attrs': extra_attrs,
193 'hide_generator': hide_generator,
194 }
195 self.items = []
196
197 def add_item(self, atom_id, title, updated, content=None, published=None, rights=None, source=None, summary=None,
198 authors=[], categories=[], contributors=[], links=[], extra_attrs={}):
199 if atom_id is None:
200 raise LookupError('Feed has no item_id method')
201 if title is None:
202 raise LookupError('Feed has no item_title method')
203 if updated is None:
204 raise LookupError('Feed has no item_updated method')
205 self.items.append({
206 'id': atom_id,
207 'title': title,
208 'updated': updated,
209 'content': content,
210 'published': published,
211 'rights': rights,
212 'source': source,
213 'summary': summary,
214 'authors': authors,
215 'categories': categories,
216 'contributors': contributors,
217 'links': links,
218 'extra_attrs': extra_attrs,
219 })
220
221 def latest_updated(self):
222 """Returns the latest item's updated or the current time if there are
223 no items."""
224 updates = [item['updated'] for item in self.items]
225 if len(updates) > 0:
226 updates.sort()
227 return updates[-1]
228 else:
229 # @@@ really we should allow a feed to define its "start" for this case
230 return datetime.now()
231
232 def write_text_construct(self, handler, element_name, data):
233 if isinstance(data, tuple):
234 text_type, text = data
235 if text_type == 'xhtml':
236 handler.startElement(element_name, {'type': text_type})
237 # write unescaped -- it had better be well-formed XML
238 handler._write(text)
239 handler.endElement(element_name)
240 else:
241 handler.addQuickElement(
242 element_name, text, {'type': text_type})
243 else:
244 handler.addQuickElement(element_name, data)
245
246 def write_person_construct(self, handler, element_name, person):
247 handler.startElement(element_name, {})
248 handler.addQuickElement(u'name', person['name'])
249 if 'uri' in person:
250 handler.addQuickElement(u'uri', person['uri'])
251 if 'email' in person:
252 handler.addQuickElement(u'email', person['email'])
253 handler.endElement(element_name)
254
255 def write_link_construct(self, handler, link):
256 if 'length' in link:
257 link['length'] = str(link['length'])
258 handler.addQuickElement(u'link', None, link)
259
260 def write_category_construct(self, handler, category):
261 handler.addQuickElement(u'category', None, category)
262
263 def write_source(self, handler, data):
264 handler.startElement(u'source', {})
265 if data.get('id'):
266 handler.addQuickElement(u'id', data['id'])
267 if data.get('title'):
268 self.write_text_construct(handler, u'title', data['title'])
269 if data.get('subtitle'):
270 self.write_text_construct(handler, u'subtitle', data['subtitle'])
271 if data.get('icon'):
272 handler.addQuickElement(u'icon', data['icon'])
273 if data.get('logo'):
274 handler.addQuickElement(u'logo', data['logo'])
275 if data.get('updated'):
276 handler.addQuickElement(u'updated', rfc3339_date(data['updated']))
277 for category in data.get('categories', []):
278 self.write_category_construct(handler, category)
279 for link in data.get('links', []):
280 self.write_link_construct(handler, link)
281 for author in data.get('authors', []):
282 self.write_person_construct(handler, u'author', author)
283 for contributor in data.get('contributors', []):
284 self.write_person_construct(handler, u'contributor', contributor)
285 if data.get('rights'):
286 self.write_text_construct(handler, u'rights', data['rights'])
287 handler.endElement(u'source')
288
289 def write_content(self, handler, data):
290 if isinstance(data, tuple):
291 content_dict, text = data
292 if content_dict.get('type') == 'xhtml':
293 handler.startElement(u'content', content_dict)
294 # write unescaped -- it had better be well-formed XML
295 handler._write(text)
296 handler.endElement(u'content')
297 else:
298 handler.addQuickElement(u'content', text, content_dict)
299 else:
300 handler.addQuickElement(u'content', data)
301
302 def write(self, outfile, encoding):
303 handler = SimplerXMLGenerator(outfile, encoding)
304 handler.startDocument()
305 feed_attrs = {u'xmlns': self.ns}
306 if self.feed.get('extra_attrs'):
307 feed_attrs.update(self.feed['extra_attrs'])
308 handler.startElement(u'feed', feed_attrs)
309 handler.addQuickElement(u'id', self.feed['id'])
310 self.write_text_construct(handler, u'title', self.feed['title'])
311 if self.feed.get('subtitle'):
312 self.write_text_construct(
313 handler, u'subtitle', self.feed['subtitle'])
314 if self.feed.get('icon'):
315 handler.addQuickElement(u'icon', self.feed['icon'])
316 if self.feed.get('logo'):
317 handler.addQuickElement(u'logo', self.feed['logo'])
318 if self.feed['updated']:
319 handler.addQuickElement(
320 u'updated', rfc3339_date(self.feed['updated']))
321 else:
322 handler.addQuickElement(
323 u'updated', rfc3339_date(self.latest_updated()))
324 for category in self.feed['categories']:
325 self.write_category_construct(handler, category)
326 for link in self.feed['links']:
327 self.write_link_construct(handler, link)
328 for author in self.feed['authors']:
329 self.write_person_construct(handler, u'author', author)
330 for contributor in self.feed['contributors']:
331 self.write_person_construct(handler, u'contributor', contributor)
332 if self.feed.get('rights'):
333 self.write_text_construct(handler, u'rights', self.feed['rights'])
334 if not self.feed.get('hide_generator'):
335 handler.addQuickElement(
336 u'generator', GENERATOR_TEXT, GENERATOR_ATTR)
337
338 self.write_items(handler)
339
340 handler.endElement(u'feed')
341
342 def write_items(self, handler):
343 for item in self.items:
344 entry_attrs = item.get('extra_attrs', {})
345 handler.startElement(u'entry', entry_attrs)
346
347 handler.addQuickElement(u'id', item['id'])
348 self.write_text_construct(handler, u'title', item['title'])
349 handler.addQuickElement(u'updated', rfc3339_date(item['updated']))
350 if item.get('published'):
351 handler.addQuickElement(
352 u'published', rfc3339_date(item['published']))
353 if item.get('rights'):
354 self.write_text_construct(handler, u'rights', item['rights'])
355 if item.get('source'):
356 self.write_source(handler, item['source'])
357
358 for author in item['authors']:
359 self.write_person_construct(handler, u'author', author)
360 for contributor in item['contributors']:
361 self.write_person_construct(
362 handler, u'contributor', contributor)
363 for category in item['categories']:
364 self.write_category_construct(handler, category)
365 for link in item['links']:
366 self.write_link_construct(handler, link)
367 if item.get('summary'):
368 self.write_text_construct(handler, u'summary', item['summary'])
369 if item.get('content'):
370 self.write_content(handler, item['content'])
371
372 handler.endElement(u'entry')
373
374 def validate(self):
375
376 def validate_text_construct(obj):
377 if isinstance(obj, tuple):
378 if obj[0] not in ['text', 'html', 'xhtml']:
379 return False
380 # @@@ no validation is done that 'html' text constructs are valid HTML
381 # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
382
383 return True
384
385 if not validate_text_construct(self.feed['title']):
386 raise ValidationError('feed title has invalid type')
387 if self.feed.get('subtitle'):
388 if not validate_text_construct(self.feed['subtitle']):
389 raise ValidationError('feed subtitle has invalid type')
390 if self.feed.get('rights'):
391 if not validate_text_construct(self.feed['rights']):
392 raise ValidationError('feed rights has invalid type')
393
394 alternate_links = {}
395 for link in self.feed.get('links'):
396 if link.get('rel') == 'alternate' or link.get('rel') == None:
397 key = (link.get('type'), link.get('hreflang'))
398 if key in alternate_links:
399 raise ValidationError(
400 'alternate links must have unique type/hreflang')
401 alternate_links[key] = link
402
403 if self.feed.get('authors'):
404 feed_author = True
405 else:
406 feed_author = False
407
408 for item in self.items:
409 if not feed_author and not item.get('authors'):
410 if item.get('source') and item['source'].get('authors'):
411 pass
412 else:
413 raise ValidationError(
414 'if no feed author, all entries must have author (possibly in source)')
415
416 if not validate_text_construct(item['title']):
417 raise ValidationError('entry title has invalid type')
418 if item.get('rights'):
419 if not validate_text_construct(item['rights']):
420 raise ValidationError('entry rights has invalid type')
421 if item.get('summary'):
422 if not validate_text_construct(item['summary']):
423 raise ValidationError('entry summary has invalid type')
424 source = item.get('source')
425 if source:
426 if source.get('title'):
427 if not validate_text_construct(source['title']):
428 raise ValidationError('source title has invalid type')
429 if source.get('subtitle'):
430 if not validate_text_construct(source['subtitle']):
431 raise ValidationError(
432 'source subtitle has invalid type')
433 if source.get('rights'):
434 if not validate_text_construct(source['rights']):
435 raise ValidationError('source rights has invalid type')
436
437 alternate_links = {}
438 for link in item.get('links'):
439 if link.get('rel') == 'alternate' or link.get('rel') == None:
440 key = (link.get('type'), link.get('hreflang'))
441 if key in alternate_links:
442 raise ValidationError(
443 'alternate links must have unique type/hreflang')
444 alternate_links[key] = link
445
446 if not item.get('content'):
447 if not alternate_links:
448 raise ValidationError(
449 'if no content, entry must have alternate link')
450
451 if item.get('content') and isinstance(item.get('content'), tuple):
452 content_type = item.get('content')[0].get('type')
453 if item.get('content')[0].get('src'):
454 if item.get('content')[1]:
455 raise ValidationError(
456 'content with src should be empty')
457 if not item.get('summary'):
458 raise ValidationError(
459 'content with src requires a summary too')
460 if content_type in ['text', 'html', 'xhtml']:
461 raise ValidationError(
462 'content with src cannot have type of text, html or xhtml')
463 if content_type:
464 if '/' in content_type and \
465 not content_type.startswith('text/') and \
466 not content_type.endswith('/xml') and not content_type.endswith('+xml') and \
467 not content_type in ['application/xml-external-parsed-entity', 'application/xml-dtd']:
468 # @@@ check content is Base64
469 if not item.get('summary'):
470 raise ValidationError(
471 'content in Base64 requires a summary too')
472 if content_type not in ['text', 'html', 'xhtml'] and '/' not in content_type:
473 raise ValidationError(
474 'content type does not appear to be valid')
475
476 # @@@ no validation is done that 'html' text constructs are valid HTML
477 # @@@ no validation is done that 'xhtml' text constructs are well-formed XML or valid XHTML
478
479 return
480
481 return
482
483
484class LegacySyndicationFeed(AtomFeed):
485 """
486 Provides an SyndicationFeed-compatible interface in its __init__ and
487 add_item but is really a new AtomFeed object.
488 """
489
490 def __init__(self, title, link, description, language=None, author_email=None,
491 author_name=None, author_link=None, subtitle=None, categories=[],
492 feed_url=None, feed_copyright=None):
493
494 atom_id = link
495 title = title
496 updated = None # will be calculated
497 rights = feed_copyright
498 subtitle = subtitle
499 author_dict = {'name': author_name}
500 if author_link:
501 author_dict['uri'] = author_uri
502 if author_email:
503 author_dict['email'] = author_email
504 authors = [author_dict]
505 if categories:
506 categories = [{'term': term} for term in categories]
507 links = [{'rel': 'alternate', 'href': link}]
508 if feed_url:
509 links.append({'rel': 'self', 'href': feed_url})
510 if language:
511 extra_attrs = {'xml:lang': language}
512 else:
513 extra_attrs = {}
514
515 # description ignored (as with Atom1Feed)
516
517 AtomFeed.__init__(self, atom_id, title, updated, rights=rights, subtitle=subtitle,
518 authors=authors, categories=categories, links=links, extra_attrs=extra_attrs)
519
520 def add_item(self, title, link, description, author_email=None,
521 author_name=None, author_link=None, pubdate=None, comments=None,
522 unique_id=None, enclosure=None, categories=[], item_copyright=None):
523
524 if unique_id:
525 atom_id = unique_id
526 else:
527 atom_id = get_tag_uri(link, pubdate)
528 title = title
529 updated = pubdate
530 if item_copyright:
531 rights = item_copyright
532 else:
533 rights = None
534 if description:
535 summary = 'html', description
536 else:
537 summary = None
538 author_dict = {'name': author_name}
539 if author_link:
540 author_dict['uri'] = author_uri
541 if author_email:
542 author_dict['email'] = author_email
543 authors = [author_dict]
544 categories = [{'term': term} for term in categories]
545 links = [{'rel': 'alternate', 'href': link}]
546 if enclosure:
547 links.append({'rel': 'enclosure', 'href': enclosure.url,
548 'length': enclosure.length, 'type': enclosure.mime_type})
549
550 AtomFeed.add_item(self, atom_id, title, updated, rights=rights, summary=summary,
551 authors=authors, categories=categories, links=links)
5520
=== 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 @@
1from notification.models import Notice
2
3
4def notification(request):
5 if request.user.is_authenticated():
6 return {
7 'notice_unseen_count': Notice.objects.unseen_count_for(request.user, on_site=True),
8 }
9 else:
10 return {}
110
=== 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 @@
1from django.utils.translation import ugettext as _
2from django.http import HttpResponse
3from django.contrib.auth import authenticate, login
4from django.conf import settings
5
6
7def simple_basic_auth_callback(request, user, *args, **kwargs):
8 """Simple callback to automatically login the given user after a successful
9 basic authentication."""
10 login(request, user)
11 request.user = user
12
13
14def basic_auth_required(realm=None, test_func=None, callback_func=None):
15 """This decorator should be used with views that need simple authentication
16 against Django's authentication framework.
17
18 The ``realm`` string is shown during the basic auth query.
19
20 It takes a ``test_func`` argument that is used to validate the given
21 credentials and return the decorated function if successful.
22
23 If unsuccessful the decorator will try to authenticate and checks if the
24 user has the ``is_active`` field set to True.
25
26 In case of a successful authentication the ``callback_func`` will be
27 called by passing the ``request`` and the ``user`` object. After that the
28 actual view function will be called.
29
30 If all of the above fails a "Authorization Required" message will be shown.
31
32 """
33 if realm is None:
34 realm = getattr(settings, 'HTTP_AUTHENTICATION_REALM',
35 _('Restricted Access'))
36 if test_func is None:
37 test_func = lambda u: u.is_authenticated()
38
39 def decorator(view_func):
40 def basic_auth(request, *args, **kwargs):
41 # Just return the original view because already logged in
42 if test_func(request.user):
43 return view_func(request, *args, **kwargs)
44
45 # Not logged in, look if login credentials are provided
46 if 'HTTP_AUTHORIZATION' in request.META:
47 auth_method, auth = request.META[
48 'HTTP_AUTHORIZATION'].split(' ', 1)
49 if 'basic' == auth_method.lower():
50 auth = auth.strip().decode('base64')
51 username, password = auth.split(':', 1)
52 user = authenticate(username=username, password=password)
53 if user is not None:
54 if user.is_active:
55 if callback_func is not None and callable(callback_func):
56 callback_func(request, user, *args, **kwargs)
57 return view_func(request, *args, **kwargs)
58
59 response = HttpResponse(
60 _('Authorization Required'), mimetype='text/plain')
61 response.status_code = 401
62 response['WWW-Authenticate'] = 'Basic realm="%s"' % realm
63 return response
64 return basic_auth
65 return decorator
660
=== modified file 'notification/engine.py'
--- notification/engine.py 2016-12-13 18:28:51 +0000
+++ notification/engine.py 2017-05-07 09:51:42 +0000
@@ -49,7 +49,10 @@
49 str(queued_batch.pickled_data).decode('base64'))49 str(queued_batch.pickled_data).decode('base64'))
50 for user, label, extra_context, on_site in notices:50 for user, label, extra_context, on_site in notices:
51 user = User.objects.get(pk=user)51 user = User.objects.get(pk=user)
52 logging.info('emitting notice to %s' % user)52 # FrankU: commented, because not all users get e-mailed
53 # and to supress useless logging
54 # logging.info('emitting notice to %s' % user)
55
53 # call this once per user to be atomic and allow for logging to56 # call this once per user to be atomic and allow for logging to
54 # accurately show how long each takes.57 # accurately show how long each takes.
55 notification.send_now(58 notification.send_now(
5659
=== 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 @@
1from datetime import datetime
2
3from django.core.urlresolvers import reverse
4from django.conf import settings
5from django.contrib.sites.models import Site
6from django.contrib.auth.models import User
7from django.shortcuts import get_object_or_404
8from django.template.defaultfilters import linebreaks, escape, striptags
9from django.utils.translation import ugettext_lazy as _
10
11from notification.models import Notice
12from notification.atomformat import Feed
13
14ITEMS_PER_FEED = getattr(settings, 'ITEMS_PER_FEED', 20)
15
16
17class BaseNoticeFeed(Feed):
18
19 def item_id(self, notification):
20 return 'http://%s%s' % (
21 Site.objects.get_current().domain,
22 notification.get_absolute_url(),
23 )
24
25 def item_title(self, notification):
26 return striptags(notification.message)
27
28 def item_updated(self, notification):
29 return notification.added
30
31 def item_published(self, notification):
32 return notification.added
33
34 def item_content(self, notification):
35 return {'type': 'html', }, linebreaks(escape(notification.message))
36
37 def item_links(self, notification):
38 return [{'href': self.item_id(notification)}]
39
40 def item_authors(self, notification):
41 return [{'name': notification.user.username}]
42
43
44class NoticeUserFeed(BaseNoticeFeed):
45
46 def get_object(self, params):
47 return get_object_or_404(User, username=params[0].lower())
48
49 def feed_id(self, user):
50 return 'http://%s%s' % (
51 Site.objects.get_current().domain,
52 reverse('notification_feed_for_user'),
53 )
54
55 def feed_title(self, user):
56 return _('Notices Feed')
57
58 def feed_updated(self, user):
59 qs = Notice.objects.filter(user=user)
60 # We return an arbitrary date if there are no results, because there
61 # must be a feed_updated field as per the Atom specifications, however
62 # there is no real data to go by, and an arbitrary date can be static.
63 if qs.count() == 0:
64 return datetime(year=2008, month=7, day=1)
65 return qs.latest('added').added
66
67 def feed_links(self, user):
68 complete_url = 'http://%s%s' % (
69 Site.objects.get_current().domain,
70 reverse('notification_notices'),
71 )
72 return ({'href': complete_url},)
73
74 def items(self, user):
75 return Notice.objects.notices_for(user).order_by('-added')[:ITEMS_PER_FEED]
760
=== 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-07 09:51:42 +0000
@@ -10,6 +10,7 @@
10 help = 'Emit queued notices.'10 help = 'Emit queued notices.'
1111
12 def handle_noargs(self, **options):12 def handle_noargs(self, **options):
13 logging.basicConfig(level=logging.DEBUG, format='%(message)s')13 # Franku: Uncomment for debugging purposes
14 # logging.basicConfig(level=logging.DEBUG, format='%(message)s')
14 logging.info('-' * 72)15 logging.info('-' * 72)
15 send_all()16 send_all()
1617
=== 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-07 09:51:42 +0000
@@ -0,0 +1,25 @@
1# -*- coding: utf-8 -*-
2from __future__ import unicode_literals
3
4from django.db import models, migrations
5
6
7class Migration(migrations.Migration):
8
9 dependencies = [
10 ('notification', '0001_initial'),
11 ]
12
13 operations = [
14 migrations.RemoveField(
15 model_name='notice',
16 name='notice_type',
17 ),
18 migrations.RemoveField(
19 model_name='notice',
20 name='user',
21 ),
22 migrations.DeleteModel(
23 name='Notice',
24 ),
25 ]
026
=== modified file 'notification/models.py'
--- notification/models.py 2016-12-13 18:28:51 +0000
+++ notification/models.py 2017-05-07 09:51:42 +0000
@@ -68,7 +68,11 @@
6868
69class NoticeSetting(models.Model):69class NoticeSetting(models.Model):
70 """Indicates, for a given user, whether to send notifications of a given70 """Indicates, for a given user, whether to send notifications of a given
71 type to a given medium."""71 type to a given medium.
72
73 Notice types for each user are added if he/she enters the notification page.
74
75 """
7276
73 user = models.ForeignKey(User, verbose_name=_('user'))77 user = models.ForeignKey(User, verbose_name=_('user'))
74 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))78 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
@@ -82,6 +86,12 @@
8286
8387
84def get_notification_setting(user, notice_type, medium):88def get_notification_setting(user, notice_type, medium):
89 """Return NotceSetting for a specific user. If a NoticeSetting of
90 given NoticeType didn't exist for given user, a NoticeSetting is created.
91
92 If a new NoticeSetting is created, the field 'default' of a NoticeType
93 decides whether NoticeSetting.send is True or False as default.
94 """
85 try:95 try:
86 return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)96 return NoticeSetting.objects.get(user=user, notice_type=notice_type, medium=medium)
87 except NoticeSetting.DoesNotExist:97 except NoticeSetting.DoesNotExist:
@@ -91,81 +101,14 @@
91 setting.save()101 setting.save()
92 return setting102 return setting
93103
94
95def should_send(user, notice_type, medium):104def should_send(user, notice_type, medium):
96 return get_notification_setting(user, notice_type, medium).send105 return get_notification_setting(user, notice_type, medium).send
97106
98107def get_observers_for(notice_type):
99class NoticeManager(models.Manager):108 """ Returns the list of users which wants to get a message (email) for this
100109 type of notice."""
101 def notices_for(self, user, archived=False, unseen=None, on_site=None):110 settings = NoticeSetting.objects.filter(notice_type__label=notice_type).filter(send=True)
102 """returns Notice objects for the given user.111 return [s.user for s in NoticeSetting.objects.filter(notice_type__label=notice_type).filter(send=True)]
103
104 If archived=False, it only include notices not archived.
105 If archived=True, it returns all notices for that user.
106
107 If unseen=None, it includes all notices.
108 If unseen=True, return only unseen notices.
109 If unseen=False, return only seen notices.
110
111 """
112 if archived:
113 qs = self.filter(user=user)
114 else:
115 qs = self.filter(user=user, archived=archived)
116 if unseen is not None:
117 qs = qs.filter(unseen=unseen)
118 if on_site is not None:
119 qs = qs.filter(on_site=on_site)
120 return qs
121
122 def unseen_count_for(self, user, **kwargs):
123 """returns the number of unseen notices for the given user but does not
124 mark them seen."""
125 return self.notices_for(user, unseen=True, **kwargs).count()
126
127
128class Notice(models.Model):
129
130 user = models.ForeignKey(User, verbose_name=_('user'))
131 message = models.TextField(_('message'))
132 notice_type = models.ForeignKey(NoticeType, verbose_name=_('notice type'))
133 added = models.DateTimeField(_('added'), default=datetime.datetime.now)
134 unseen = models.BooleanField(_('unseen'), default=True)
135 archived = models.BooleanField(_('archived'), default=False)
136 on_site = models.BooleanField(_('on site'))
137
138 objects = NoticeManager()
139
140 def __unicode__(self):
141 return self.message
142
143 def archive(self):
144 self.archived = True
145 self.save()
146
147 def is_unseen(self):
148 """returns value of self.unseen but also changes it to false.
149
150 Use this in a template to mark an unseen notice differently the
151 first time it is shown.
152
153 """
154 unseen = self.unseen
155 if unseen:
156 self.unseen = False
157 self.save()
158 return unseen
159
160 class Meta:
161 ordering = ['-added']
162 verbose_name = _('notice')
163 verbose_name_plural = _('notices')
164
165 def get_absolute_url(self):
166 return ('notification_notice', [str(self.pk)])
167 get_absolute_url = models.permalink(get_absolute_url)
168
169112
170class NoticeQueueBatch(models.Model):113class NoticeQueueBatch(models.Model):
171 """A queued notice.114 """A queued notice.
@@ -262,66 +205,68 @@
262 if extra_context is None:205 if extra_context is None:
263 extra_context = {}206 extra_context = {}
264207
265 notice_type = NoticeType.objects.get(label=label)208 # FrankU: This try statement is added to pass notice types
266209 # which are deleted but used by third party apps to create a notice
267 current_site = Site.objects.get_current()210 # e.g. django-messages installed some notice-types which are superfluous
268 notices_url = u"http://%s%s" % (211 # because they just create a notice (which is not used anymore), but not
269 unicode(current_site),212 # used for sending email, like: 'message deleted' or 'message recovered'
270 reverse('notification_notices'),213 try:
271 )214 notice_type = NoticeType.objects.get(label=label)
272215
273 current_language = get_language()216 current_site = Site.objects.get_current()
274217 notices_url = u"http://%s%s" % (
275 formats = (218 unicode(current_site),
276 'short.txt',219 reverse('notification_notices'),
277 'full.txt',220 )
278 'notice.html',221
279 'full.html',222 current_language = get_language()
280 ) # TODO make formats configurable223
281224 formats = (
282 for user in users:225 'short.txt',
283 recipients = []226 'full.txt',
284 # get user language for user from language store defined in227 ) # TODO make formats configurable
285 # NOTIFICATION_LANGUAGE_MODULE setting228
286 try:229 for user in users:
287 language = get_notification_language(user)230 recipients = []
288 except LanguageStoreNotAvailable:231 # get user language for user from language store defined in
289 language = None232 # NOTIFICATION_LANGUAGE_MODULE setting
290233 try:
291 if language is not None:234 language = get_notification_language(user)
292 # activate the user's language235 except LanguageStoreNotAvailable:
293 activate(language)236 language = None
294237
295 # update context with user specific translations238 if language is not None:
296 context = Context({239 # activate the user's language
297 'user': user,240 activate(language)
298 'notice': ugettext(notice_type.display),241
299 'notices_url': notices_url,242 # update context with user specific translations
300 'current_site': current_site,243 context = Context({
301 })244 'user': user,
302 context.update(extra_context)245 'notices_url': notices_url,
303246 'current_site': current_site,
304 # get prerendered format messages247 })
305 messages = get_formatted_messages(formats, label, context)248 context.update(extra_context)
306249
307 # Strip newlines from subject250 # get prerendered format messages
308 subject = ''.join(render_to_string('notification/email_subject.txt', {251 messages = get_formatted_messages(formats, label, context)
309 'message': messages['short.txt'],252
310 }, context).splitlines())253 # Strip newlines from subject
311254 subject = ''.join(render_to_string('notification/email_subject.txt', {
312 body = render_to_string('notification/email_body.txt', {255 'message': messages['short.txt'],
313 'message': messages['full.txt'],256 }, context).splitlines())
314 }, context)257
315258 body = render_to_string('notification/email_body.txt', {
316 notice = Notice.objects.create(user=user, message=messages['notice.html'],259 'message': messages['full.txt'],
317 notice_type=notice_type, on_site=on_site)260 }, context)
318 if should_send(user, notice_type, '1') and user.email: # Email261
319 recipients.append(user.email)262 if should_send(user, notice_type, '1') and user.email: # Email
320 send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)263 recipients.append(user.email)
321264 send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, recipients)
322 # reset environment to original language265
323 activate(current_language)266 # reset environment to original language
324267 activate(current_language)
268 except NoticeType.DoesNotExist:
269 pass
325270
326def send(*args, **kwargs):271def send(*args, **kwargs):
327 """A basic interface around both queue and send_now.272 """A basic interface around both queue and send_now.
@@ -409,6 +354,15 @@
409 def send_notice(self):354 def send_notice(self):
410 send([self.user], self.notice_type.label,355 send([self.user], self.notice_type.label,
411 {'observed': self.observed_object})356 {'observed': self.observed_object})
357
358 def get_content_object(self):
359 """
360 taken from threadedcomments:
361
362 Wrapper around the GenericForeignKey due to compatibility reasons
363 and due to ``list_display`` limitations.
364 """
365 return self.observed_object
412366
413367
414def observe(observed, observer, notice_type_label, signal='post_save'):368def observe(observed, observer, notice_type_label, signal='post_save'):
415369
=== modified file 'notification/urls.py'
--- notification/urls.py 2016-12-13 18:28:51 +0000
+++ notification/urls.py 2017-05-07 09:51:42 +0000
@@ -1,10 +1,7 @@
1from django.conf.urls import url1from django.conf.urls import url
22
3from notification.views import notices, mark_all_seen, feed_for_user, single3from notification.views import notice_settings
44
5urlpatterns = [5urlpatterns = [
6 url(r'^$', notices, name='notification_notices'),6 url(r'^$', notice_settings, name='notification_notices'),
7 url(r'^(\d+)/$', single, name='notification_notice'),
8 #url(r'^feed/$', feed_for_user, name="notification_feed_for_user"),
9 url(r'^mark_all_seen/$', mark_all_seen, name='notification_mark_all_seen'),
10]7]
118
=== modified file 'notification/views.py'
--- notification/views.py 2016-12-13 18:28:51 +0000
+++ notification/views.py 2017-05-07 09:51:42 +0000
@@ -1,30 +1,19 @@
1from django.core.urlresolvers import reverse1from django.shortcuts import render_to_response
2from django.shortcuts import render_to_response, get_object_or_404
3from django.http import HttpResponseRedirect, Http404
4from django.template import RequestContext2from django.template import RequestContext
5from django.contrib.auth.decorators import login_required3from django.contrib.auth.decorators import login_required
6from django.contrib.syndication.views import Feed4from collections import OrderedDict
7
8from notification.models import *5from notification.models import *
9from notification.decorators import basic_auth_required, simple_basic_auth_callback
10from notification.feeds import NoticeUserFeed
11
12
13@basic_auth_required(realm='Notices Feed', callback_func=simple_basic_auth_callback)
14def feed_for_user(request):
15 url = 'feed/%s' % request.user.username
16 return Feed(request, url, {
17 'feed': NoticeUserFeed,
18 })
196
207
21@login_required8@login_required
22def notices(request):9def notice_settings(request):
23 notice_types = NoticeType.objects.all()10 app_tables = {}
24 notices = Notice.objects.notices_for(request.user, on_site=True)11 for notice_type in NoticeType.objects.all().order_by('label'):
25 settings_table = []12 # Assuming each notice_type.label begins with the name of the app
26 for notice_type in NoticeType.objects.all():13 # followed by an underscore:
27 settings_row = []14 app = notice_type.label.partition('_')[0]
15 app_tables.setdefault(app, [])
16 checkbox_values = []
28 for medium_id, medium_display in NOTICE_MEDIA:17 for medium_id, medium_display in NOTICE_MEDIA:
29 form_label = '%s_%s' % (notice_type.label, medium_id)18 form_label = '%s_%s' % (notice_type.label, medium_id)
30 setting = get_notification_setting(19 setting = get_notification_setting(
@@ -35,65 +24,11 @@
35 else:24 else:
36 setting.send = False25 setting.send = False
37 setting.save()26 setting.save()
38 settings_row.append((form_label, setting.send))27 checkbox_values.append((form_label, setting.send))
39 settings_table.append(28
40 {'notice_type': notice_type, 'cells': settings_row})29 app_tables[app].append({'notice_type': notice_type, 'html_values': checkbox_values})
4130
42 notice_settings = {31 return render_to_response('notification/notice_settings.html', {
43 'column_headers': [medium_display for medium_id, medium_display in NOTICE_MEDIA],32 'column_headers': [medium_display for medium_id, medium_display in NOTICE_MEDIA],
44 'rows': settings_table,33 'app_tables': OrderedDict(sorted(app_tables.items(), key=lambda t: t[0]))
45 }
46
47 return render_to_response('notification/notices.html', {
48 'notices': notices,
49 'notice_types': notice_types,
50 'notice_settings': notice_settings,
51 }, context_instance=RequestContext(request))34 }, context_instance=RequestContext(request))
52
53
54@login_required
55def single(request, id):
56 notice = get_object_or_404(Notice, id=id)
57 if request.user == notice.user:
58 return render_to_response('notification/single.html', {
59 'notice': notice,
60 }, context_instance=RequestContext(request))
61 raise Http404
62
63
64@login_required
65def archive(request, noticeid=None, next_page=None):
66 if noticeid:
67 try:
68 notice = Notice.objects.get(id=noticeid)
69 if request.user == notice.user or request.user.is_superuser:
70 notice.archive()
71 else: # you can archive other users' notices
72 # only if you are superuser.
73 return HttpResponseRedirect(next_page)
74 except Notice.DoesNotExist:
75 return HttpResponseRedirect(next_page)
76 return HttpResponseRedirect(next_page)
77
78
79@login_required
80def delete(request, noticeid=None, next_page=None):
81 if noticeid:
82 try:
83 notice = Notice.objects.get(id=noticeid)
84 if request.user == notice.user or request.user.is_superuser:
85 notice.delete()
86 else: # you can delete other users' notices
87 # only if you are superuser.
88 return HttpResponseRedirect(next_page)
89 except Notice.DoesNotExist:
90 return HttpResponseRedirect(next_page)
91 return HttpResponseRedirect(next_page)
92
93
94@login_required
95def mark_all_seen(request):
96 for notice in Notice.objects.notices_for(request.user, unseen=True):
97 notice.unseen = False
98 notice.save()
99 return HttpResponseRedirect(reverse('notification_notices'))
10035
=== modified file 'pybb/admin.py'
--- pybb/admin.py 2016-12-15 10:43:41 +0000
+++ pybb/admin.py 2017-05-07 09:51:42 +0000
@@ -48,7 +48,9 @@
48 }48 }
49 ),49 ),
50 )50 )
5151
52class SubscribersInline(admin.TabularInline):
53 model = Topic.subscribers.through
5254
53class TopicAdmin(admin.ModelAdmin):55class TopicAdmin(admin.ModelAdmin):
54 list_display = ['name', 'forum', 'created', 'head', 'is_hidden']56 list_display = ['name', 'forum', 'created', 'head', 'is_hidden']
@@ -59,14 +61,13 @@
59 fieldsets = (61 fieldsets = (
60 (None, {62 (None, {
61 'fields': ('forum', 'name', 'user', ('created', 'updated'))63 'fields': ('forum', 'name', 'user', ('created', 'updated'))
62 }64 }),
63 ),
64 (_('Additional options'), {65 (_('Additional options'), {
65 'classes': ('collapse',),66 'classes': ('collapse',),
66 'fields': (('views',), ('sticky', 'closed'), 'subscribers')67 'fields': (('views',), ('sticky', 'closed'),)
67 }68 }),
68 ),
69 )69 )
70 inlines = [ SubscribersInline, ]
7071
7172
72class PostAdmin(admin.ModelAdmin):73class PostAdmin(admin.ModelAdmin):
7374
=== modified file 'pybb/forms.py'
--- pybb/forms.py 2017-01-23 13:01:31 +0000
+++ pybb/forms.py 2017-05-07 09:51:42 +0000
@@ -10,9 +10,7 @@
10from pybb.models import Topic, Post, PrivateMessage, Attachment10from pybb.models import Topic, Post, PrivateMessage, Attachment
11from pybb import settings as pybb_settings11from pybb import settings as pybb_settings
12from django.conf import settings12from django.conf import settings
13from notification.models import send13from notification.models import send, get_observers_for
14from django.core.mail import send_mail
15from django.contrib.sites.models import Site
1614
1715
18class AddPostForm(forms.ModelForm):16class AddPostForm(forms.ModelForm):
@@ -94,11 +92,11 @@
9492
95 if not hidden:93 if not hidden:
96 if topic_is_new:94 if topic_is_new:
97 send(User.objects.all(), 'forum_new_topic',95 send(get_observers_for('forum_new_topic'), 'forum_new_topic',
98 {'topic': topic, 'post': post, 'user': topic.user})96 {'topic': topic, 'post': post, 'user': topic.user}, queue = True)
99 else:97 else:
100 send(self.topic.subscribers.all(), 'forum_new_post',98 send(self.topic.subscribers.all(), 'forum_new_post',
101 {'post': post, 'topic': topic, 'user': post.user})99 {'post': post, 'topic': topic, 'user': post.user}, queue = True)
102100
103 return post101 return post
104102
105103
=== 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-07 09:51:42 +0000
@@ -14,6 +14,8 @@
14 _('Forum New Post'),14 _('Forum New Post'),
15 _('a new comment has been posted to a topic you observe'))15 _('a new comment has been posted to a topic you observe'))
1616
17 # TODO (Franku): post_syncdb is deprecated since Django 1.7
18 # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
17 signals.post_syncdb.connect(create_notice_types,19 signals.post_syncdb.connect(create_notice_types,
18 sender=notification)20 sender=notification)
19except ImportError:21except ImportError:
2022
=== 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-07 09:51:42 +0000
@@ -1,5 +1,5 @@
1{% load i18n %}{% blocktrans %}You have received the following notice from {{ current_site }}:1{% load i18n %}{% blocktrans %}You have received the following notice from {{ current_site }}:
22
3{{ message }}3{{ message }}
4To see other notices or change how you receive notifications, please go to {{ notices_url }}.
5{% endblocktrans %}
6\ No newline at end of file4\ No newline at end of file
5To change how you receive notifications, please go to {{ notices_url }}.
6{% endblocktrans %}
77
=== 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 @@
1{% load i18n %}{% url 'profile_view' user.username as user_url %}
2{% blocktrans with topic.get_absolute_url as topic_url and post.get_absolute_url as post_url%}
3<a href="{{ user_url }}">{{ user }}</a>
4has <a href="{{ post_url }}">replied</a>
5to the forum topic <a href="{{ topic_url }}">{{ topic }}</a>.{% endblocktrans %}
60
=== 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 @@
1{% load i18n %}{% url 'profile_view' user.username as user_url %}
2{% blocktrans with topic.get_absolute_url as topic_url %}
3A new forum topic has be created under <a href="{{ topic_url }}">{{ topic }}</a>
4by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
50
=== 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 @@
1{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
2\ No newline at end of file0\ No newline at end of file
31
=== added directory 'templates/notification/maps_new_map'
=== added file 'templates/notification/maps_new_map/full.txt'
--- templates/notification/maps_new_map/full.txt 1970-01-01 00:00:00 +0000
+++ templates/notification/maps_new_map/full.txt 2017-05-07 09:51:42 +0000
@@ -0,0 +1,7 @@
1{% load i18n %}A new map was uploaded to the Website by {{ user }}:
2{% blocktrans %}
3Mapname: {{ mapname }}
4Description: {{ uploader_comment }}
5–––––––––––
6Link to map: http://{{ current_site }}{{ url }}
7{% endblocktrans %}
0\ No newline at end of file8\ No newline at end of file
19
=== 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 @@
1{% load i18n %}{% blocktrans %}You have deleted the message '{{ message }}'.{% endblocktrans %}
20
=== 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 @@
1{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have deleted the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}
20
=== 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 @@
1{% load i18n %}
2{% load wlprofile_extras %}
3{% 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 %}
40
=== 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 @@
1{% load i18n %}{% blocktrans %}You have recovered the message '{{ message }}'.{% endblocktrans %}
20
=== 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 @@
1{% load i18n %}{% blocktrans with message.get_absolute_url as message_url %}You have recovered the message <a href="{{ message_url }}">{{ message }}</a>.{% endblocktrans %}
20
=== 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 @@
1{% 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 %}
20
=== 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 @@
1{% load i18n %}
2{% load wlprofile_extras %}
3{% 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 %}
40
=== 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 @@
1{% load i18n %}
2{% load wlprofile_extras %}
3{% 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 %}
40
=== 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 @@
1{% load i18n %}{% blocktrans with message.recipient as message_recipient %}You have sent the message '{{ message }}' to {{ message_recipient }}.{% endblocktrans %}
20
=== 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 @@
1{% load i18n %}
2{% load wlprofile_extras %}
3{% 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 %}
40
=== 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 @@
1{% load i18n %}{% blocktrans %}{{ notice }}{% endblocktrans %}
2\ No newline at end of file0\ No newline at end of file
31
=== 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-07 09:51:42 +0000
@@ -9,39 +9,7 @@
9{% endblock %}9{% endblock %}
1010
11{% block content %}11{% block content %}
12<h1>{% trans "Notifications" %}</h1>12<h1>{% trans "Notification Settings" %}</h1>
13{% comment "Testing if this is used by users" %}
14<div class="blogEntry">
15{% autopaginate notices %}
16
17{% if notices %}
18 <a href="{% url 'notification_mark_all_seen' %}" class="posRight small">{% trans "Mark all as seen" %}</a>
19 {% paginate %}
20
21 {# TODO: get timezone support working with regroup #}
22 {% regroup notices by added.date as notices_by_date %}
23 {% for date in notices_by_date %}
24 <br />
25 <h3>{{ date.grouper|naturalday:_("MONTH_DAY_FORMAT")|title }}</h3>
26 <table class="notifications">
27 {% for notice in date.list %}
28 <tr class="{% cycle "odd" "even" %} {% if notice.is_unseen %}italic{% endif %}">
29 <td class="type"><a href="{% url 'notification_notice' notice.pk %} ">{% trans notice.notice_type.display %}</a></td>
30 <td class="text">{{ notice.message|safe }}</td>
31 <td class="date">{{ notice.added|custom_date:user }}</td>
32 </tr>
33 {% endfor %}
34 </table>
35 {% endfor %}
36 <br />
37 {% paginate %}
38{% else %}
39 <p>
40 {% trans "No notifications." %}
41 </p>
42{% endif %}
43</div>
44{% endcomment %}
4513
46<div class="blogEntry">14<div class="blogEntry">
47 <h2>{% trans "Settings" %}</h2>15 <h2>{% trans "Settings" %}</h2>
@@ -57,32 +25,34 @@
57 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>.25 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>.
58 </p>26 </p>
59 {% endif %}27 {% endif %}
6028 <form method="POST" action=".">
61 <form method="POST" action="."> {# doubt this easy to do in uni-form #}29 {% for app, settings in app_tables.items %}
30 <h3>{{ app|capfirst }} </h3>
62 <table class="notifications">31 <table class="notifications">
63 <tr>32 <tr>
64 <th class="small">{% trans "Notification Type" %}</th>33 <th class="small">{% trans "Notification Type" %}</th>
65 {% for header in notice_settings.column_headers %}34 {% for header in column_headers %}
66 <th class="small">{{ header }}</th>35 <th class="small">{{ header }}</th>
67 {% endfor %}36 {% endfor %}
68 </tr>37 </tr>
69 {% for row in notice_settings.rows %}38 {% for setting in settings %}
70 <tr class="{% cycle "odd" "even" %}">39 <tr class="{% cycle "odd" "even" %}">
71 <td>40 <td>
72 {% trans row.notice_type.display %}<br />41 {% trans setting.notice_type.display %}<br />
73 {% trans row.notice_type.description %}42 {% trans setting.notice_type.description %}
74 </td>43 </td>
75 {% for cell in row.cells %}44 <td style="width: 5em;">
76 <td>45 {% for html_value in setting.html_values %}
77 <input type="checkbox" name="{{ cell.0 }}" {% if cell.1 %}checked="checked" {% endif %}/>46 <input type="checkbox" name="{{ html_value.0 }}" {% if html_value.1 %}checked="checked" {% endif %}/>
78 </td>47 {% endfor %}
48 </td>
49 </tr>
79 {% endfor %}50 {% endfor %}
80 </tr>
81 {% endfor %}
82 </table>51 </table>
83 <br />52 <br />
84 {% csrf_token %}53 {% endfor %}
85 <input type="submit" value="{% trans "Change" %}" />54 {% csrf_token %}
55 <input class="posRight" type="submit" value="{% trans "Change" %}" />
86 </form>56 </form>
87</div>57</div>
88{% endblock %}58{% endblock %}
8959
=== 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 @@
1{% extends "notification/base.html" %}
2
3{% load humanize i18n %}
4{% load custom_date %}
5
6{% block title %}
7{% trans "Notification "%} - {{ block.super }}
8{% endblock %}
9
10{% block content %}
11<h1>{% trans "Notification" %}: {{ notice.notice_type.display }}</h1>
12<div class="blogEntry">
13 <table>
14 <tr>
15 <td class="grey">Notice:</td>
16 <td>{{ notice.message|safe }}</td>
17 </tr>
18 <tr>
19 <td class="grey">Notice Type:</td>
20 <td>{{ notice.notice_type.display }}</td>
21 </tr>
22 <tr>
23 <td class="grey">Date of Notice:</td>
24 <td>{{ notice.added }}</td>
25 </tr>
26 <tr>
27 <td class="grey">Notice Unseen:</td>
28 <td>{{ notice.unseen }}</td>
29 </tr>
30 <tr>
31 <td class="grey">Notice Archiv:</td>
32 <td>{{ notice.archived }}</td>
33 </tr>
34 <tr>
35 <td class="grey">Notice Site:</td>
36 <td>{{ notice.on_site }}</td>
37 </tr>
38 </table>
39</div>
40{% endblock %}
410
=== 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 @@
1{% load i18n %}{% url 'profile_view' user.username as user_url %}
2{% blocktrans with article.get_absolute_url as article_url %}
3The wiki article <a href="{{ article_url }}">{{ article }}</a>
4has been edited by <a href="{{ user_url }}">{{ user }}</a>.{% endblocktrans %}
50
=== 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 @@
1{% 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 %}
20
=== 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 @@
1{% load i18n %}
2{% 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 %}
30
=== modified file 'wiki/forms.py'
--- wiki/forms.py 2017-02-24 19:15:17 +0000
+++ wiki/forms.py 2017-05-07 09:51:42 +0000
@@ -8,6 +8,11 @@
8from wiki.models import Article8from wiki.models import Article
9from wiki.models import ChangeSet9from wiki.models import ChangeSet
10from settings import WIKI_WORD_RE10from settings import WIKI_WORD_RE
11try:
12 from notification import models as notification
13except:
14 notification = None
15
1116
12wikiword_pattern = re.compile('^' + WIKI_WORD_RE + '$')17wikiword_pattern = re.compile('^' + WIKI_WORD_RE + '$')
1318
@@ -102,6 +107,8 @@
102 article.creator = editor107 article.creator = editor
103 article.group = group108 article.group = group
104 article.save(*args, **kwargs)109 article.save(*args, **kwargs)
110 if notification:
111 notification.observe(article, editor, 'wiki_observed_article_changed')
105112
106 # 4 - Create new revision113 # 4 - Create new revision
107 changeset = article.new_revision(114 changeset = article.new_revision(
108115
=== modified file 'wiki/management.py'
--- wiki/management.py 2016-12-13 18:28:51 +0000
+++ wiki/management.py 2017-05-07 09:51:42 +0000
@@ -6,9 +6,6 @@
6 from notification import models as notification6 from notification import models as notification
77
8 def create_notice_types(app, created_models, verbosity, **kwargs):8 def create_notice_types(app, created_models, verbosity, **kwargs):
9 notification.create_notice_type('wiki_article_edited',
10 _('Article Edited'),
11 _('your article has been edited'))
12 notification.create_notice_type('wiki_revision_reverted',9 notification.create_notice_type('wiki_revision_reverted',
13 _('Article Revision Reverted'),10 _('Article Revision Reverted'),
14 _('your revision has been reverted'))11 _('your revision has been reverted'))
@@ -16,6 +13,8 @@
16 _('Observed Article Changed'),13 _('Observed Article Changed'),
17 _('an article you observe has changed'))14 _('an article you observe has changed'))
1815
16 # TODO (Franku): post_syncdb is deprecated since Django 1.7
17 # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
19 signals.post_syncdb.connect(create_notice_types,18 signals.post_syncdb.connect(create_notice_types,
20 sender=notification)19 sender=notification)
21except ImportError:20except ImportError:
2221
=== added file 'wlmaps/management.py'
--- wlmaps/management.py 1970-01-01 00:00:00 +0000
+++ wlmaps/management.py 2017-05-07 09:51:42 +0000
@@ -0,0 +1,18 @@
1from django.db.models import signals
2
3from django.utils.translation import ugettext_noop as _
4
5try:
6 from notification import models as notification
7
8 def create_notice_types(app, created_models, verbosity, **kwargs):
9 notification.create_notice_type('maps_new_map',
10 _('A new Map is available'),
11 _('a new map is available for download'),1)
12
13 # TODO (Franku): post_syncdb is deprecated since Django 1.7
14 # See: https://docs.djangoproject.com/en/1.8/ref/signals/#post-syncdb
15 signals.post_syncdb.connect(create_notice_types,
16 sender=notification)
17except ImportError:
18 print 'Skipping creation of NoticeTypes as notification app not found'
019
=== modified file 'wlmaps/models.py'
--- wlmaps/models.py 2016-08-11 18:13:59 +0000
+++ wlmaps/models.py 2017-05-07 09:51:42 +0000
@@ -5,6 +5,10 @@
5from django.contrib.auth.models import User5from django.contrib.auth.models import User
6from django.template.defaultfilters import slugify6from django.template.defaultfilters import slugify
7import datetime7import datetime
8try:
9 from notification import models as notification
10except ImportError:
11 notification = None
812
9import settings13import settings
10if settings.USE_SPHINX:14if settings.USE_SPHINX:
@@ -64,4 +68,8 @@
64 self.slug = slugify(self.name)68 self.slug = slugify(self.name)
6569
66 map = super(Map, self).save(*args, **kwargs)70 map = super(Map, self).save(*args, **kwargs)
67 return map71 if notification:
72 notification.send(notification.get_observers_for('wlmaps_new_map'), 'wlmaps_new_map',
73 {'mapname': self.name, 'url': self.get_absolute_url(), 'user': self.uploader, 'uploader_comment': self.uploader_comment}, queue=True)
74
75 return map

Subscribers

People subscribed via source and target branches