Merge branch 'bbmaster' of ssh://git.cy55.de/home/git/loops into bbmaster
This commit is contained in:
commit
9ec755ecd3
12 changed files with 248 additions and 35 deletions
Binary file not shown.
|
@ -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 <helmutm@cy55.de>\n"
|
||||
"Language-Team: loops developers <helmutm@cy55.de>\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"
|
||||
|
||||
|
|
|
@ -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
|
||||
--------------
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -35,4 +35,17 @@
|
|||
class="loops.organize.personal.browser.notification.NotificationsListing"
|
||||
permission="zope.View" />
|
||||
|
||||
<browser:page
|
||||
for="loops.interfaces.INode"
|
||||
name="notifications_view"
|
||||
class="loops.organize.personal.browser.notification.NotificationsView"
|
||||
permission="zope.View" />
|
||||
|
||||
<zope:adapter
|
||||
name="notification_read"
|
||||
for="loops.browser.node.NodeView
|
||||
zope.publisher.interfaces.browser.IBrowserRequest"
|
||||
factory="loops.organize.personal.browser.notification.ReadNotification"
|
||||
permission="zope.View" />
|
||||
|
||||
</configure>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -47,28 +47,57 @@
|
|||
</metal:actions>
|
||||
|
||||
|
||||
<metal:actions define-macro="notifications_portlet"
|
||||
tal:define="view nocall:context/@@notifications_view;
|
||||
numNews python:len(view.getNotifications())">
|
||||
<div tal:condition="not:numNews">
|
||||
<a i18n:translate=""
|
||||
tal:attributes="href macro/url">
|
||||
No new notifications</a></div>
|
||||
<div tal:condition="python:numNews == 1">
|
||||
<a i18n:translate=""
|
||||
tal:attributes="href macro/url">
|
||||
<span i18n:name="numNews" tal:content="numNews" /> new notification</a></div>
|
||||
<div tal:condition="python:numNews > 1">
|
||||
<a i18n:translate=""
|
||||
tal:attributes="href macro/url">
|
||||
<span i18n:name="numNews" tal:content="numNews" /> new notifications</a></div>
|
||||
</metal:actions>
|
||||
|
||||
|
||||
<metal:block define-macro="notifications">
|
||||
<div tal:attributes="class string:content-$level;">
|
||||
<metal:title use-macro="item/concept_macros/concepttitle" />
|
||||
<table class="listing">
|
||||
<tr class="header">
|
||||
<th>Date/Time</th>
|
||||
<th>Sender</th>
|
||||
<th>Object</th>
|
||||
<th>Text</th>
|
||||
<th>Date/Time read</th>
|
||||
</tr>
|
||||
<tr tal:repeat="notif item/getNotificationsFormatted">
|
||||
<td tal:content="notif/timeStamp" />
|
||||
<td tal:define="sender notif/sender">
|
||||
<a tal:attributes="href sender/url"
|
||||
tal:content="sender/label" /></td>
|
||||
<td tal:define="object notif/object">
|
||||
<a tal:attributes="href object/url"
|
||||
tal:content="object/label" /></td>
|
||||
<td tal:content="notif/text" />
|
||||
<td tal:content="notif/read_ts" />
|
||||
</tr>
|
||||
</table>
|
||||
<form name="notifications" method="post">
|
||||
<input type="checkbox" name="show_all" id="notifications.show_all"
|
||||
title="Show all notifications"
|
||||
i18n:attributes="title"
|
||||
onclick="submit()"
|
||||
tal:attributes="checked request/form/show_all|nothing" />
|
||||
<label for="notifications.show_all"
|
||||
i18n:translate="">Show all notifications</label>
|
||||
<br />
|
||||
<table class="listing">
|
||||
<tr class="header">
|
||||
<th i18n:translate="">Date/Time</th>
|
||||
<th i18n:translate="">Sender</th>
|
||||
<th i18n:translate="">Object</th>
|
||||
<th i18n:translate="">Message</th>
|
||||
<th i18n:translate="">Date/Time Read</th>
|
||||
</tr>
|
||||
<tr tal:repeat="notif item/getNotificationsFormatted">
|
||||
<td tal:content="notif/timeStamp" />
|
||||
<td tal:define="sender notif/sender">
|
||||
<a tal:omit-tag="not:sender/url"
|
||||
tal:attributes="href sender/url"
|
||||
tal:content="sender/label" /></td>
|
||||
<td tal:define="object notif/object">
|
||||
<a tal:attributes="href object/url"
|
||||
tal:content="object/label" /></td>
|
||||
<td tal:content="notif/text" />
|
||||
<td tal:content="notif/read_ts" />
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</metal:block>
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -181,6 +181,12 @@ Querying objects by state
|
|||
[<...>]
|
||||
|
||||
|
||||
Person States
|
||||
=============
|
||||
|
||||
>>> from loops.organize.stateful.person import personStates
|
||||
|
||||
|
||||
Task States
|
||||
===========
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ template = ViewPageTemplateFile('view_macros.pt')
|
|||
|
||||
statefulActions = ('classification_quality',
|
||||
'simple_publishing',
|
||||
'person_states',
|
||||
'task_states',
|
||||
'publishable_task',)
|
||||
|
||||
|
|
|
@ -35,6 +35,20 @@
|
|||
set_schema="cybertools.stateful.interfaces.IStateful" />
|
||||
</zope:class>
|
||||
|
||||
<zope:utility
|
||||
factory="loops.organize.stateful.person.personStates"
|
||||
name="person_states" />
|
||||
|
||||
<zope:adapter
|
||||
factory="loops.organize.stateful.person.StatefulPerson"
|
||||
name="person_states" />
|
||||
<zope:class class="loops.organize.stateful.person.StatefulPerson">
|
||||
<require permission="zope.View"
|
||||
interface="cybertools.stateful.interfaces.IStateful" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="cybertools.stateful.interfaces.IStateful" />
|
||||
</zope:class>
|
||||
|
||||
<zope:utility
|
||||
factory="loops.organize.stateful.task.taskStates"
|
||||
name="task_states" />
|
||||
|
|
63
organize/stateful/person.py
Normal file
63
organize/stateful/person.py
Normal file
|
@ -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'
|
||||
|
Loading…
Add table
Reference in a new issue