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"
|
"Project-Id-Version: 0.13.0\n"
|
||||||
"POT-Creation-Date: 2007-05-22 12:00 CET\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"
|
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
|
||||||
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
"Language-Team: loops developers <helmutm@cy55.de>\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
@ -483,6 +483,30 @@ msgstr "Lesezeichen für aktuelles Objekt hinzufügen"
|
||||||
msgid "Remove from favorites"
|
msgid "Remove from favorites"
|
||||||
msgstr "Lesezeichen entfernen"
|
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"
|
msgid "Personal Informations"
|
||||||
msgstr "Persönliche Informationen"
|
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.
|
It's possible to store more than one notification concerning the same object.
|
||||||
|
|
||||||
>>> notifications.add(d001, person, 'I send myself another letter.')
|
>>> notifications.add(d001, person, 'I send myself another letter.')
|
||||||
>>> len(list(notifications.listTracks()))
|
>>> len(list(notifications.listTracks(unreadOnly=False)))
|
||||||
2
|
2
|
||||||
|
|
||||||
|
Only unread notifications are listed by default.
|
||||||
|
|
||||||
|
>>> len(list(notifications.listTracks()))
|
||||||
|
1
|
||||||
|
|
||||||
User interface
|
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
|
# 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
|
# 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.
|
A view configurator provides configuration data for a view controller.
|
||||||
|
|
||||||
$Id$
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from zope import component
|
from zope import component
|
||||||
|
@ -30,7 +28,9 @@ from zope.traversing.browser.absoluteurl import absoluteURL
|
||||||
|
|
||||||
from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty
|
from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty
|
||||||
from cybertools.meta.interfaces import IOptions
|
from cybertools.meta.interfaces import IOptions
|
||||||
|
from loops.browser.node import NodeView
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
from loops.organize.personal.notification import Notifications
|
||||||
from loops.util import _
|
from loops.util import _
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,10 +44,11 @@ class PortletConfigurator(ViewConfigurator):
|
||||||
def __init__(self, context, request):
|
def __init__(self, context, request):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.request = request
|
self.request = request
|
||||||
|
self.view = NodeView(self.context, self.request)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def viewProperties(self):
|
def viewProperties(self):
|
||||||
return self.favorites + self.filters
|
return self.favorites + self.filters + self.notifications
|
||||||
|
|
||||||
@Lazy
|
@Lazy
|
||||||
def records(self):
|
def records(self):
|
||||||
|
@ -97,3 +98,27 @@ class PortletConfigurator(ViewConfigurator):
|
||||||
#url=absoluteURL(self.context, self.request) + '/@@filters.html',
|
#url=absoluteURL(self.context, self.request) + '/@@filters.html',
|
||||||
))
|
))
|
||||||
return [filters]
|
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"
|
class="loops.organize.personal.browser.notification.NotificationsListing"
|
||||||
permission="zope.View" />
|
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>
|
</configure>
|
||||||
|
|
|
@ -24,8 +24,10 @@ from zope import component
|
||||||
from zope.app.pagetemplate import ViewPageTemplateFile
|
from zope.app.pagetemplate import ViewPageTemplateFile
|
||||||
from zope.cachedescriptors.property import Lazy
|
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.concept import ConceptView
|
||||||
|
from loops.browser.node import NodeView
|
||||||
from loops.common import adapted, baseObject
|
from loops.common import adapted, baseObject
|
||||||
from loops.organize.personal.notification import Notifications
|
from loops.organize.personal.notification import Notifications
|
||||||
from loops.organize.party import getPersonForUser
|
from loops.organize.party import getPersonForUser
|
||||||
|
@ -50,19 +52,28 @@ class NotificationsListing(ConceptView):
|
||||||
return Notifications(self.person)
|
return Notifications(self.person)
|
||||||
|
|
||||||
def getNotifications(self, unreadOnly=True):
|
def getNotifications(self, unreadOnly=True):
|
||||||
tracks = self.notifications.listTracks()
|
if self.person is None:
|
||||||
|
return []
|
||||||
|
tracks = self.notifications.listTracks(unreadOnly)
|
||||||
return tracks
|
return tracks
|
||||||
|
|
||||||
def getNotificationsFormatted(self, unreadOnly=True):
|
def getNotificationsFormatted(self):
|
||||||
|
unreadOnly = not self.request.form.get('show_all')
|
||||||
result = []
|
result = []
|
||||||
for track in self.getNotifications(unreadOnly):
|
for track in self.getNotifications(unreadOnly):
|
||||||
data = track.data
|
data = track.data
|
||||||
s = util.getObjectForUid(data.get('sender'))
|
s = util.getObjectForUid(data.get('sender'))
|
||||||
sender = dict(label=s.title,
|
if s is None:
|
||||||
url=self.nodeView.getUrlForTarget(baseObject(s)))
|
sender = dict(label=u'???', url=u'')
|
||||||
|
else:
|
||||||
|
sender = dict(label=s.title,
|
||||||
|
url=self.nodeView.getUrlForTarget(baseObject(s)))
|
||||||
obj = util.getObjectForUid(track.taskId)
|
obj = util.getObjectForUid(track.taskId)
|
||||||
object = dict(label=obj.title,
|
ov = self.nodeView.getViewForTarget(obj)
|
||||||
url=self.nodeView.getUrlForTarget(baseObject(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'))
|
read_ts = self.formatTimeStamp(data.get('read_ts'))
|
||||||
item = dict(timeStamp=self.formatTimeStamp(track.timeStamp),
|
item = dict(timeStamp=self.formatTimeStamp(track.timeStamp),
|
||||||
sender=sender,
|
sender=sender,
|
||||||
|
@ -71,3 +82,22 @@ class NotificationsListing(ConceptView):
|
||||||
read_ts=read_ts)
|
read_ts=read_ts)
|
||||||
result.append(item)
|
result.append(item)
|
||||||
return result
|
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>
|
||||||
|
|
||||||
|
|
||||||
|
<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">
|
<metal:block define-macro="notifications">
|
||||||
<div tal:attributes="class string:content-$level;">
|
<div tal:attributes="class string:content-$level;">
|
||||||
<metal:title use-macro="item/concept_macros/concepttitle" />
|
<metal:title use-macro="item/concept_macros/concepttitle" />
|
||||||
<table class="listing">
|
<form name="notifications" method="post">
|
||||||
<tr class="header">
|
<input type="checkbox" name="show_all" id="notifications.show_all"
|
||||||
<th>Date/Time</th>
|
title="Show all notifications"
|
||||||
<th>Sender</th>
|
i18n:attributes="title"
|
||||||
<th>Object</th>
|
onclick="submit()"
|
||||||
<th>Text</th>
|
tal:attributes="checked request/form/show_all|nothing" />
|
||||||
<th>Date/Time read</th>
|
<label for="notifications.show_all"
|
||||||
</tr>
|
i18n:translate="">Show all notifications</label>
|
||||||
<tr tal:repeat="notif item/getNotificationsFormatted">
|
<br />
|
||||||
<td tal:content="notif/timeStamp" />
|
<table class="listing">
|
||||||
<td tal:define="sender notif/sender">
|
<tr class="header">
|
||||||
<a tal:attributes="href sender/url"
|
<th i18n:translate="">Date/Time</th>
|
||||||
tal:content="sender/label" /></td>
|
<th i18n:translate="">Sender</th>
|
||||||
<td tal:define="object notif/object">
|
<th i18n:translate="">Object</th>
|
||||||
<a tal:attributes="href object/url"
|
<th i18n:translate="">Message</th>
|
||||||
tal:content="object/label" /></td>
|
<th i18n:translate="">Date/Time Read</th>
|
||||||
<td tal:content="notif/text" />
|
</tr>
|
||||||
<td tal:content="notif/read_ts" />
|
<tr tal:repeat="notif item/getNotificationsFormatted">
|
||||||
</tr>
|
<td tal:content="notif/timeStamp" />
|
||||||
</table>
|
<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>
|
</div>
|
||||||
</metal:block>
|
</metal:block>
|
||||||
|
|
|
@ -33,9 +33,12 @@ class Notifications(Favorites):
|
||||||
self.context = (baseObject(person).
|
self.context = (baseObject(person).
|
||||||
getLoopsRoot().getRecordManager()['favorites'])
|
getLoopsRoot().getRecordManager()['favorites'])
|
||||||
|
|
||||||
def listTracks(self):
|
def listTracks(self, unreadOnly=True):
|
||||||
return super(Notifications, self).listTracks(
|
tracks = super(Notifications, self).listTracks(
|
||||||
baseObject(self.person), type='notification')
|
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):
|
def add(self, obj, sender, text):
|
||||||
senderUid = util.getUidForObject(baseObject(sender))
|
senderUid = util.getUidForObject(baseObject(sender))
|
||||||
|
|
|
@ -181,6 +181,12 @@ Querying objects by state
|
||||||
[<...>]
|
[<...>]
|
||||||
|
|
||||||
|
|
||||||
|
Person States
|
||||||
|
=============
|
||||||
|
|
||||||
|
>>> from loops.organize.stateful.person import personStates
|
||||||
|
|
||||||
|
|
||||||
Task States
|
Task States
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ template = ViewPageTemplateFile('view_macros.pt')
|
||||||
|
|
||||||
statefulActions = ('classification_quality',
|
statefulActions = ('classification_quality',
|
||||||
'simple_publishing',
|
'simple_publishing',
|
||||||
|
'person_states',
|
||||||
'task_states',
|
'task_states',
|
||||||
'publishable_task',)
|
'publishable_task',)
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,20 @@
|
||||||
set_schema="cybertools.stateful.interfaces.IStateful" />
|
set_schema="cybertools.stateful.interfaces.IStateful" />
|
||||||
</zope:class>
|
</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
|
<zope:utility
|
||||||
factory="loops.organize.stateful.task.taskStates"
|
factory="loops.organize.stateful.task.taskStates"
|
||||||
name="task_states" />
|
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