diff --git a/browser/common.py b/browser/common.py index 8639958..82c6d65 100644 --- a/browser/common.py +++ b/browser/common.py @@ -49,7 +49,7 @@ from zope.security import canAccess from zope.security.interfaces import ForbiddenAttribute, Unauthorized from zope.security.proxy import removeSecurityProxy from zope.traversing.browser import absoluteURL -from zope.traversing.api import getName, getParent +from zope.traversing.api import getName, getParent, traverse from cybertools.ajax.dojo import dojoMacroTemplate from cybertools.browser.view import GenericView @@ -70,7 +70,7 @@ from loops.organize.tracking import access from loops.resource import Resource from loops.security.common import checkPermission from loops.security.common import canAccessObject, canListObject, canWriteObject -from loops.type import ITypeConcept +from loops.type import ITypeConcept, LoopsTypeInfo from loops import util from loops.util import _, saveRequest from loops import version @@ -481,7 +481,7 @@ class BaseView(GenericView, I18NView): return absoluteURL(provider, self.request) return None - def renderText(self, text, contentType): + def renderText(self, text, contentType='text/restructured'): text = util.toUnicode(text) typeKey = util.renderingFactories.get(contentType, None) if typeKey is None: @@ -531,11 +531,29 @@ class BaseView(GenericView, I18NView): def conceptTypes(self): return util.KeywordVocabulary(self.listTypes(('concept',), ('hidden',))) + def parentTypesFromOtherSites(self): + result = [] + typeNames = self.typeOptions('foreign_parent_types') or [] + for path in self.typeOptions('foreign_parent_sites') or []: + site = traverse(self.loopsRoot, path, None) + if site is None: + continue + cm = site.getConceptManager() + for tname in typeNames: + t = cm.get(tname) + if t is not None: + type = LoopsTypeInfo(t) + type.isForeignReference = True + result.append(type) + return result + def listTypesForSearch(self, include=None, exclude=None, sortOn='title'): types = [dict(token=t.tokenForSearch, title=t.title) for t in ITypeManager(self.context).listTypes(include, exclude)] if sortOn: types.sort(key=lambda x: x[sortOn]) + for t in self.parentTypesFromOtherSites(): + types.append(dict(token=t.tokenForSearch, title=t.title)) return types def typesForSearch(self): diff --git a/browser/compound/__init__.py b/browser/compound/__init__.py new file mode 100644 index 0000000..e12cdd8 --- /dev/null +++ b/browser/compound/__init__.py @@ -0,0 +1,4 @@ +""" +package loops.browser.compound +""" + diff --git a/browser/compound/configure.zcml b/browser/compound/configure.zcml new file mode 100644 index 0000000..fac57ba --- /dev/null +++ b/browser/compound/configure.zcml @@ -0,0 +1,14 @@ + + + + + diff --git a/browser/compound/standard.py b/browser/compound/standard.py new file mode 100644 index 0000000..4e5576f --- /dev/null +++ b/browser/compound/standard.py @@ -0,0 +1,54 @@ +# +# Copyright (c) 2013 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 +# + +""" +Definition of compound views. +""" + +from zope import interface, component +from zope.app.pagetemplate import ViewPageTemplateFile +from zope.cachedescriptors.property import Lazy + +from loops.browser.concept import ConceptView +from loops.util import _ + + +compound_macros = ViewPageTemplateFile('view_macros.pt') + + +class CompoundView(ConceptView): + + @Lazy + def macro(self): + return compound_macros.macros['standard'] + + def getParts(self): + parts = (self.options('view_parts') or self.typeOptions('view_parts') or []) + return self.getPartViews(parts) + + def getPartViews(self, parts): + result = [] + for p in parts: + view = component.queryMultiAdapter((self.adapted, self.request), name=p) + if view is None: + view = component.queryMultiAdapter((self.context, self.request), name=p) + if view is not None: + view.parent = self + result.append(view) + return result + diff --git a/browser/compound/view_macros.pt b/browser/compound/view_macros.pt new file mode 100644 index 0000000..c854558 --- /dev/null +++ b/browser/compound/view_macros.pt @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/browser/concept.py b/browser/concept.py index 50ff0a5..f06c419 100644 --- a/browser/concept.py +++ b/browser/concept.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -50,6 +50,7 @@ from cybertools.meta.interfaces import IOptions from cybertools.typology.interfaces import IType, ITypeManager from cybertools.util.jeep import Jeep from loops.browser.common import EditForm, BaseView, LoopsTerms, concept_macros +from loops.browser.common import ViewMode from loops.common import adapted from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList from loops.i18n.browser import I18NView @@ -196,6 +197,12 @@ class BaseRelationView(BaseView): return u'' return self.predicateTitle + @Lazy + def relationInfo(self): + predInfo = ', ' .join(p.title for p in self.predicates + if p != self.defaultPredicate) + return ' | '.join(t for t in (self.description, predInfo) if t) + class ConceptView(BaseView): @@ -228,6 +235,7 @@ class ConceptView(BaseView): subMacro=concept_macros.macros['parents'], priority=20, info=self) + # the part-based layout is now implemented in loops.browser.compound def getParts(self): parts = (self.params.get('parts') or []) # deprecated! if not parts: @@ -740,3 +748,30 @@ class ListTypeInstances(ListChildren): noDuplicates, useFilter, [self.typePredicate]): yield c + +class TabbedPage(ConceptView): + + @Lazy + def subpagePredicates(self): + pred = self.conceptManager.get('issubpage') + if pred is None: + pred = self.isPartOfPredicate + return [pred] + + def viewModes(self): + modes = Jeep() + for s in self.getSiblings(self.subpagePredicates): + url = self.nodeView.getUrlForTarget(s) + modes.append(ViewMode(getName(s), s.title, url)) + if not modes: + return modes + modes[getName(self.context)].active = True + return modes + + def getSiblings(self, preds): + for p in self.context.getParents(preds): + parent = p + break + else: + return [] + return p.getChildren(preds) diff --git a/browser/concept_macros.pt b/browser/concept_macros.pt index 78ca2b3..eb3ed12 100644 --- a/browser/concept_macros.pt +++ b/browser/concept_macros.pt @@ -62,9 +62,10 @@ string:$resourceBase/cybertools.icons/table.png" /> - + -

Description

@@ -158,11 +159,8 @@ tal:attributes="dojoType python: item.editable and 'dojo.dnd.Source' or ''"> - + + title related/relationInfo"> Resource Title @@ -241,11 +239,8 @@ tal:attributes="dojoType python: item.editable and 'dojo.dnd.Source' or ''"> - + + title related/relationInfo">
Resource Title
diff --git a/browser/configure.zcml b/browser/configure.zcml index f6f3695..75e5d7d 100644 --- a/browser/configure.zcml +++ b/browser/configure.zcml @@ -553,6 +553,14 @@ factory="loops.browser.concept.ListTypeInstances" permission="zope.View" /> + + + diff --git a/browser/form_macros.pt b/browser/form_macros.pt index 63543d3..47e846b 100644 --- a/browser/form_macros.pt +++ b/browser/form_macros.pt @@ -51,6 +51,8 @@ + +
@@ -59,7 +61,7 @@ - @@ -119,6 +121,8 @@ tal:attributes="value typeToken" /> + +
@@ -127,7 +131,7 @@ - diff --git a/browser/lobo/standard.pt b/browser/lobo/standard.pt index 9148b61..34a015e 100644 --- a/browser/lobo/standard.pt +++ b/browser/lobo/standard.pt @@ -117,6 +117,7 @@ - + diff --git a/browser/resource.py b/browser/resource.py index 015c57b..0960cb2 100644 --- a/browser/resource.py +++ b/browser/resource.py @@ -40,6 +40,7 @@ from zope.traversing.browser import absoluteURL from cybertools.browser.action import actions from cybertools.meta.interfaces import IOptions from cybertools.typology.interfaces import IType +from cybertools.util.html import extractFirstPart from cybertools.xedit.browser import ExternalEditorView, fromUnicode from loops.browser.action import DialogAction, TargetAction from loops.browser.common import EditForm, BaseView @@ -131,6 +132,9 @@ class ResourceView(BaseView): def macro(self): if 'image/' in self.context.contentType: return self.template.macros['image'] + #elif 'audio/' in self.context.contentType: + # self.registerDojoAudio() + # return self.template.macros['audio'] else: return self.template.macros['download'] @@ -252,6 +256,12 @@ class ResourceView(BaseView): #return util.toUnicode(wp.render(self.request)) return super(ResourceView, self).renderText(text, contentType) + def renderShortText(self): + return self.renderDescription() or self.createShortText(self.render()) + + def createShortText(self, text=None): + return extractFirstPart(text or self.render()) + def download(self): """ Force download, e.g. of a PDF file """ return self.show(True) diff --git a/browser/skin/lobo/body.pt b/browser/skin/lobo/body.pt index f1f08c6..2a3ea56 100644 --- a/browser/skin/lobo/body.pt +++ b/browser/skin/lobo/body.pt @@ -30,7 +30,13 @@
-
+ +
+ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/compound/book/interfaces.py b/compound/book/interfaces.py deleted file mode 100644 index 3dd1e43..0000000 --- a/compound/book/interfaces.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# Copyright (c) 2012 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 -# - -""" -Books, sections, pages... -""" - -from zope.interface import Interface, Attribute -from zope import interface, component, schema - -from loops.compound.interfaces import ICompound -from loops.util import _ - - -class IPage(ICompound): - - pass diff --git a/compound/book/loops_book_de.dmp b/compound/book/loops_book_de.dmp index 5185bb2..cc430f2 100644 --- a/compound/book/loops_book_de.dmp +++ b/compound/book/loops_book_de.dmp @@ -1,14 +1,15 @@ type(u'documenttype', u'Dokumentenart', options=u'qualifier:assign', + typeInterface=u'loops.interfaces.IOptions', viewName=u'') # book types type(u'book', u'Buch', viewName=u'book_overview', typeInterface=u'', options=u'action.portlet:create_subtype,edit_concept') -#type(u'page', u'Seite', viewName=u'page_layout', -# typeInterface=u'loops.compound.book.interfaces.IPage', -# options=u'action.portlet:edit_concept') type(u'section', u'Kapitel', viewName=u'section_view', typeInterface=u'', options=u'action.portlet:create_subtype,edit_concept') +#type(u'topic', u'Thema', viewName=u'book_topic_view', +# typeInterface=u'loops.knowledge.interfaces.ITopic', +# options=u'action.portlet:create_topic,edit_topic') concept(u'system', u'System', u'domain') @@ -26,8 +27,8 @@ concept(u'quote', u'Zitat', u'documenttype') concept(u'story', u'Geschichte', u'documenttype') concept(u'tip', u'Tipp', u'documenttype') concept(u'usecase', u'Fallbeispiel', u'documenttype') +concept(u'warning', u'Warnung', u'documenttype') # book structure child(u'book', u'section', u'issubtype', usePredicate=u'ispartof') child(u'section', u'section', u'issubtype', usePredicate=u'ispartof') -#child(u'section', u'page', u'issubtype', usePredicate=u'ispartof') diff --git a/compound/book/view_macros.pt b/compound/book/view_macros.pt index 0431662..43be71b 100644 --- a/compound/book/view_macros.pt +++ b/compound/book/view_macros.pt @@ -1,14 +1,22 @@ - - -
+ + -
-
-
-
-
-
- - - - + + +
+
+
+

+ + +

-
- -
- -
+


- - - -
- -
-
- - - -
-

- + + +

Children

+ +

Text Elements

+
+
+

+ +

+ + diff --git a/expert/browser/search.py b/expert/browser/search.py index af4f437..1776e57 100644 --- a/expert/browser/search.py +++ b/expert/browser/search.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2011 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -19,14 +19,12 @@ """ Definition of basic view classes and other browser related stuff for the loops.expert package. - -$Id$ """ from zope import interface, component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy -from zope.traversing.api import getName, getParent +from zope.traversing.api import getName, getParent, traverse from cybertools.browser.form import FormController from cybertools.stateful.interfaces import IStateful, IStatesDefinition @@ -150,12 +148,16 @@ class Search(ConceptView): if not isinstance(types, (list, tuple)): types = [types] for type in types: + site = self.loopsRoot + if type.startswith('/'): + parts = type.split(':') + site = traverse(self.loopsRoot, parts[0], site) result = self.executeQuery(title=title or None, type=type, exclude=('hidden',)) fv = FilterView(self.context, self.request) result = fv.apply(result) for o in result: - if o.getLoopsRoot() == self.loopsRoot: + if o.getLoopsRoot() == site: adObj = adapted(o, self.languageInfo) if filterMethod is not None and not filterMethod(adObj): continue diff --git a/expert/concept.py b/expert/concept.py index abd0b6f..d87dfcf 100644 --- a/expert/concept.py +++ b/expert/concept.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Query concepts management stuff. - -$Id$ """ from BTrees.IOBTree import IOBTree @@ -29,6 +27,7 @@ from zope.interface import Interface, Attribute, implements from zope.app.catalog.interfaces import ICatalog from zope.app.intid.interfaces import IIntIds from zope.cachedescriptors.property import Lazy +from zope.traversing.api import traverse from cybertools.typology.interfaces import IType from loops.common import AdapterBase @@ -66,6 +65,11 @@ class BaseQuery(object): return self.context.context.getLoopsRoot() def queryConcepts(self, title=None, type=None, **kw): + site = self.loopsRoot + if type.startswith('/'): + parts = type.split(':') + site = traverse(self.loopsRoot, parts[0], site) + type = 'loops:' + ':'.join(parts[1:]) if type.endswith('*'): start = type[:-1] end = start + '\x7f' @@ -76,7 +80,7 @@ class BaseQuery(object): result = cat.searchResults(loops_type=(start, end), loops_title=title) else: result = cat.searchResults(loops_type=(start, end)) - result = set(r for r in result if r.getLoopsRoot() == self.loopsRoot + result = set(r for r in result if r.getLoopsRoot() == site and canListObject(r)) if 'exclude' in kw: r1 = set() diff --git a/integrator/office/base.py b/integrator/office/base.py index bed343b..542aa0a 100644 --- a/integrator/office/base.py +++ b/integrator/office/base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -78,28 +78,28 @@ class OfficeFile(ExternalFileAdapter): @Lazy def docPropertyDom(self): fn = self.docFilename - dummy = dict(core=[], custom=[]) + result = dict(core=[], custom=[]) root, ext = os.path.splitext(fn) if not ext.lower() in self.fileExtensions: - return dummy + return result try: zf = ZipFile(fn, 'r') except IOError, e: from logging import getLogger self.logger.warn(e) - return dummy + return result if self.corePropFileName not in zf.namelist(): self.logger.warn('Core properties not found in file %s.' % self.externalAddress) + else: + result['core'] = etree.fromstring(zf.read(self.corePropFileName)) if self.propFileName not in zf.namelist(): self.logger.warn('Custom properties not found in file %s.' % self.externalAddress) - propsXml = zf.read(self.propFileName) - corePropsXml = zf.read(self.corePropFileName) - # TODO: read core.xml, return both trees in dictionary + else: + result['custom'] = etree.fromstring(zf.read(self.propFileName)) zf.close() - return {'custom': etree.fromstring(propsXml), - 'core': etree.fromstring(corePropsXml)} + return result def getDocProperty(self, pname): for p in self.docPropertyDom['custom']: diff --git a/interfaces.py b/interfaces.py index 1330a6a..cc82fde 100644 --- a/interfaces.py +++ b/interfaces.py @@ -690,9 +690,21 @@ class IIndexAttributes(Interface): """ +# reusable interface elements + +class IOptions(Interface): + + options = schema.List( + title=_(u'Options'), + description=_(u'Additional settings.'), + value_type=schema.TextLine(), + default=[], + required=False) + + # types stuff -class ITypeConcept(IConceptSchema, ILoopsAdapter): +class ITypeConcept(IConceptSchema, ILoopsAdapter, IOptions): """ Concepts of type 'type' should be adaptable to this interface. """ @@ -725,13 +737,6 @@ class ITypeConcept(IConceptSchema, ILoopsAdapter): default=u'', required=False) - options = schema.List( - title=_(u'Options'), - description=_(u'Additional settings.'), - value_type=schema.TextLine(), - default=[], - required=False) - # storage = schema.Choice() diff --git a/knowledge/qualification/base.py b/knowledge/qualification/base.py index 32c9910..8a667e2 100644 --- a/knowledge/qualification/base.py +++ b/knowledge/qualification/base.py @@ -26,6 +26,7 @@ from zope.component import adapts from zope.interface import implementer, implements from loops.common import AdapterBase +from loops.interfaces import IConcept from loops.knowledge.qualification.interfaces import ICompetence from loops.type import TypeInterfaceSourceList diff --git a/knowledge/qualification/configure.zcml b/knowledge/qualification/configure.zcml index d8cbc74..dea246f 100644 --- a/knowledge/qualification/configure.zcml +++ b/knowledge/qualification/configure.zcml @@ -4,7 +4,14 @@ i18n_domain="loops"> + factory="loops.knowledge.qualification.base.Competence" + trusted="True" /> + + + + diff --git a/knowledge/qualification/interfaces.py b/knowledge/qualification/interfaces.py index 85a002a..87b3e2d 100644 --- a/knowledge/qualification/interfaces.py +++ b/knowledge/qualification/interfaces.py @@ -23,11 +23,11 @@ Interfaces for knowledge management and elearning with loops. from zope.interface import Interface, Attribute from zope import interface, component, schema -from loops.interfaces import IConceptSchema +from loops.interfaces import IConceptSchema, ILoopsAdapter from loops.util import _ -class ICompetence(IConceptSchema): +class ICompetence(ILoopsAdapter): """ The competence of a person. Maybe assigned to the person via a 'knows' relation or diff --git a/knowledge/survey/base.py b/knowledge/survey/base.py index a4a5d57..cacdcc7 100644 --- a/knowledge/survey/base.py +++ b/knowledge/survey/base.py @@ -62,7 +62,7 @@ class QuestionGroup(AdapterBase, QuestionGroup): _contextAttributes = list(IQuestionGroup) _adapterAttributes = AdapterBase._adapterAttributes + ( - 'questionnaire', 'questions', 'feedbackItems',) + 'questionnaire', 'questions', 'feedbackItems') _noexportAttributes = _adapterAttributes @property @@ -109,9 +109,6 @@ class Question(AdapterBase, Question): def questionnaire(self): return self.questionGroup.questionnaire - def __hash__(self): - return hash(self.context) - class FeedbackItem(AdapterBase, FeedbackItem): @@ -125,4 +122,3 @@ class FeedbackItem(AdapterBase, FeedbackItem): @property def text(self): return self.context.description - diff --git a/knowledge/survey/browser.py b/knowledge/survey/browser.py index 657c3e9..d9075d6 100644 --- a/knowledge/survey/browser.py +++ b/knowledge/survey/browser.py @@ -21,26 +21,40 @@ Definition of view classes and other browser related stuff for surveys and self-assessments. """ +import csv +from cStringIO import StringIO from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy +from zope.i18n import translate from cybertools.knowledge.survey.questionnaire import Response +from cybertools.util.date import formatTimeStamp from loops.browser.concept import ConceptView +from loops.browser.node import NodeView from loops.common import adapted +from loops.knowledge.survey.response import Responses from loops.organize.party import getPersonForUser +from loops.util import getObjectForUid +from loops.util import _ template = ViewPageTemplateFile('view_macros.pt') class SurveyView(ConceptView): - tabview = 'index.html' data = None + errors = None @Lazy def macro(self): + self.registerDojo() return template.macros['survey'] + @Lazy + def tabview(self): + if self.editable: + return 'index.html' + def results(self): result = [] response = None @@ -52,22 +66,114 @@ class SurveyView(ConceptView): if key.startswith('question_'): uid = key[len('question_'):] question = adapted(self.getObjectForUid(uid)) - value = int(value) - self.data[uid] = value - response.values[question] = value - # TODO: store self.data in track - # else: - # get response from track + if value != 'none': + value = int(value) + self.data[uid] = value + response.values[question] = value + Responses(self.context).save(self.data) + self.errors = self.check(response) + if self.errors: + return [] if response is not None: result = response.getGroupedResult() return [dict(category=r[0].title, text=r[1].text, score=int(round(r[2] * 100))) for r in result] - def getValues(self, question): - setting = 0 - if self.data is not None: - setting = self.data.get(question.uid) or 0 - return [dict(value=i, checked=(i == setting)) - for i in range(question.answerRange)] + def check(self, response): + errors = [] + values = response.values + for qu in self.adapted.questions: + if qu.required and qu not in values: + errors.append('Please answer the obligatory questions.') + break + qugroups = {} + for qugroup in self.adapted.questionGroups: + qugroups[qugroup] = 0 + for qu in values: + qugroups[qu.questionGroup] += 1 + for qugroup, count in qugroups.items(): + minAnswers = qugroup.minAnswers + if minAnswers in (u'', None): + minAnswers = len(qugroup.questions) + if count < minAnswers: + errors.append('Please answer the minimum number of questions.') + break + return errors + + def getInfoText(self, qugroup): + lang = self.languageInfo.language + text = qugroup.description + info = None + if qugroup.minAnswers in (u'', None): + info = translate(_(u'Please answer all questions.'), target_language=lang) + elif qugroup.minAnswers > 0: + info = translate(_(u'Please answer at least $minAnswers questions.', + mapping=dict(minAnswers=qugroup.minAnswers)), + target_language=lang) + if info: + text = u'%s
(%s)' % (text, info) + return text + + def getValues(self, question): + setting = None + if self.data is None: + self.data = Responses(self.context).load() + if self.data: + setting = self.data.get(question.uid) + noAnswer = [dict(value='none', checked=(setting == None), + radio=(not question.required))] + return noAnswer + [dict(value=i, checked=(setting == i), radio=True) + for i in reversed(range(question.answerRange))] + + +class SurveyCsvExport(NodeView): + + encoding = 'ISO8859-15' + + def encode(self, text): + text.encode(self.encoding) + + @Lazy + def questions(self): + result = [] + for idx1, qug in enumerate(adapted(self.virtualTargetObject).questionGroups): + for idx2, qu in enumerate(qug.questions): + result.append((idx1, idx2, qug, qu)) + return result + + @Lazy + def columns(self): + infoCols = ['Name', 'Timestamp'] + dataCols = ['%02i-%02i' % (item[0], item[1]) for item in self.questions] + return infoCols + dataCols + + def getRows(self): + for tr in Responses(self.virtualTargetObject).getAllTracks(): + p = adapted(getObjectForUid(tr.userName)) + name = p and p.title or u'???' + ts = formatTimeStamp(tr.timeStamp) + cells = [tr.data.get(qu.uid, -1) + for (idx1, idx2, qug, qu) in self.questions] + yield [name, ts] + cells + + def __call__(self): + f = StringIO() + writer = csv.writer(f, delimiter=',') + writer.writerow(self.columns) + for row in self.getRows(): + writer.writerow(row) + text = f.getvalue() + self.setDownloadHeader(text) + return text + + def setDownloadHeader(self, text): + response = self.request.response + filename = 'survey_data.csv' + response.setHeader('Content-Disposition', + 'attachment; filename=%s' % filename) + response.setHeader('Cache-Control', '') + response.setHeader('Pragma', '') + response.setHeader('Content-Length', len(text)) + response.setHeader('Content-Type', 'text/csv') diff --git a/knowledge/survey/configure.zcml b/knowledge/survey/configure.zcml index 81c33d3..423889d 100644 --- a/knowledge/survey/configure.zcml +++ b/knowledge/survey/configure.zcml @@ -7,19 +7,56 @@ + provides="loops.knowledge.survey.interfaces.IQuestionnaire" + trusted="True" /> + + + + + provides="loops.knowledge.survey.interfaces.IQuestionGroup" + trusted="True" /> + + + + + provides="loops.knowledge.survey.interfaces.IQuestion" + trusted="True" /> + + + + + provides="loops.knowledge.survey.interfaces.IFeedbackItem" + trusted="True" /> + + + + + + + + + + + @@ -31,4 +68,9 @@ factory="loops.knowledge.survey.browser.SurveyView" permission="zope.View" /> + + diff --git a/knowledge/survey/interfaces.py b/knowledge/survey/interfaces.py index ea15928..e2adffc 100644 --- a/knowledge/survey/interfaces.py +++ b/knowledge/survey/interfaces.py @@ -24,7 +24,7 @@ from zope.interface import Interface, Attribute from zope import interface, component, schema from cybertools.knowledge.survey import interfaces -from loops.interfaces import IConceptSchema +from loops.interfaces import IConceptSchema, ILoopsAdapter from loops.util import _ @@ -38,16 +38,43 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire): default=4, required=True) + feedbackHeader = schema.Text( + title=_(u'Feedback Header'), + description=_(u'Text that will appear at the top of the feedback page.'), + default=u'', + missing_value=u'', + required=False) + + feedbackFooter = schema.Text( + title=_(u'Feedback Footer'), + description=_(u'Text that will appear at the end of the feedback page.'), + default=u'', + missing_value=u'', + required=False) + class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup): """ A group of questions within a questionnaire. """ + minAnswers = schema.Int( + title=_(u'Minimum Number of Answers'), + description=_(u'Minumum number of questions that have to be answered. ' + 'Empty means all questions have to be answered.'), + default=None, + required=False) + class IQuestion(IConceptSchema, interfaces.IQuestion): """ A single question within a questionnaire. """ + required = schema.Bool( + title=_(u'Required'), + description=_(u'Question must be answered.'), + default=False, + required=False) + revertAnswerOptions = schema.Bool( title=_(u'Negative'), description=_(u'Value inversion: High selection means low value.'), diff --git a/knowledge/survey/response.py b/knowledge/survey/response.py index f2f1733..3841c4f 100644 --- a/knowledge/survey/response.py +++ b/knowledge/survey/response.py @@ -29,20 +29,34 @@ from loops.knowledge.survey.interfaces import IResponse, IResponses from loops.organize.tracking.base import BaseRecordManager +class Responses(BaseRecordManager): + + implements(IResponses) + + storageName = 'survey_responses' + + def __init__(self, context): + self.context = context + + def save(self, data): + if self.personId: + self.storage.saveUserTrack(self.uid, 0, self.personId, data, + update=True, overwrite=True) + + def load(self): + if self.personId: + tracks = self.storage.getUserTracks(self.uid, 0, self.personId) + if tracks: + return tracks[0].data + return {} + + def getAllTracks(self): + return self.storage.query(taskId=self.uid) + + class Response(Track): - """ A survey response. - """ implements(IResponse) typeName = 'Response' - typeInterface = IResponse - - -class Responses(BaseRecordManager): - """ A tracking storage adapter for survey responses. - """ - - implements(IResponses) - adapts(ITrackingStorage) diff --git a/knowledge/survey/view_macros.pt b/knowledge/survey/view_macros.pt index 3956120..9e7593c 100644 --- a/knowledge/survey/view_macros.pt +++ b/knowledge/survey/view_macros.pt @@ -3,11 +3,18 @@ - + tal:define="feedback item/results; + errors item/errors"> + + + +

Feedback

- +
+
@@ -19,42 +26,74 @@
Category Response
-
+ +
+
+
+

Questionnaire

+
+
+ +
+
+
+ + + + + + + + + + + + + +
 
+ +
+ +
+
No answerFully appliesDoes not apply
+ + + *** + +
+ + +
-

Questionnaire

-
- - - - - - - - -
- - - - - -
Does not applyFully applies
-
- -   -   - -
-
- -
diff --git a/layout/base.py b/layout/base.py index 8a0e6eb..9cae325 100644 --- a/layout/base.py +++ b/layout/base.py @@ -158,4 +158,5 @@ class TargetLayoutInstance(NodeLayoutInstance): target = self.viewAnnotations.get('target') if target is None: target = adapted(self.context.target) + #self.viewAnnotations['target'] = target # TODO: has to be tested! return target diff --git a/layout/browser/base.py b/layout/browser/base.py index a6494d2..cef66da 100644 --- a/layout/browser/base.py +++ b/layout/browser/base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2009 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Base classes for layout-based views. - -$Id$ """ from zope.app.security.interfaces import IUnauthenticatedPrincipal @@ -29,6 +27,7 @@ from zope.proxy import removeAllProxies from zope.security.proxy import removeSecurityProxy from zope.traversing.browser import absoluteURL +from cybertools.meta.interfaces import IOptions from cybertools.util import format from loops.common import adapted from loops.i18n.browser import LanguageInfo @@ -170,3 +169,7 @@ class BaseView(object): def getMetaDescription(self): return self.context.title + @Lazy + def globalOptions(self): + return IOptions(self.loopsRoot) + diff --git a/layout/browser/node.py b/layout/browser/node.py index e2a7167..46435b1 100644 --- a/layout/browser/node.py +++ b/layout/browser/node.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Layout node views. - -$Id$ """ from zope.app.security.interfaces import IUnauthenticatedPrincipal @@ -66,6 +64,9 @@ class LayoutNodeView(Page, BaseView): if self.target is not None: targetView = component.getMultiAdapter((self.target, self.request), name='layout') - return ' - '.join((self.context.title, targetView.title)) + parts = [self.context.title, targetView.title] else: - return self.context.title + parts = [self.context.title] + if self.globalOptions('reverseHeadTitle'): + parts.reverse() + return ' - '.join(parts) diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index 0bf32ea..403cceb 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 74f7395..46c2e6b 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-03-07 12:00 CET\n" +"PO-Revision-Date: 2013-07-15 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -86,6 +86,9 @@ msgstr "Thema bearbeiten..." msgid "Modify topic." msgstr "Thema ändern" +msgid "Please correct the indicated errors." +msgstr "Bitte berichtigen Sie die angezeigten Fehler." + # blog msgid "Edit Blog Post..." @@ -175,11 +178,29 @@ msgstr "Glossareintrag anlegen." msgid "Answer Range" msgstr "Abstufung Bewertungen" +msgid "Feedback Footer" +msgstr "Auswertungs-Hinweis" + +msgid "Text that will appear at the end of the feedback page." +msgstr "Text, der am Ende der Auswertungsseite erscheinen soll." + msgid "Number of items (answer options) to select from." msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann." -msgid "Negativ" -msgstr "Negativbewertung" +msgid "Minimum Number of Answers" +msgstr "Mindestanzahl an Antworten" + +msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered." +msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden." + +msgid "Required" +msgstr "Pflichtfrage" + +msgid "Question must be answered." +msgstr "Frage muss unbedingt beantwortet werden." + +msgid "Negative" +msgstr "Negative Polarität" msgid "Value inversion: High selection means low value." msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." @@ -196,27 +217,54 @@ msgstr "Kategorie" msgid "Response" msgstr "Beurteilung" +msgid "No answer" +msgstr "Keine Antwort" + msgid "Does not apply" msgstr "Trifft nicht zu" msgid "Fully applies" msgstr "Trifft voll zu" +msgid "survey_value_none" +msgstr "Keine Antwort" + msgid "survey_value_0" -msgstr "trifft für unser Unternehmen überhaupt nicht zu" +msgstr "Trifft für unser Unternehmen überhaupt nicht zu" msgid "survey_value_1" -msgstr "trifft eher nicht zu" +msgstr "Trifft eher nicht zu" msgid "survey_value_2" -msgstr "trifft eher zu" +msgstr "Trifft eher zu" msgid "survey_value_3" -msgstr "trifft für unser Unternehmen voll und ganz zu" +msgstr "Trifft für unser Unternehmen voll und ganz zu" msgid "Evaluate Questionnaire" msgstr "Fragebogen auswerten" +msgid "Reset Responses Entered" +msgstr "Eingaben zurücksetzen" + +msgid "Back to Questionnaire" +msgstr "Zurück zum Fragebogen" + +msgid "Please answer at least $minAnswers questions." +msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen." + +msgid "Please answer all questions." +msgstr "Bitte beantworten Sie alle Fragen." + +msgid "Please answer the obligatory questions." +msgstr "Bitte beantworten Sie die Pflichtfragen." + +msgid "Please answer the minimum number of questions." +msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe." + +msgid "Obligatory question, must be answered" +msgstr "Pflichtfrage, muss beantwortet werden" + # competence (qualification) msgid "Validity Period (Months)" @@ -495,6 +543,9 @@ msgstr "Unterbegriffe" msgid "Resources" msgstr "Ressourcen" +msgid "Text Elements" +msgstr "Texte" + msgid "Title" msgstr "Titel" @@ -660,6 +711,9 @@ msgstr "Zugeordnete Begriffe" msgid "more..." msgstr "Mehr..." +msgid "More..." +msgstr "Mehr..." + msgid "Versioning" msgstr "Versionierung" @@ -708,12 +762,27 @@ msgstr "Teilnehmerregistrierung" msgid "Register" msgstr "Benutzer registrieren" +msgid "Register new member" +msgstr "Neu registrieren" + +msgid "Login name already taken." +msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben." + msgid "Your old password was not entered correctly." msgstr "Sie haben Ihr altes Passwort nicht korrekt eingegeben." msgid "Password and password confirmation do not match." msgstr "Die Passwort-Wiederholung stimmt nicht mit dem eingegebenen Passwort überein." +msgid "confirmation_mail_subject" +msgstr "Benutzer-Registrierung" + +msgid "confirmation_mail_text" +msgstr "Bitte clicken Sie auf den folgenden Link, um die Anmeldung abzuschließen." + +msgid "The user account has been created." +msgstr "Ihr Benutzerkonto wurde eingerichtet." + msgid "Your password has been changed." msgstr "Ihr Passwort wurde geändert." @@ -893,6 +962,9 @@ msgstr "Kalender" msgid "Work Items" msgstr "Aktivitäten" +msgid "Work Items for $title" +msgstr "Aktivitäten für $title" + msgid "Day" msgstr "Tag" @@ -956,14 +1028,26 @@ msgid "Restrict to objects with certain states" msgstr "Auf Objekte mit bestimmtem Status beschränken" msgid "Workflow" -msgstr "Statusdefinition/Workflow" +msgstr "Workflow" msgid "States" msgstr "Statuswerte" +msgid "States Definition" +msgstr "Workflowdefinition" + +msgid "State Transition" +msgstr "Workflow-Statusänderung" + +msgid "Transition" +msgstr "Aktion" + msgid "State information for $definition: $title" msgstr "Status ($definition): $title" +msgid "Available Transitions" +msgstr "Übergänge" + msgid "classification_quality" msgstr "Klassifizierung" @@ -976,6 +1060,12 @@ msgstr "Aufgabe" msgid "publishable_task" msgstr "Aufgabe/Zugriff" +msgid "label_transition_comments" +msgstr "Bemerkung" + +msgid "desc_transition_comments" +msgstr "Notizen zum Statusübergang." + # state names msgid "accepted" diff --git a/organize/README.txt b/organize/README.txt index e013d9f..110efe6 100644 --- a/organize/README.txt +++ b/organize/README.txt @@ -185,7 +185,7 @@ sure that a principal object can be served by a corresponding factory): ... 'lastName': u'Sawyer', ... 'firstName': u'Tom', ... 'email': u'tommy@sawyer.com', - ... 'action': 'update',} + ... 'form.action': 'update',} and register it. diff --git a/organize/browser/configure.zcml b/organize/browser/configure.zcml index 91d40f1..e351efa 100644 --- a/organize/browser/configure.zcml +++ b/organize/browser/configure.zcml @@ -27,6 +27,18 @@ class="loops.organize.browser.member.MemberRegistration" permission="zope.View" /> + + + + 0: + # show form again + return True + login = form.get('loginName') + regMan = IMemberRegistrationManager(self.context.getLoopsRoot()) + pw = generateName() + email = form.get('email') + try: + result = regMan.register(login, pw, + form.get('lastName'), form.get('firstName'), + email=email,) + except ValueError, e: + fi = formState.fieldInstances['loginName'] + fi.setError('duplicate_loginname', self.formErrors) + formState.severity = max(formState.severity, fi.severity) + return True + self.object = result + person = result.context + pa = self.getPrincipalAnnotation( + getPrincipalForUserId(adapted(person).getUserId())) + pa['id'] = generateName() + pa['timestamp'] = datetime.utcnow() + self.notifyEmail(login, email, pa['id']) + if self.feedbackUrl: + self.request.response.redirect(self.feedbackUrl) + else: + msg = self.message + self.request.response.redirect('%s?loops.message=%s' % (self.url, msg)) + return False + + def notifyEmail(self, userid, recipient, id): + baseUrl = absoluteURL(self.context.getMenu(), self.request) + url = u'%s/selfservice_confirmation.html?login=%s&id=%s' % ( + baseUrl, userid, id,) + recipients = [recipient] + subject = _(u'confirmation_mail_subject') + name = '.'.join((self.text_names_prefix, self.email_key)) + text = self.resourceManager.get(name) + if text: + message = (text.data % url).encode('UTF-8') + subject = text.description or subject + else: + message = _(u'confirmation_mail_text') + u':\n\n' + message = (message + url).encode('UTF-8') + senderInfo = self.globalOptions('email.sender') + sender = senderInfo and senderInfo[0] or 'info@loops.cy55.de' + sender = sender.encode('UTF-8') + msg = MIMEText(message, 'plain', 'utf-8') + msg['Subject'] = subject.encode('UTF-8') + msg['From'] = sender + msg['To'] = ', '.join(recipients) + mailhost = component.getUtility(IMailDelivery, 'Mail') + mailhost.send(sender, recipients, msg.as_string()) + + +class ConfirmMemberRegistration(BaseMemberRegistration, Form): + + permissions_key = u'secure_registration.permissions' + roles_key = u'secure_registration.roles' + info_key = 'confirm_info' + feedback_key = 'confirm_feedback' + email_key = 'confirm_email' + + form_action = 'confirm_registration' + + @Lazy + def macro(self): + return organize_macros.macros['confirm'] + + @Lazy + def data(self): + form = self.request.form + return dict(loginName=form.get('login'), id=form.get('id')) + + @Lazy + def schema(self): + schema = super(ConfirmMemberRegistration, self).schema + schema.fields.remove('salutation') + schema.fields.remove('academicTitle') + schema.fields.remove('birthDate') + schema.fields.remove('phoneNumbers') + schema.fields.remove('loginName') + schema.fields.remove('firstName') + schema.fields.remove('lastName') + schema.fields.remove('email') + return schema + + def update(self): + form = self.request.form + if form.get('form.action') != 'confirm_registration': + return True + if not form.get('login'): + return True + regMan = IMemberRegistrationManager(self.context.getLoopsRoot()) + prefix = regMan.getPrincipalFolderFromOption().prefix + userId = prefix + form['login'] + principal = getPrincipalForUserId(userId) + pa = self.getPrincipalAnnotation(principal) + id = form.get('id') + if not id or id != pa.get('id'): + return True + instance = component.getAdapter(self.object, IInstance, name='editor') + instance.template = self.schema + self.formState = formState = instance.applyTemplate(data=form, + fieldHandlers=self.fieldHandlers) + #formState = self.formState = self.validate(form) + if formState.severity > 0: + return True + pw = form.get('password') + pwConfirm = form.get('passwordConfirm') + if pw != pwConfirm: + fi = formState.fieldInstances['password'] + fi.setError('confirm_nomatch', self.formErrors) + formState.severity = max(formState.severity, fi.severity) + return True + del pa['id'] + del pa['timestamp'] + ip = getInternalPrincipal(userId) + ip.setPassword(pw) + if self.feedbackUrl: + self.request.response.redirect(self.feedbackUrl) + else: + url = '%s?loops.message=%s' % (self.url, self.message) + self.request.response.redirect(url) + return False + + class PasswordChange(NodeView, Form): interface = IPasswordChange diff --git a/organize/browser/view_macros.pt b/organize/browser/view_macros.pt index 9eb86b9..93ef80e 100644 --- a/organize/browser/view_macros.pt +++ b/organize/browser/view_macros.pt @@ -1,5 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Login Name +
+ + +
+
+
+ + diff --git a/organize/member.py b/organize/member.py index 9753bd6..ab2aded 100644 --- a/organize/member.py +++ b/organize/member.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -62,19 +62,29 @@ class MemberRegistrationManager(object): def __init__(self, context): self.context = context + @Lazy + def personType(self): + concepts = self.context.getConceptManager() + return adapted(concepts[self.person_typeName]) + + def getPrincipalFolderFromOption(self): + options = IOptions(self.personType) + pfName = options(self.principalfolder_key, + (self.default_principalfolder,))[0] + return getPrincipalFolder(self.context, pfName) + def register(self, userId, password, lastName, firstName=u'', groups=[], useExisting=False, pfName=None, **kw): - concepts = self.context.getConceptManager() - personType = adapted(concepts[self.person_typeName]) - options = IOptions(personType) + options = IOptions(self.personType) if pfName is None: pfName = options(self.principalfolder_key, (self.default_principalfolder,))[0] - self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting) - if len(groups)==0: + self.createPrincipal(pfName, userId, password, lastName, firstName, + useExisting=useExisting) + if not groups: groups = options(self.groups_key, ()) self.setGroupsForPrincipal(pfName, userId, groups=groups) - self.createPersonForPrincipal(pfName, userId, lastName, firstName, + return self.createPersonForPrincipal(pfName, userId, lastName, firstName, useExisting, **kw) def createPrincipal(self, pfName, userId, password, lastName, diff --git a/organize/stateful/base.py b/organize/stateful/base.py index fc2d942..be2a257 100644 --- a/organize/stateful/base.py +++ b/organize/stateful/base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Basic implementations for stateful objects and adapters. - -$Id$ """ from zope.app.catalog.interfaces import ICatalog @@ -27,6 +25,7 @@ from zope.cachedescriptors.property import Lazy from zope import component from zope.component import adapts, adapter +from cybertools.composer.schema.field import Field from cybertools.meta.interfaces import IOptions from cybertools.stateful.base import Stateful as BaseStateful from cybertools.stateful.base import StatefulAdapter, IndexInfo @@ -34,6 +33,7 @@ from cybertools.stateful.interfaces import IStatesDefinition, ITransitionEvent from loops.common import adapted from loops.interfaces import ILoopsObject, IConcept, IResource from loops import util +from loops.util import _ class Stateful(BaseStateful): @@ -93,3 +93,10 @@ def handleTransition(obj, event): if next != previous: cat = component.getUtility(ICatalog) cat.index_doc(int(util.getUidForObject(obj)), obj) + + +# predefined fields for transition forms + +commentsField = Field('comments', _(u'label_transition_comments'), 'textarea', + description=_(u'desc_transition_comments'), + nostore=True) diff --git a/organize/stateful/browser.py b/organize/stateful/browser.py index ae57c87..fea8bf3 100644 --- a/organize/stateful/browser.py +++ b/organize/stateful/browser.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ -23,12 +23,16 @@ Views and actions for states management. from zope import component from zope.app.pagetemplate import ViewPageTemplateFile from zope.cachedescriptors.property import Lazy +from zope.event import notify from zope.i18n import translate +from zope.lifecycleevent import ObjectModifiedEvent, Attributes from cybertools.browser.action import Action, actions +from cybertools.composer.schema.schema import Schema from cybertools.stateful.interfaces import IStateful, IStatesDefinition from loops.browser.common import BaseView from loops.browser.concept import ConceptView +from loops.browser.form import ObjectForm, EditObject from loops.expert.query import And, Or, State, Type, getObjects from loops.expert.browser.search import search_template from loops.security.common import checkPermission @@ -43,6 +47,16 @@ statefulActions = ('classification_quality', 'publishable_task',) +def registerStatesPortlet(controller, view, statesDefs, + region='portlet_right', priority=98): + cm = controller.macros + stfs = [component.getAdapter(view.context, IStateful, name=std) + for std in statesDefs] + cm.register(region, 'states', title=_(u'Workflow'), + subMacro=template.macros['portlet_states'], + priority=priority, info=view, stfs=stfs) + + class StateAction(Action): url = None @@ -67,22 +81,92 @@ class StateAction(Action): @Lazy def icon(self): - icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color - return 'cybertools.icons/' + icon + return self.stateObject.stateIcon + #icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color + #return 'cybertools.icons/' + icon for std in statefulActions: actions.register('state.' + std, 'object', StateAction, - definition = std, + definition=std, cssClass='icon-action', ) +class ChangeStateBase(object): + + @Lazy + def stateful(self): + return component.getAdapter(self.view.virtualTargetObject, IStateful, + name=self.definition) + + @Lazy + def definition(self): + return self.request.form.get('stdef') or u'' + + @Lazy + def action(self): + return self.request.form.get('action') or u'' + + @Lazy + def transition(self): + return self.stateful.getStatesDefinition().transitions[self.action] + + @Lazy + def stateObject(self): + return self.stateful.getStateObject() + + @Lazy + def schema(self): + schema = self.transition.schema + if schema is None: + return Schema() + else: + schema.manager = self + schema.request = self.request + return schema + + +class ChangeStateForm(ChangeStateBase, ObjectForm): + + form_action = 'change_state_action' + data = {} + + @Lazy + def macro(self): + return template.macros['change_state'] + + @Lazy + def title(self): + return self.virtualTargetObject.title + + +class ChangeState(ChangeStateBase, EditObject): + + def update(self): + formData = self.request.form + # store data in target object (unless field.nostore) + self.object = self.target + formState = self.instance.applyTemplate(data=formData) + # TODO: check formState + # track all fields + trackData = dict(transition=self.action) + for f in self.fields: + if f.readonly: + continue + name = f.name + fi = formState.fieldInstances[name] + rawValue = fi.getRawValue(formData, name, u'') + trackData[name] = fi.unmarshall(rawValue) + self.stateful.doTransition(self.action) + notify(ObjectModifiedEvent(self.view.virtualTargetObject, trackData)) + return True + + #class StateQuery(ConceptView): class StateQuery(BaseView): template = template - form_action = 'execute_search_action' @Lazy diff --git a/organize/stateful/configure.zcml b/organize/stateful/configure.zcml index 7833208..30ad487 100644 --- a/organize/stateful/configure.zcml +++ b/organize/stateful/configure.zcml @@ -77,7 +77,7 @@ set_schema="cybertools.stateful.interfaces.IStateful" /> - + + + + + diff --git a/organize/stateful/task.py b/organize/stateful/task.py index 0721bc4..dea88d4 100644 --- a/organize/stateful/task.py +++ b/organize/stateful/task.py @@ -26,12 +26,15 @@ 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 _ def setPermissionsForRoles(settings): @@ -42,6 +45,10 @@ def setPermissionsForRoles(settings): return setSecurity +defaultSchema = Schema(commentsField, + name='change_state') + + @implementer(IStatesDefinition) def taskStates(): return StatesDefinition('task_states', @@ -55,10 +62,11 @@ def taskStates(): color='x'), State('archived', 'archived', ('reopen',), color='grey'), - Transition('release', 'release', 'active'), - Transition('finish', 'finish', 'finished'), - Transition('cancel', 'cancel', 'cancelled'), - Transition('reopen', 're-open', 'draft'), + Transition('release', 'release', 'active', schema=defaultSchema), + Transition('finish', 'finish', 'finished', schema=defaultSchema), + Transition('cancel', 'cancel', 'cancelled', schema=defaultSchema), + Transition('reopen', 're-open', 'draft', schema=defaultSchema), + Transition('archive', 'archive', 'archived', schema=defaultSchema), initialState='draft') diff --git a/organize/stateful/view_macros.pt b/organize/stateful/view_macros.pt index 6dfad17..b99d5dc 100644 --- a/organize/stateful/view_macros.pt +++ b/organize/stateful/view_macros.pt @@ -68,4 +68,82 @@ + +
+
+ States Definition + +
+
+ State: + + +
+
Available Transitions: + +
+
+
+ + + +
+
+
+

State Transition - + +

+
+ State: + - + Transition: + +
+ + + +
+
+ + +
+
+ +
+
+
+
+ + + + +
+
+
+
+ + diff --git a/organize/tracking/base.py b/organize/tracking/base.py index ab3c2c6..645e091 100644 --- a/organize/tracking/base.py +++ b/organize/tracking/base.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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,10 +18,9 @@ """ Base class(es) for track/record managers. - -$Id$ """ +from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.cachedescriptors.property import Lazy from cybertools.meta.interfaces import IOptions @@ -46,6 +45,10 @@ class BaseRecordManager(object): def loopsRoot(self): return self.context.getLoopsRoot() + @Lazy + def uid(self): + return util.getUidForObject(self.context) + @Lazy def storage(self): records = self.loopsRoot.getRecordManager() @@ -63,6 +66,8 @@ class BaseRecordManager(object): else: principal = getPrincipalForUserId(userId, context=self.context) if principal is not None: + if IUnauthenticatedPrincipal.providedBy(principal): + return None person = getPersonForUser(self.context, principal=principal) if person is None: return principal.id diff --git a/organize/tracking/change.py b/organize/tracking/change.py index b3e3057..d9e8bbd 100644 --- a/organize/tracking/change.py +++ b/organize/tracking/change.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Recording changes to loops objects. - -$Id$ """ from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent @@ -53,6 +51,9 @@ class ChangeManager(BaseRecordManager): @Lazy def valid(self): + req = util.getRequest() + if req and req.form.get('organize.suppress_tracking'): + return False return (not (self.context is None or self.storage is None or self.personId is None) @@ -70,6 +71,12 @@ class ChangeManager(BaseRecordManager): if relation is not None: data['predicate'] = util.getUidForObject(relation.predicate) data['second'] = util.getUidForObject(relation.second) + event = kw.get('event') + if event is not None: + desc = getattr(event, 'descriptions', ()) + for item in desc: + if isinstance(item, dict): + data.update(item) if update: self.storage.updateTrack(last, data) else: @@ -90,16 +97,18 @@ class ChangeRecord(Track): @adapter(ILoopsObject, IObjectModifiedEvent) def recordModification(obj, event): - ChangeManager(obj).recordModification() + ChangeManager(obj).recordModification(event=event) @adapter(ILoopsObject, IObjectAddedEvent) def recordAdding(obj, event): - ChangeManager(obj).recordModification('add') + ChangeManager(obj).recordModification('add', event=event) @adapter(ILoopsObject, IAssignmentEvent) def recordAssignment(obj, event): - ChangeManager(obj).recordModification('assign', relation=event.relation) + ChangeManager(obj).recordModification('assign', + event=event, relation=event.relation) @adapter(ILoopsObject, IDeassignmentEvent) def recordDeassignment(obj, event): - ChangeManager(obj).recordModification('deassign', relation=event.relation) + ChangeManager(obj).recordModification('deassign', + event=event, relation=event.relation) diff --git a/organize/work/browser.py b/organize/work/browser.py index 564f90b..3a78a10 100644 --- a/organize/work/browser.py +++ b/organize/work/browser.py @@ -43,13 +43,14 @@ from loops.browser.concept import ConceptView from loops.browser.form import ObjectForm, EditObject from loops.browser.node import NodeView from loops.common import adapted +from loops.organize.interfaces import IPerson from loops.organize.party import getPersonForUser from loops.organize.stateful.browser import StateAction from loops.organize.tracking.browser import BaseTrackView from loops.organize.tracking.report import TrackDetails from loops.organize.work.base import WorkItem from loops.security.common import canAccessObject, canListObject, canWriteObject -from loops.security.common import checkPermission +from loops.security.common import canAccessRestricted, checkPermission from loops import util from loops.util import _ @@ -228,6 +229,10 @@ class BaseWorkItemsView(object): def macro(self): return self.work_macros['workitems_query'] + @Lazy + def title(self): + return _(u'Work Items for $title', mapping=dict(title=self.context.title)) + @Lazy def workItems(self): rm = self.loopsRoot.getRecordManager() @@ -312,19 +317,25 @@ class RelatedTaskWorkItems(AllWorkItems): class PersonWorkItems(BaseWorkItemsView, ConceptView): - """ A query view showing work items for a person, the query's parent. + """ A view showing work items for a person or the context object's parents. """ columns = set(['Task', 'Title', 'Day', 'Start', 'End', 'Duration', 'Info']) + def checkPermissions(self): + return canAccessRestricted(self.context) + def getCriteria(self): return self.baseCriteria def listWorkItems(self): criteria = self.getCriteria() - for target in self.context.getParents([self.defaultPredicate]): - un = criteria.setdefault('userName', []) - un.append(util.getUidForObject(target)) + un = criteria.setdefault('userName', []) + if IPerson.providedBy(self.adapted): + un.append(util.getUidForObject(self.context)) + else: + for target in self.context.getParents([self.defaultPredicate]): + un.append(util.getUidForObject(target)) return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) diff --git a/query.py b/query.py index 1c0ddce..eb6fafb 100644 --- a/query.py +++ b/query.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Query management stuff. - -$Id$ """ from BTrees.IOBTree import IOBTree @@ -33,6 +31,7 @@ from zope.cachedescriptors.property import Lazy from cybertools.typology.interfaces import IType from loops.common import AdapterBase from loops.interfaces import IConcept, IConceptSchema, ILoopsAdapter +from loops.interfaces import IOptions from loops.security.common import canListObject from loops.type import TypeInterfaceSourceList from loops.versioning.util import getVersion @@ -182,7 +181,7 @@ class ConceptQuery(BaseQuery): # QueryConcept: concept objects that allow querying the database. -class IQueryConcept(IConceptSchema, ILoopsAdapter): +class IQueryConcept(IConceptSchema, ILoopsAdapter, IOptions): """ The schema for the query type. """ @@ -194,13 +193,6 @@ class IQueryConcept(IConceptSchema, ILoopsAdapter): default=u'', required=False) - options = schema.List( - title=_(u'Options'), - description=_(u'Additional settings.'), - value_type=schema.TextLine(), - default=[], - required=False) - class QueryConcept(AdapterBase): diff --git a/security/common.py b/security/common.py index 30c1c3c..f1904ec 100644 --- a/security/common.py +++ b/security/common.py @@ -74,6 +74,9 @@ def canListObject(obj, noCheck=False): return True return canAccess(obj, 'title') +def canAccessRestricted(obj): + return checkPermission('loops.ViewRestricted', obj) + def canWriteObject(obj): return canWrite(obj, 'title') or canAssignAsParent(obj) diff --git a/type.py b/type.py index 4e5f783..0f9ffee 100644 --- a/type.py +++ b/type.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2006 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 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 @@ """ Type management stuff. - -$Id$ """ from zope import component, schema @@ -28,12 +26,13 @@ from zope.interface import implements from zope.cachedescriptors.property import Lazy from zope.dottedname.resolve import resolve from zope.security.proxy import removeSecurityProxy -from zope.traversing.api import getName +from zope.traversing.api import getName, getPath from cybertools.typology.type import BaseType, TypeManager from cybertools.typology.interfaces import ITypeManager from loops.interfaces import ILoopsObject, IConcept, IResource from loops.interfaces import ITypeConcept +from loops.interfaces import IOptions from loops.interfaces import IResourceAdapter, IFile, IExternalFile, IImage from loops.interfaces import ITextDocument, INote from loops.concept import Concept @@ -49,10 +48,15 @@ class LoopsType(BaseType): #document=Document) containerMapping = dict(concept='concepts', resource='resources') + isForeignReference = False + @Lazy def title(self): tp = self.typeProvider - return tp is None and u'Unknown Type' or tp.title + title = tp is None and u'Unknown Type' or tp.title + if self.isForeignReference: + title += (' (Site: %s)' % getName(self.root)) + return title @Lazy def token(self): @@ -63,7 +67,11 @@ class LoopsType(BaseType): def tokenForSearch(self): tp = self.typeProvider typeName = tp is None and 'unknown' or str(getName(tp)) - return ':'.join(('loops', self.qualifiers[0], typeName,)) + if self.isForeignReference: + root = getPath(self.root) + else: + root = 'loops' + return ':'.join((root, self.qualifiers[0], typeName,)) @Lazy def typeInterface(self): @@ -272,7 +280,8 @@ class TypeInterfaceSourceList(object): implements(schema.interfaces.IIterableSource) - typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote) + typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote, + IOptions) def __init__(self, context): self.context = context diff --git a/util.py b/util.py index 74f152b..55096f3 100644 --- a/util.py +++ b/util.py @@ -141,4 +141,7 @@ def saveRequest(request): local_data.request = request def getRequest(): - return local_data.request + try: + return local_data.request + except AttributeError: + return None