+
+
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
-
+
+
Category
Response
@@ -19,42 +26,74 @@
-
+
+
+
+
- Questionnaire
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 :
+
+
+
+
+
+
+
+
+
+
+
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