diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index 0a244f1..e8b16d3 100644 Binary files a/locales/de/LC_MESSAGES/loops.mo and b/locales/de/LC_MESSAGES/loops.mo differ diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index afe516f..7d440da 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: 0.13.0\n" "POT-Creation-Date: 2007-05-22 12:00 CET\n" -"PO-Revision-Date: 2013-07-15 12:00 CET\n" +"PO-Revision-Date: 2015-10-30 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -483,6 +483,30 @@ msgstr "Lesezeichen für aktuelles Objekt hinzufügen" msgid "Remove from favorites" msgstr "Lesezeichen entfernen" +msgid "Notifications" +msgstr "Nachrichten" + +msgid "No new notifications" +msgstr "Keine neuen Nachrichten" + +msgid "${numNews} new notification" +msgstr "${numNews} neue Nachricht" + +msgid "${numNews} new notifications" +msgstr "${numNews} neue Nachrichten" + +msgid "Show all notifications" +msgstr "Alle Nachrichten anzeigen" + +msgid "Sender" +msgstr "Absender" + +msgid "Object" +msgstr "Objekt" + +msgid "Date/Time Read" +msgstr "Gelesen" + msgid "Personal Informations" msgstr "Persönliche Informationen" diff --git a/organize/personal/README.txt b/organize/personal/README.txt index ca8ff4d..59c0ff6 100644 --- a/organize/personal/README.txt +++ b/organize/personal/README.txt @@ -146,9 +146,14 @@ When the notification is marked as read the read timestamp will be set. It's possible to store more than one notification concerning the same object. >>> notifications.add(d001, person, 'I send myself another letter.') - >>> len(list(notifications.listTracks())) + >>> len(list(notifications.listTracks(unreadOnly=False))) 2 +Only unread notifications are listed by default. + + >>> len(list(notifications.listTracks())) + 1 + User interface -------------- diff --git a/organize/personal/browser/configurator.py b/organize/personal/browser/configurator.py index 8ce2f9e..e93e44e 100644 --- a/organize/personal/browser/configurator.py +++ b/organize/personal/browser/configurator.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2010 Helmut Merz helmutm@cy55.de +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ A view configurator provides configuration data for a view controller. - -$Id$ """ from zope import component @@ -30,7 +28,9 @@ from zope.traversing.browser.absoluteurl import absoluteURL from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty from cybertools.meta.interfaces import IOptions +from loops.browser.node import NodeView from loops.organize.party import getPersonForUser +from loops.organize.personal.notification import Notifications from loops.util import _ @@ -44,10 +44,11 @@ class PortletConfigurator(ViewConfigurator): def __init__(self, context, request): self.context = context self.request = request + self.view = NodeView(self.context, self.request) @property def viewProperties(self): - return self.favorites + self.filters + return self.favorites + self.filters + self.notifications @Lazy def records(self): @@ -97,3 +98,27 @@ class PortletConfigurator(ViewConfigurator): #url=absoluteURL(self.context, self.request) + '/@@filters.html', )) return [filters] + + @property + def notifications(self): + if self.person is None: + return [] + notif = self.view.globalOptions.organize.showNotifications + if not notif: + return [] + if not list(Notifications(self.person).listTracks(unreadOnly=False)): + return [] + if isinstance(notif, list): + notifPage = notif[0] + else: + notifPage = 'notifications' + portlet = MacroViewProperty(self.context, self.request) + portlet.setParams(dict( + slot='portlet_left', + identifier='loops.organize.notifications', + title=_(u'Notifications'), + subMacro=personal_macros.macros['notifications_portlet'], + priority=10, + url='%s/%s' % (self.view.menu.url, notifPage), + )) + return [portlet] diff --git a/organize/personal/browser/configure.zcml b/organize/personal/browser/configure.zcml index 5d7ff50..7eadee8 100644 --- a/organize/personal/browser/configure.zcml +++ b/organize/personal/browser/configure.zcml @@ -35,4 +35,17 @@ class="loops.organize.personal.browser.notification.NotificationsListing" permission="zope.View" /> + + + + diff --git a/organize/personal/browser/notification.py b/organize/personal/browser/notification.py index 90697f3..2258125 100644 --- a/organize/personal/browser/notification.py +++ b/organize/personal/browser/notification.py @@ -24,8 +24,10 @@ from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy -from cybertools.util.date import formatTimeStamp +from cybertools.browser.form import FormController +from cybertools.util.date import formatTimeStamp, getTimeStamp from loops.browser.concept import ConceptView +from loops.browser.node import NodeView from loops.common import adapted, baseObject from loops.organize.personal.notification import Notifications from loops.organize.party import getPersonForUser @@ -50,19 +52,28 @@ class NotificationsListing(ConceptView): return Notifications(self.person) def getNotifications(self, unreadOnly=True): - tracks = self.notifications.listTracks() + if self.person is None: + return [] + tracks = self.notifications.listTracks(unreadOnly) return tracks - def getNotificationsFormatted(self, unreadOnly=True): + def getNotificationsFormatted(self): + unreadOnly = not self.request.form.get('show_all') result = [] for track in self.getNotifications(unreadOnly): data = track.data s = util.getObjectForUid(data.get('sender')) - sender = dict(label=s.title, - url=self.nodeView.getUrlForTarget(baseObject(s))) + if s is None: + sender = dict(label=u'???', url=u'') + else: + sender = dict(label=s.title, + url=self.nodeView.getUrlForTarget(baseObject(s))) obj = util.getObjectForUid(track.taskId) - object = dict(label=obj.title, - url=self.nodeView.getUrlForTarget(baseObject(obj))) + ov = self.nodeView.getViewForTarget(obj) + url = '%s?form.action=notification_read&track=%s' % ( + self.nodeView.getUrlForTarget(obj), + util.getUidForObject(track)) + object = dict(label=ov.title, url=url) read_ts = self.formatTimeStamp(data.get('read_ts')) item = dict(timeStamp=self.formatTimeStamp(track.timeStamp), sender=sender, @@ -71,3 +82,22 @@ class NotificationsListing(ConceptView): read_ts=read_ts) result.append(item) return result + + +class NotificationsView(NodeView, NotificationsListing): + + pass + + +class ReadNotification(FormController): + + def update(self): + form = self.request.form + trackId = form.get('track') + track = util.getObjectForUid(trackId) + data = track.data + alreadyRead = data.get('read_ts') + if not alreadyRead: + data['read_ts'] = getTimeStamp() + track.data = data + return True diff --git a/organize/personal/browser/personal_macros.pt b/organize/personal/browser/personal_macros.pt index c5988f8..91792f6 100644 --- a/organize/personal/browser/personal_macros.pt +++ b/organize/personal/browser/personal_macros.pt @@ -47,28 +47,57 @@ + + + + + + +
- - - - - - - - - - - - -
Date/TimeSenderObjectTextDate/Time read
- - - - -
+
+ + +
  + + + + + + + + + + + + +
Date/TimeSenderObjectMessageDate/Time Read
+ + + + +
+
diff --git a/organize/personal/notification.py b/organize/personal/notification.py index 5f65f60..76617f2 100644 --- a/organize/personal/notification.py +++ b/organize/personal/notification.py @@ -33,9 +33,12 @@ class Notifications(Favorites): self.context = (baseObject(person). getLoopsRoot().getRecordManager()['favorites']) - def listTracks(self): - return super(Notifications, self).listTracks( + def listTracks(self, unreadOnly=True): + tracks = super(Notifications, self).listTracks( baseObject(self.person), type='notification') + if unreadOnly: + tracks = [t for t in tracks if not t.data.get('read_ts')] + return tracks def add(self, obj, sender, text): senderUid = util.getUidForObject(baseObject(sender)) diff --git a/organize/stateful/README.txt b/organize/stateful/README.txt index 11182e4..d9906f1 100644 --- a/organize/stateful/README.txt +++ b/organize/stateful/README.txt @@ -181,6 +181,12 @@ Querying objects by state [<...>] +Person States +============= + + >>> from loops.organize.stateful.person import personStates + + Task States =========== diff --git a/organize/stateful/browser.py b/organize/stateful/browser.py index 2d05748..03363fe 100644 --- a/organize/stateful/browser.py +++ b/organize/stateful/browser.py @@ -44,6 +44,7 @@ template = ViewPageTemplateFile('view_macros.pt') statefulActions = ('classification_quality', 'simple_publishing', + 'person_states', 'task_states', 'publishable_task',) diff --git a/organize/stateful/configure.zcml b/organize/stateful/configure.zcml index 30ad487..89e0c73 100644 --- a/organize/stateful/configure.zcml +++ b/organize/stateful/configure.zcml @@ -35,6 +35,20 @@ set_schema="cybertools.stateful.interfaces.IStateful" /> + + + + + + + + diff --git a/organize/stateful/person.py b/organize/stateful/person.py new file mode 100644 index 0000000..06ee292 --- /dev/null +++ b/organize/stateful/person.py @@ -0,0 +1,63 @@ +# +# Copyright (c) 2015 Helmut Merz helmutm@cy55.de +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# + +""" +Implementations for stateful persons. +""" + +from zope.app.security.settings import Allow, Deny, Unset +from zope import component +from zope.component import adapter +from zope.interface import implementer +from zope.traversing.api import getName + +from cybertools.composer.schema.schema import Schema +from cybertools.stateful.definition import StatesDefinition +from cybertools.stateful.definition import State, Transition +from cybertools.stateful.interfaces import IStatesDefinition, IStateful +from loops.common import adapted +from loops.organize.stateful.base import commentsField +from loops.organize.stateful.base import StatefulLoopsObject +from loops.security.interfaces import ISecuritySetter +from loops.util import _ + + +defaultSchema = Schema(commentsField, + name='change_state') + + +@implementer(IStatesDefinition) +def personStates(): + return StatesDefinition('person_states', + State('prospective', 'prospective', ('activate', 'inactivate',), + color='blue'), + State('active', 'active', ('reset', 'inactivate',), + color='green'), + State('inactive', 'inactive', ('reactivate',), + color='x'), + Transition('activate', 'activate', 'active', schema=defaultSchema), + Transition('reset', 'reset', 'prospective', schema=defaultSchema), + Transition('inactivate', 'inactivate', 'inactive', schema=defaultSchema), + Transition('reactivate', 'reactivate', 'active', schema=defaultSchema), + initialState='active') + + +class StatefulPerson(StatefulLoopsObject): + + statesDefinition = 'person_states' +