From 60af09cedbef7ff033edf33444c977299a4008b9 Mon Sep 17 00:00:00 2001 From: helmutm Date: Thu, 13 Mar 2008 17:42:05 +0000 Subject: [PATCH] added favorites portlet with add and remove functionality git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@2452 fd906abe-77d9-0310-91a1-e0d9ade77398 --- browser/loops.css | 10 +++ browser/node.py | 7 ++ locales/de/LC_MESSAGES/loops.mo | Bin 6027 -> 6341 bytes locales/de/LC_MESSAGES/loops.po | 15 ++++ organize/personal/README.txt | 58 ++++++++++-- organize/personal/browser/configurator.py | 62 +++++++++++++ organize/personal/browser/configure.zcml | 23 +++++ organize/personal/browser/favorite.py | 88 +++++++++++++++++++ organize/personal/browser/personal_macros.pt | 24 +++++ organize/personal/configure.zcml | 10 ++- organize/personal/favorite.py | 44 +++++++++- organize/personal/interfaces.py | 10 ++- 12 files changed, 340 insertions(+), 11 deletions(-) create mode 100644 organize/personal/browser/configurator.py create mode 100644 organize/personal/browser/configure.zcml create mode 100644 organize/personal/browser/favorite.py create mode 100644 organize/personal/browser/personal_macros.pt diff --git a/browser/loops.css b/browser/loops.css index 0e483b4..dbf9962 100644 --- a/browser/loops.css +++ b/browser/loops.css @@ -137,6 +137,10 @@ div.box h4 { padding: 0.2em 0.2em 0.2em 0.3em; } +div.action { + border-top: 1px solid #eeeeee; +} + div.menu-1, div.menu-2 { border-top: 1px solid #eeeeee; font-weight: bold; @@ -152,6 +156,12 @@ div.menu-1, div.menu-2 { font-size: 90% } +.delete-item a[href] { + color: #ff7777; + font-weight: bold; + text-decoration: none; +} + .subcolumn { display: inline; float: left; diff --git a/browser/node.py b/browser/node.py index 9603cd1..6aa1d67 100644 --- a/browser/node.py +++ b/browser/node.py @@ -290,6 +290,13 @@ class NodeView(BaseView): target = getVersion(target, self.request) return target + @Lazy + def targetUid(self): + if self.virtualTargetObject: + return util.getUidForObject(self.virtualTargetObject) + else: + return None + def targetView(self, name='index.html', methodName='show'): target = self.virtualTargetObject if target is not None: diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index 33c19e12fca0c102c9d1e6c4923c5e6cfab77d7e..529e1f04865d7a01d8c135ca7b91c881eef1a256 100644 GIT binary patch delta 2468 zcmZ|Qe@sOe`XlU*T-H|0_5NHQo&C`>zUOt$bIyIf z&#!aXU-`-E#B^T9K|=`=#Y9WGG0)?ZnH(su_>5`8Be)7L;I+6U%a{_p8FSH(^Kd)T zgz3X<4B2`Q7jgX*7GM(jOPF^!D%VN84nM*N@CziDSvGezVHxIgU5$CT8I^b^D)Aty zojtaG1oiv@T!?Sr_4qy(;@6l*eluflT*AfND4^R5uo^SakMzYfBY$Qa2PF!iIvB$F z_&Dl!BdCszp*s2+YUC#{2S2y>&tew&%?u|>bP=oZFWtaOhNT~@MJ3vVs&7LrK_@C< zkJUqdYMw>4H-`L~B!^Ob9o4ZZ)bD@5gc?4_Nfn;AHx`psOHqo~U=6Ax4IDI*EvVmh zqY{NsGZIG4gooNyX!#<+ij2h^ME(9%RL76n`VW!SHdFJNfA#De``|Q|aeWT8yK|WaO;r^t zQJr(VM&CD=rNuIOUFQJwq@fIg~@LkmFaT3+D zFHwnSun5!WjVwkrSc}WC8TH-Rh5VTpIjG?ysE!;%&Cm%{o-?-oGbB&Ke9wtS_B$%! zTv}J6rKpA*PzgIYsDokDNS;A0$pO3rlbBjc)QnD}K19EwX6!e6|8HbeMB~;&wtfQH zr{<{UUn4t31#eBNsD~;$iSZ5O6aY) zx@d+~9wN%M{$-rpr5efxqKD`uRJIcxghsuFSVr7KbP<|4ZM+?Xil$hlo=8ib%^ox} z6@+H=a(Tp_v}yhCC$zC{vz7OvrkhE*QuHRMG!iw$HeFD(-X*CsV{Ss1(9)=A^FB;y zNty|5)&Q}QP^lymEgZB-RMrx$sWbksV;!N-^9oz14~_PNKB+2Oh|PpHp^Da8>#9AW zvSgOjzjK{-677V(8@z=onwZ(5kB;_26QNztrZ#@UA+#y0h;`#Nz85O``kda_(2yJP zoM_KuZm;KfQ70JK9UbcT++owmeJcF6V)C>vndZBEcf38j%hw+na6I=3Z~X1-3whz_ zZr2G8MZ;H`m^?i1)AWr4Ud$aFbcdZ*atwG*Jm$2x!*1N|?+v*TC)6K_$AV{0Kk7zI y=l|TA?9MGt&%Wwc^t{55lm8e_x%X_#RJ-JysZnao}^lJ+khss1Mb delta 2163 zcmYk-duWzb9KiA8rkm4EbIrNbx6SgBI-PU6wr*?HtSoJAE?v{M6)S=U6$GKwgZPiG z3&iW?4>J*}KSC>e(F&weu!Kf~f?OEuf}#urC8?PbeZQ|~&|$ylbIy66=iHB;sD8aF z87dyTGmyH8nZ)ee5Vm4v9uHF8_z;@09v5OC=Hn48#4nLg!gn|k2V(tcoJ#pTmg2uy zfKz$cbU98AAqjKIv{F%vyc7D+4$^od?!jXG7;nJiXvaUI4gD7D&!O*+Uqwuz>!<8)STO7uvyl=n5P}J2)Ku7WoT5^RS__=t>OZ9Q+&YC`F~; zFGky|#Ut8^YKD>g-I5OMFIDog%58gpLcpshlhv-Fw4?24L%XnUObGk%0m_4{-<_CZ zz5Tr%`6)aV%P*oc--I5@H_=L|Em za|r)~a(1RAgtuihxxb5uM&efD5yI3&c>hgp*9mLU$(YLAfA`e1ieTkKJMkE?oG`g} zmBcy=B$v+QlDglfDuUI_ZlC2@K1goE?Swa^g)q${8i+?Nxc{bd!fSXR@enbZ=Eh9+ zJdo$mXHyN&4TS#)xn)O`hO-qLc6pjHVT2?j@EI z_YcN?C6D?UJs{)spHl JnH8l;&VQ$Cu_6Ef diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index ba77769..932cf8b 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -65,6 +65,18 @@ msgstr "Glossareintrag anlegen..." msgid "Create Glossary Item" msgstr "Glossareintrag anlegen" +msgid "Favorites" +msgstr "Lesezeichen" + +msgid "Add to Favorites" +msgstr "Zu Lesezeichen hinzufügen" + +msgid "Add current object to favorites" +msgstr "Aktuelles Objekt zu Lesezeichen hinzufügen" + +msgid "Remove from favorites" +msgstr "Aus Lesezeichen entfernen" + msgid "Actions" msgstr "Aktionen" @@ -134,6 +146,9 @@ msgstr "Format" msgid "Link URL" msgstr "URL" +msgid "Link text" +msgstr "Link Text" + msgid "Assign Parent Concepts" msgstr "Oberbegriffe zuordnen" diff --git a/organize/personal/README.txt b/organize/personal/README.txt index b4ab241..a7dd4c9 100644 --- a/organize/personal/README.txt +++ b/organize/personal/README.txt @@ -79,6 +79,15 @@ Finally, we log in as the newly created user. >>> from loops.tests.auth import login >>> login(pJohn) +One step is still missing: As we are now working with a real principal +the security checks e.g. in views are active. So we have to provide +our user with the necessary permissions. + + >>> grantPermission = setupData.rolePermissions.grantPermissionToRole + >>> assignRole = setupData.principalRoles.assignRoleToPrincipal + >>> grantPermission('zope.View', 'zope.Member') + >>> assignRole('zope.Member', 'users.john') + Working with the favorites storage ---------------------------------- @@ -93,23 +102,26 @@ can remember as favorites. >>> d003Id = util.getUidForObject(resources['d003.txt']) >>> johnCId = util.getUidForObject(johnC) -(We always need a "run" - can we try to ignore this for favorites?) +We do not access the favorites storage directly but by using an adapter. - >>> runId = favorites.startRun() + >>> from loops.organize.personal.favorite import Favorites + >>> component.provideAdapter(Favorites) + >>> from loops.organize.personal.interfaces import IFavorites + >>> favAdapted = IFavorites(favorites) -For favorites we don't need any data... +The adapter provides convenience methods for accessing the favorites storage. - >>> favorites.saveUserTrack(d001Id, runId, johnCId, {}) + >>> favAdapted.add(resources['d001.txt'], johnC) '0000001' - >>> favorites.saveUserTrack(d003Id, runId, johnCId, {}) - '0000002' So we are now ready to query the favorites. >>> favs = favorites.query(userName=johnCId) >>> favs - [, - ] + [] + + >>> list(favAdapted.list(johnC)) + ['27'] >>> util.getObjectForUid(favs[0].taskId) is resources['d001.txt'] True @@ -117,3 +129,33 @@ So we are now ready to query the favorites. User interface -------------- + >>> from loops.view import Node + >>> home = views['home'] = Node() + >>> from loops.tests.auth import TestRequest + >>> from loops.organize.personal.browser.configurator import PortletConfigurator + + >>> portletConf = PortletConfigurator(home, TestRequest()) + >>> len(portletConf.viewProperties) + 1 + + >>> from loops.organize.personal.browser.favorite import FavoriteView + >>> view = FavoriteView(home, TestRequest()) + +Let's now trigger the saving of a favorite. + + >>> d002Id = util.getUidForObject(resources['d002.txt']) + >>> request = TestRequest(form=dict(id=d002Id)) + >>> view = FavoriteView(home, request) + + >>> view.add() + + >>> len(favorites.query(userName=johnCId)) + 2 + + >>> d002Id = util.getUidForObject(resources['d001.txt']) + >>> request = TestRequest(form=dict(id=d002Id)) + >>> view = FavoriteView(home, request) + >>> view.remove() + + >>> len(favorites.query(userName=johnCId)) + 1 diff --git a/organize/personal/browser/configurator.py b/organize/personal/browser/configurator.py new file mode 100644 index 0000000..2249c20 --- /dev/null +++ b/organize/personal/browser/configurator.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2008 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 +# + +""" +A view configurator provides configuration data for a view controller. + +$Id$ +""" + +from zope import component +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.app.security.interfaces import IUnauthenticatedPrincipal +from zope.cachedescriptors.property import Lazy +from zope.traversing.browser.absoluteurl import absoluteURL + +from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty +from loops.organize.party import getPersonForUser +from loops.util import _ + + +personal_macros = ViewPageTemplateFile('personal_macros.pt') + + +class PortletConfigurator(ViewConfigurator): + """ Specify portlets. + """ + + def __init__(self, context, request): + self.context = context + self.request = request + + @property + def viewProperties(self): + if getPersonForUser(self.context, self.request) is None: + #if IUnauthenticatedPrincipal.providedBy(self.request.principal): + return [] + favorites = MacroViewProperty(self.context, self.request) + favorites.setParams(dict( + slot='portlet_right', + identifier='loops.organize.favorites', + title=_(u'Favorites'), + subMacro=personal_macros.macros['favorites_portlet'], + priority=200, + #url=absoluteURL(self.context, self.request) + '/@@favorites.html', + )) + return [favorites] + diff --git a/organize/personal/browser/configure.zcml b/organize/personal/browser/configure.zcml new file mode 100644 index 0000000..f5bd7ce --- /dev/null +++ b/organize/personal/browser/configure.zcml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/organize/personal/browser/favorite.py b/organize/personal/browser/favorite.py new file mode 100644 index 0000000..6f376c3 --- /dev/null +++ b/organize/personal/browser/favorite.py @@ -0,0 +1,88 @@ +# +# Copyright (c) 2008 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 +# + +""" +A view (to be used by listings, portlets, ...) for favorites. + +$Id$ +""" + +from zope import component +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy + +from cybertools.browser.configurator import ViewConfigurator, MacroViewProperty +from loops.browser.node import NodeView +from loops.organize.party import getPersonForUser +from loops.organize.personal.interfaces import IFavorites +from loops import util + + +personal_macros = ViewPageTemplateFile('personal_macros.pt') + + +class FavoriteView(NodeView): + + @Lazy + def item(self): + return self + + @Lazy + def person(self): + return getPersonForUser(self.context, self.request) + + @Lazy + def favorites(self): + records = self.loopsRoot.getRecordManager() + if records is not None: + storage = records.get('favorites') + if storage is not None: + return IFavorites(storage) + return None + + def listFavorites(self): + if self.favorites is None: + return + for uid in self.favorites.list(self.person): + obj = util.getObjectForUid(uid) + if obj is not None: + yield dict(url=self.getUrlForTarget(obj), + uid=uid, + title=obj.title, + description=obj.description, + object=obj) + + def add(self): + if self.favorites is None: + return + uid = self.request.get('id') + if not uid: + return + obj = util.getObjectForUid(uid) + self.favorites.add(obj, self.person) + self.request.response.redirect(self.virtualTargetUrl) + + def remove(self): + if self.favorites is None: + return + uid = self.request.get('id') + if not uid: + return + obj = util.getObjectForUid(uid) + self.favorites.remove(obj, self.person) + self.request.response.redirect(self.virtualTargetUrl) diff --git a/organize/personal/browser/personal_macros.pt b/organize/personal/browser/personal_macros.pt new file mode 100644 index 0000000..a6f9b52 --- /dev/null +++ b/organize/personal/browser/personal_macros.pt @@ -0,0 +1,24 @@ + +
+  X  + Some object +
+ +
diff --git a/organize/personal/configure.zcml b/organize/personal/configure.zcml index bfcbd68..c946b74 100644 --- a/organize/personal/configure.zcml +++ b/organize/personal/configure.zcml @@ -2,7 +2,7 @@ + i18n_domain="loops"> + + + + + + + diff --git a/organize/personal/favorite.py b/organize/personal/favorite.py index 826bd2a..2f7cd3b 100644 --- a/organize/personal/favorite.py +++ b/organize/personal/favorite.py @@ -22,10 +22,51 @@ Base classes for a notification framework. $Id$ """ +from zope.component import adapts from zope.interface import implements from cybertools.tracking.btree import Track -from loops.organize.personal.interfaces import IFavorite +from cybertools.tracking.interfaces import ITrackingStorage +from loops.organize.personal.interfaces import IFavorites, IFavorite +from loops import util + + +class Favorites(object): + + implements(IFavorites) + adapts(ITrackingStorage) + + def __init__(self, context): + self.context = context + + def list(self, person, sortKey=None): + if person is None: + return + personUid = util.getUidForObject(person) + if sortKey is None: + sortKey = lambda x: -x.timeStamp + for item in sorted(self.context.query(userName=personUid), key=sortKey): + yield item.taskId + + def add(self, obj, person): + if None in (obj, person): + return False + uid = util.getUidForObject(obj) + personUid = util.getUidForObject(person) + if self.context.query(userName=personUid, taskId=uid): + return False + return self.context.saveUserTrack(uid, 0, personUid, {}) + + def remove(self, obj, person): + if None in (obj, person): + return False + uid = util.getUidForObject(obj) + personUid = util.getUidForObject(person) + changed = False + for t in self.context.query(userName=personUid, taskId=uid): + changed = True + self.context.removeTrack(t) + return changed class Favorite(Track): @@ -33,3 +74,4 @@ class Favorite(Track): implements(IFavorite) typeName = 'Favorite' + diff --git a/organize/personal/interfaces.py b/organize/personal/interfaces.py index b3fe7ad..adfdea7 100644 --- a/organize/personal/interfaces.py +++ b/organize/personal/interfaces.py @@ -28,10 +28,18 @@ from zope import schema from cybertools.tracking.interfaces import ITrack +class IFavorites(Interface): + """ A collection of favorites. + """ + + def add(objectId, personId): + """ Add an object to the person's favorites collection. + """ + + class IFavorite(ITrack): """ A favorite references a content object via the task id attribute; the user name references the user/person for which the favorite is to be stored. The tracking storage's run management is not used. """ -