Merge branch 'master' of ssh://git.cy55.de/home/git/loops

This commit is contained in:
hplattner 2013-07-26 14:59:22 +02:00
commit 4178829685
59 changed files with 1377 additions and 512 deletions

View file

@ -49,7 +49,7 @@ from zope.security import canAccess
from zope.security.interfaces import ForbiddenAttribute, Unauthorized from zope.security.interfaces import ForbiddenAttribute, Unauthorized
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL 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.ajax.dojo import dojoMacroTemplate
from cybertools.browser.view import GenericView from cybertools.browser.view import GenericView
@ -70,7 +70,7 @@ from loops.organize.tracking import access
from loops.resource import Resource from loops.resource import Resource
from loops.security.common import checkPermission from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject 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 import util
from loops.util import _, saveRequest from loops.util import _, saveRequest
from loops import version from loops import version
@ -481,7 +481,7 @@ class BaseView(GenericView, I18NView):
return absoluteURL(provider, self.request) return absoluteURL(provider, self.request)
return None return None
def renderText(self, text, contentType): def renderText(self, text, contentType='text/restructured'):
text = util.toUnicode(text) text = util.toUnicode(text)
typeKey = util.renderingFactories.get(contentType, None) typeKey = util.renderingFactories.get(contentType, None)
if typeKey is None: if typeKey is None:
@ -531,11 +531,29 @@ class BaseView(GenericView, I18NView):
def conceptTypes(self): def conceptTypes(self):
return util.KeywordVocabulary(self.listTypes(('concept',), ('hidden',))) 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'): def listTypesForSearch(self, include=None, exclude=None, sortOn='title'):
types = [dict(token=t.tokenForSearch, title=t.title) types = [dict(token=t.tokenForSearch, title=t.title)
for t in ITypeManager(self.context).listTypes(include, exclude)] for t in ITypeManager(self.context).listTypes(include, exclude)]
if sortOn: if sortOn:
types.sort(key=lambda x: x[sortOn]) types.sort(key=lambda x: x[sortOn])
for t in self.parentTypesFromOtherSites():
types.append(dict(token=t.tokenForSearch, title=t.title))
return types return types
def typesForSearch(self): def typesForSearch(self):

View file

@ -0,0 +1,4 @@
"""
package loops.browser.compound
"""

View file

@ -0,0 +1,14 @@
<configure
xmlns:zope="http://namespaces.zope.org/zope"
xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops">
<zope:adapter
name="compound.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.browser.compound.standard.CompoundView"
permission="zope.View" />
</configure>

View file

@ -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

View file

@ -0,0 +1,13 @@
<html i18n:domain="loops">
<metal:data define-macro="standard">
<tal:part repeat="item item/getParts">
<tal:check condition="item/checkPermissions">
<metal:part use-macro="item/macro" />
</tal:check>
</tal:part>
</metal:data>
</html>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -50,6 +50,7 @@ from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType, ITypeManager from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.jeep import Jeep from cybertools.util.jeep import Jeep
from loops.browser.common import EditForm, BaseView, LoopsTerms, concept_macros from loops.browser.common import EditForm, BaseView, LoopsTerms, concept_macros
from loops.browser.common import ViewMode
from loops.common import adapted from loops.common import adapted
from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList
from loops.i18n.browser import I18NView from loops.i18n.browser import I18NView
@ -196,6 +197,12 @@ class BaseRelationView(BaseView):
return u'' return u''
return self.predicateTitle 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): class ConceptView(BaseView):
@ -228,6 +235,7 @@ class ConceptView(BaseView):
subMacro=concept_macros.macros['parents'], subMacro=concept_macros.macros['parents'],
priority=20, info=self) priority=20, info=self)
# the part-based layout is now implemented in loops.browser.compound
def getParts(self): def getParts(self):
parts = (self.params.get('parts') or []) # deprecated! parts = (self.params.get('parts') or []) # deprecated!
if not parts: if not parts:
@ -740,3 +748,30 @@ class ListTypeInstances(ListChildren):
noDuplicates, useFilter, [self.typePredicate]): noDuplicates, useFilter, [self.typePredicate]):
yield c 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)

View file

@ -64,7 +64,8 @@
</h1> </h1>
<metal:block use-macro="view/concept_macros/filter_input" /> <metal:block use-macro="view/concept_macros/filter_input" />
</metal:title> </metal:title>
<p tal:define="description description|item/renderedDescription" <p metal:define-macro="conceptdescription"
tal:define="description description|item/renderedDescription"
tal:condition="description"> tal:condition="description">
<i tal:content="structure description">Description</i></p> <i tal:content="structure description">Description</i></p>
</metal:title> </metal:title>
@ -158,11 +159,8 @@
tal:attributes="dojoType python: tal:attributes="dojoType python:
item.editable and 'dojo.dnd.Source' or ''"> item.editable and 'dojo.dnd.Source' or ''">
<tal:items repeat="related children"> <tal:items repeat="related children">
<tal:item define="class python: repeat['related'].odd() and 'even' or 'odd'; <tal:item define="class python:
description related/description; repeat['related'].odd() and 'even' or 'odd';">
predicate related/predicateTitle;
info python: ' | '.join(
t for t in (description, predicate) if t)">
<tr tal:attributes="class string:$class dojoDndItem dojoDndHandle; <tr tal:attributes="class string:$class dojoDndItem dojoDndHandle;
id related/uniqueId"> id related/uniqueId">
<td tal:condition="item/showCheckboxes|nothing" <td tal:condition="item/showCheckboxes|nothing"
@ -172,7 +170,7 @@
tal:attributes="value uid;" /></td> tal:attributes="value uid;" /></td>
<td valign="top"> <td valign="top">
<a tal:attributes="href python: view.getUrlForTarget(related); <a tal:attributes="href python: view.getUrlForTarget(related);
title info"> title related/relationInfo">
<span tal:replace="related/title">Resource Title</span> <span tal:replace="related/title">Resource Title</span>
</a> </a>
</td> </td>
@ -241,11 +239,8 @@
tal:attributes="dojoType python: tal:attributes="dojoType python:
item.editable and 'dojo.dnd.Source' or ''"> item.editable and 'dojo.dnd.Source' or ''">
<tal:items repeat="related resources"> <tal:items repeat="related resources">
<tal:item define="class python: repeat['related'].odd() and 'even' or 'odd'; <tal:item define="class python:
description related/description; repeat['related'].odd() and 'even' or 'odd';">
predicate related/predicateTitle;
info python: ' | '.join(
t for t in (description, predicate) if t)">
<tr tal:attributes="class string:$class dojoDndItem dojoDndHandle; <tr tal:attributes="class string:$class dojoDndItem dojoDndHandle;
id related/uniqueId"> id related/uniqueId">
<td tal:condition="item/showCheckboxes|nothing" <td tal:condition="item/showCheckboxes|nothing"
@ -262,7 +257,7 @@
<img tal:attributes="src icon/src" /> <img tal:attributes="src icon/src" />
</a> </a>
<a tal:attributes="href python: view.getUrlForTarget(related); <a tal:attributes="href python: view.getUrlForTarget(related);
title info"> title related/relationInfo">
<div tal:content="related/title">Resource Title</div> <div tal:content="related/title">Resource Title</div>
</a> </a>
</td> </td>

View file

@ -553,6 +553,14 @@
factory="loops.browser.concept.ListTypeInstances" factory="loops.browser.concept.ListTypeInstances"
permission="zope.View" /> permission="zope.View" />
<zope:adapter
name="tabbed_page.html"
for="loops.interfaces.IConcept
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.browser.concept.TabbedPage"
permission="zope.View" />
<!-- dialogs/forms (end-user views) --> <!-- dialogs/forms (end-user views) -->
<page <page
@ -757,6 +765,7 @@
attribute="cleanup" attribute="cleanup"
permission="zope.ManageSite" /> permission="zope.ManageSite" />
<include package=".compound" />
<include package=".skin" /> <include package=".skin" />
<include package=".lobo" /> <include package=".lobo" />
<include package=".mobile" /> <include package=".mobile" />

View file

@ -51,6 +51,8 @@
</th> </th>
</tr></tbody> </tr></tbody>
<tbody metal:define-slot="custom_header" />
<tbody><tr><td colspan="5" style="padding-right: 15px"> <tbody><tr><td colspan="5" style="padding-right: 15px">
<div id="form.fields"> <div id="form.fields">
<metal:fields use-macro="view/fieldRenderers/fields" /> <metal:fields use-macro="view/fieldRenderers/fields" />
@ -59,7 +61,7 @@
<tbody> <tbody>
<tr metal:use-macro="view/template/macros/assignments" /> <tr metal:use-macro="view/template/macros/assignments" />
<tal:custom define="customMacro view/customMacro" <tal:custom define="customMacro view/customMacro|nothing"
condition="customMacro"> condition="customMacro">
<tr metal:use-macro="customMacro" /> <tr metal:use-macro="customMacro" />
</tal:custom> </tal:custom>
@ -119,6 +121,8 @@
tal:attributes="value typeToken" /> tal:attributes="value typeToken" />
</th></tr></tbody> </th></tr></tbody>
<tbody metal:define-slot="custom_header" />
<tbody><tr><td colspan="5"> <tbody><tr><td colspan="5">
<div id="form.fields"> <div id="form.fields">
<metal:fields use-macro="view/fieldRenderers/fields" /> <metal:fields use-macro="view/fieldRenderers/fields" />
@ -127,7 +131,7 @@
<tbody> <tbody>
<tr metal:use-macro="view/template/macros/assignments" /> <tr metal:use-macro="view/template/macros/assignments" />
<tal:custom define="customMacro view/customMacro" <tal:custom define="customMacro view/customMacro|nothing"
condition="customMacro"> condition="customMacro">
<tr metal:use-macro="customMacro" /> <tr metal:use-macro="customMacro" />
</tal:custom> </tal:custom>

View file

@ -117,6 +117,7 @@
<tal:img condition="cell/img"> <tal:img condition="cell/img">
<a dojoType="dojox.image.Lightbox" group="mediasset" <a dojoType="dojox.image.Lightbox" group="mediasset"
i18n:attributes="title" i18n:attributes="title"
tal:omit-tag="python:part.imageSize in ('large',)"
tal:attributes="href cell/img/fullImageUrl; tal:attributes="href cell/img/fullImageUrl;
title python: cell.img['description'] or cell.img['title']"> title python: cell.img['description'] or cell.img['title']">
<img tal:condition="showImageLink|python:False" <img tal:condition="showImageLink|python:False"

View file

@ -1,4 +1,4 @@
/* $Id$ */ /* loops.js */
function openEditWindow(url) { function openEditWindow(url) {
zmi = window.open(url, 'zmi'); zmi = window.open(url, 'zmi');
@ -19,6 +19,12 @@ function toggleCheckBoxes(toggle, fieldName) {
for (i in w) w[i].checked=toggle.checked; for (i in w) w[i].checked=toggle.checked;
} }
function setRadioButtons(value) {
dojo.forEach(dojo.query('input[type="radio"][value="' + value + '"]'),
function(n) {
n.checked = true;})
}
function validate(nodeName, required) { function validate(nodeName, required) {
// (work in progress) - may be used for onBlur event handler // (work in progress) - may be used for onBlur event handler
var w = dojo.byId(nodeName); var w = dojo.byId(nodeName);

View file

@ -195,7 +195,11 @@ class NodeView(BaseView):
subMacro=calendar_macros.macros['main'], subMacro=calendar_macros.macros['main'],
priority=90) priority=90)
# force early portlet registrations by target by setting up target view # force early portlet registrations by target by setting up target view
self.virtualTarget if self.virtualTarget is not None:
std = self.virtualTarget.typeOptions('portlet_states')
if std:
from loops.organize.stateful.browser import registerStatesPortlet
registerStatesPortlet(self.controller, self.virtualTarget, std)
@Lazy @Lazy
def usersPresent(self): def usersPresent(self):
@ -381,6 +385,8 @@ class NodeView(BaseView):
ht = super(NodeView, self).headTitle ht = super(NodeView, self).headTitle
if ht not in parts: if ht not in parts:
parts.append(ht) parts.append(ht)
if self.globalOptions('reverseHeadTitle'):
parts.reverse()
return ' - ' .join(parts) return ' - ' .join(parts)
@Lazy @Lazy

View file

@ -314,8 +314,13 @@
<metal:login define-macro="login"> <metal:login define-macro="login">
<div><a href="login.html" <div>
<a href="login.html"
i18n:translate="">Log in</a></div> i18n:translate="">Log in</a></div>
<div tal:define="register python:view.globalOptions('provideLogin')"
tal:condition="register">
<a tal:attributes="href python:register[0]"
i18n:translate="">Register new member</a></div>
</metal:login> </metal:login>

View file

@ -40,6 +40,7 @@ from zope.traversing.browser import absoluteURL
from cybertools.browser.action import actions from cybertools.browser.action import actions
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from cybertools.util.html import extractFirstPart
from cybertools.xedit.browser import ExternalEditorView, fromUnicode from cybertools.xedit.browser import ExternalEditorView, fromUnicode
from loops.browser.action import DialogAction, TargetAction from loops.browser.action import DialogAction, TargetAction
from loops.browser.common import EditForm, BaseView from loops.browser.common import EditForm, BaseView
@ -131,6 +132,9 @@ class ResourceView(BaseView):
def macro(self): def macro(self):
if 'image/' in self.context.contentType: if 'image/' in self.context.contentType:
return self.template.macros['image'] return self.template.macros['image']
#elif 'audio/' in self.context.contentType:
# self.registerDojoAudio()
# return self.template.macros['audio']
else: else:
return self.template.macros['download'] return self.template.macros['download']
@ -252,6 +256,12 @@ class ResourceView(BaseView):
#return util.toUnicode(wp.render(self.request)) #return util.toUnicode(wp.render(self.request))
return super(ResourceView, self).renderText(text, contentType) 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): def download(self):
""" Force download, e.g. of a PDF file """ """ Force download, e.g. of a PDF file """
return self.show(True) return self.show(True)

View file

@ -30,7 +30,13 @@
<metal:tabs use-macro="views/node_macros/breadcrumbs" /> <metal:tabs use-macro="views/node_macros/breadcrumbs" />
</metal:breadcrumbs> </metal:breadcrumbs>
<div metal:define-slot="actions"></div> <div metal:define-slot="actions"></div>
<div metal:define-slot="message"></div> <metal:message define-slot="message">
<div class="message"
i18n:translate=""
tal:define="msg request/loops.message|nothing"
tal:condition="msg"
tal:content="msg" />
</metal:message>
<metal:tabs use-macro="views/node_macros/view_modes" /> <metal:tabs use-macro="views/node_macros/view_modes" />
<metal:content define-slot="content"> <metal:content define-slot="content">
<tal:content define="item nocall:view/item; <tal:content define="item nocall:view/item;

View file

@ -62,6 +62,10 @@ h1, h2, h3, h4, h5, h6 {
margin-bottom: 0.4em; margin-bottom: 0.4em;
} }
a {
text-decoration: none;
}
a[href]:hover { a[href]:hover {
text-decoration: none; text-decoration: none;
color: #6060c0; color: #6060c0;
@ -120,6 +124,10 @@ thead th {
margin-bottom: 0.3em; margin-bottom: 0.3em;
} }
.infotext {
font-size: 90%;
}
.fields td { .fields td {
vertical-align: top; vertical-align: top;
} }
@ -163,6 +171,10 @@ table.listing td {
border-bottom: 1px dotted #dddddd; border-bottom: 1px dotted #dddddd;
} }
table.listing tr.vpad td {
padding: 7px 2px 7px 2px;
}
fieldset.box table.listing td { fieldset.box table.listing td {
padding: 0 1px 0 1px; padding: 0 1px 0 1px;
} }
@ -267,7 +279,8 @@ fieldset.box td {
.top-actions { .top-actions {
position: absolute; position: absolute;
top: 30px; top: 40px;
margin-left: 10px;
} }
.quicksearch input { .quicksearch input {
@ -281,7 +294,7 @@ fieldset.box td {
.page-actions { .page-actions {
position: absolute; position: absolute;
top: 55px; top: 75px;
margin-left: 210px; margin-left: 210px;
} }

View file

@ -113,6 +113,9 @@ class AdapterBase(object):
self.context = context self.context = context
self.__parent__ = context # to get the permission stuff right self.__parent__ = context # to get the permission stuff right
def __hash__(self):
return hash(self.context)
def __getattr__(self, attr): def __getattr__(self, attr):
self.checkAttr(attr) self.checkAttr(attr)
return getattr(self.context, '_' + attr, None) return getattr(self.context, '_' + attr, None)

View file

@ -355,7 +355,7 @@ Books, Sections, and Pages
>>> importPath = os.path.join(os.path.dirname(__file__), 'book') >>> importPath = os.path.join(os.path.dirname(__file__), 'book')
>>> importData(loopsRoot, importPath, 'loops_book_de.dmp') >>> importData(loopsRoot, importPath, 'loops_book_de.dmp')
>>> from loops.compound.book.browser import PageLayout >>> from loops.compound.book.browser import BookView, SectionView, TopicView
Fin de partie Fin de partie

View file

@ -1,52 +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
#
"""
Implementation of book and book components
"""
from zope.cachedescriptors.property import Lazy
from zope.interface import implements
from zope.traversing.api import getName
from loops.compound.base import Compound
from loops.compound.book.interfaces import IPage
from loops.type import TypeInterfaceSourceList
TypeInterfaceSourceList.typeInterfaces += (IPage,)
class Page(Compound):
implements(IPage)
compoundPredicateNames = ['ispartof', 'standard']
@Lazy
def documentType(self):
return self.context.getConceptManager()['documenttype']
def getParts(self):
result = {}
for r in super(Page, self).getParts():
for parent in r.getParents():
if parent.conceptType == self.documentType:
item = result.setdefault(getName(parent), [])
item.append(r)
return result

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -26,6 +26,7 @@ from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName from zope.traversing.api import getName
from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from loops.browser.lobo import standard from loops.browser.lobo import standard
from loops.browser.concept import ConceptView from loops.browser.concept import ConceptView
@ -45,10 +46,22 @@ class Base(object):
def book_macros(self): def book_macros(self):
return book_template.macros return book_template.macros
@Lazy
def documentTypeType(self):
return self.conceptManager['documenttype']
@Lazy
def sectionType(self):
return self.conceptManager['section']
@Lazy @Lazy
def isPartOfPredicate(self): def isPartOfPredicate(self):
return self.conceptManager['ispartof'] return self.conceptManager['ispartof']
@Lazy
def showNavigation(self):
return self.typeOptions.show_navigation
@Lazy @Lazy
def breadcrumbsParent(self): def breadcrumbsParent(self):
for p in self.context.getParents([self.isPartOfPredicate]): for p in self.context.getParents([self.isPartOfPredicate]):
@ -82,34 +95,8 @@ class Base(object):
if self.editable: if self.editable:
return 'index.html' return 'index.html'
class BookOverview(Base, ConceptView):
@Lazy
def macro(self):
return book_template.macros['book']
class SectionView(Base, ConceptView):
@Lazy
def macro(self):
return book_template.macros['section']
@Lazy
def documentTypeType(self):
return self.conceptManager['documenttype']
@Lazy
def showNavigation(self):
return self.typeOptions.show_navigation
@Lazy
def sectionType(self):
return self.conceptManager['section']
def getResources(self): def getResources(self):
relViews = super(SectionView, self).getResources() relViews = super(Base, self).getResources()
return relViews return relViews
@Lazy @Lazy
@ -132,11 +119,36 @@ class SectionView(Base, ConceptView):
self.images[idx].append(img) self.images[idx].append(img)
return result return result
def getCssClassForResource(self, r): def getDocumentTypeForResource(self, r):
for c in r.context.getConcepts([self.defaultPredicate]): for c in r.context.getConcepts([self.defaultPredicate]):
if c.conceptType == self.documentTypeType: if c.conceptType == self.documentTypeType:
return getName(c) return c
def getOptionsForResource(self, r, name):
dt = self.getDocumentTypeForResource(r)
if dt is not None:
return IOptions(adapted(dt))(name)
def getTitleForResource(self, r):
if self.getOptionsForResource(r, 'showtitle'):
return r.title
def getIconForResource(self, r):
icon = self.getOptionsForResource(r, 'icon')
if icon:
return '/'.join((self.controller.resourceBase, icon[0]))
def getCssClassForResource(self, r):
dt = self.getDocumentTypeForResource(r)
if dt is None:
return 'textelement' return 'textelement'
css = IOptions(adapted(dt))('cssclass')
if css:
return css
return getName(dt)
def getMacroForResource(self, r):
return self.book_macros['default_text']
def getParentsForResource(self, r): def getParentsForResource(self, r):
for c in r.context.getConcepts([self.defaultPredicate]): for c in r.context.getConcepts([self.defaultPredicate]):
@ -144,64 +156,23 @@ class SectionView(Base, ConceptView):
yield c yield c
# layout parts - probably obsolete: class BookView(Base, ConceptView):
class PageLayout(Base, standard.Layout): @Lazy
def macro(self):
def getParts(self): return book_template.macros['book']
parts = ['headline', 'keyquestions', 'quote', 'maintext',
'story', 'tip', 'usecase']
return self.getPartViews(parts)
class PagePart(object): class SectionView(Base, ConceptView):
template = book_template @Lazy
templateName = 'compound.book' def macro(self):
macroName = 'text' return book_template.macros['section']
partName = None # define in subclass
gridPattern = ['span-4']
def getResources(self):
result = []
res = self.adapted.getParts().get(self.partName) or []
for idx, r in enumerate(res):
result.append(standard.ResourceView(
r, self.request, parent=self, idx=idx))
return result
class Headline(PagePart, standard.Header2): class TopicView(Base, ConceptView):
macroName = 'headline' @Lazy
def macro(self):
return book_template.macros['topic']
class MainText(PagePart, standard.BasePart):
partName = 'maintext'
class KeyQuestions(PagePart, standard.BasePart):
partName = 'keyquestions'
class Story(PagePart, standard.BasePart):
partName = 'story'
class Tip(PagePart, standard.BasePart):
partName = 'tip'
class UseCase(PagePart, standard.BasePart):
partName = 'usecase'
class Quote(PagePart, standard.BasePart):
partName = 'quote'
gridPattern = ['span-2 last']

View file

@ -3,18 +3,6 @@
xmlns:browser="http://namespaces.zope.org/browser" xmlns:browser="http://namespaces.zope.org/browser"
i18n_domain="loops"> i18n_domain="loops">
<!-- type adapters -->
<zope:adapter factory="loops.compound.book.base.Page"
provides="loops.compound.book.interfaces.IPage"
trusted="True" />
<zope:class class="loops.compound.book.base.Page">
<require permission="zope.View"
interface="loops.compound.book.interfaces.IPage" />
<require permission="zope.ManageContent"
set_schema="loops.compound.book.interfaces.IPage" />
</zope:class>
<!-- Views --> <!-- Views -->
<zope:adapter <zope:adapter
@ -22,7 +10,7 @@
for="loops.interfaces.IConcept for="loops.interfaces.IConcept
loops.browser.skin.Lobo" loops.browser.skin.Lobo"
provides="zope.interface.Interface" provides="zope.interface.Interface"
factory="loops.compound.book.browser.BookOverview" factory="loops.compound.book.browser.BookView"
permission="zope.View" /> permission="zope.View" />
<zope:adapter <zope:adapter
@ -34,69 +22,11 @@
permission="zope.View" /> permission="zope.View" />
<zope:adapter <zope:adapter
name="page_layout" name="book_topic_view"
for="loops.interfaces.IConcept for="loops.interfaces.IConcept
loops.browser.skin.Lobo" loops.browser.skin.Lobo"
provides="zope.interface.Interface" provides="zope.interface.Interface"
factory="loops.compound.book.browser.PageLayout" factory="loops.compound.book.browser.TopicView"
permission="zope.View" />
<!-- parts -->
<zope:adapter
name="lobo_headline"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.Headline"
permission="zope.View" />
<zope:adapter
name="lobo_keyquestions"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.KeyQuestions"
permission="zope.View" />
<zope:adapter
name="lobo_maintext"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.MainText"
permission="zope.View" />
<zope:adapter
name="lobo_story"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.Story"
permission="zope.View" />
<zope:adapter
name="lobo_tip"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.Tip"
permission="zope.View" />
<zope:adapter
name="lobo_usecase"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.UseCase"
permission="zope.View" />
<zope:adapter
name="lobo_quote"
for="loops.compound.book.interfaces.IPage
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.Quote"
permission="zope.View" /> permission="zope.View" />
</configure> </configure>

View file

@ -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

View file

@ -1,14 +1,15 @@
type(u'documenttype', u'Dokumentenart', options=u'qualifier:assign', type(u'documenttype', u'Dokumentenart', options=u'qualifier:assign',
typeInterface=u'loops.interfaces.IOptions',
viewName=u'') viewName=u'')
# book types # book types
type(u'book', u'Buch', viewName=u'book_overview', typeInterface=u'', type(u'book', u'Buch', viewName=u'book_overview', typeInterface=u'',
options=u'action.portlet:create_subtype,edit_concept') 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'', type(u'section', u'Kapitel', viewName=u'section_view', typeInterface=u'',
options=u'action.portlet:create_subtype,edit_concept') 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') 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'story', u'Geschichte', u'documenttype')
concept(u'tip', u'Tipp', u'documenttype') concept(u'tip', u'Tipp', u'documenttype')
concept(u'usecase', u'Fallbeispiel', u'documenttype') concept(u'usecase', u'Fallbeispiel', u'documenttype')
concept(u'warning', u'Warnung', u'documenttype')
# book structure # book structure
child(u'book', u'section', u'issubtype', usePredicate=u'ispartof') child(u'book', u'section', u'issubtype', usePredicate=u'ispartof')
child(u'section', 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')

View file

@ -1,14 +1,22 @@
<html i18n:domain="loops"> <html i18n:domain="loops">
<metal:book define-macro="book"> <metal:children define-macro="children">
<metal:info use-macro="view/concept_macros/concepttitle" /> <div tal:repeat="related item/children"
<div tal:repeat="related item/children"> tal:define="level python:level + 1"
tal:attributes="class string:content-$level">
<h3> <h3>
<a tal:attributes="href python:view.getUrlForTarget(related)" <a tal:attributes="href python:view.getUrlForTarget(related)"
tal:content="related/title" /></h3> tal:content="related/title" />
</h3>
<div tal:content="structure related/renderedDescription" /> <div tal:content="structure related/renderedDescription" />
<!-- TODO: show next level (+/-) -->
</div> </div>
</metal:children>
<metal:book define-macro="book">
<metal:info use-macro="view/concept_macros/concepttitle" />
<metal:info use-macro="item/book_macros/children" />
</metal:book> </metal:book>
@ -32,11 +40,23 @@
</div> </div>
</metal:navigation> </metal:navigation>
<metal:info use-macro="view/concept_macros/concepttitle" /> <metal:info use-macro="view/concept_macros/concepttitle" />
<div tal:repeat="related item/textResources"> <metal:info use-macro="item/book_macros/children" />
<metal:text define-macro="textresources">
<div style="clear: both"
tal:repeat="related item/textResources">
<div class="span-4"> <div class="span-4">
<div tal:attributes="class python: <div metal:define-macro="default_text"
item.getCssClassForResource(related)" tal:attributes="class python:
tal:content="structure related/render" /> item.getCssClassForResource(related)">
<h3 tal:define="ttitle python:item.getTitleForResource(related)"
tal:condition="ttitle"
tal:content="ttitle" />
<img class="flow-left" style="padding-top: 5px"
tal:define="icon python:item.getIconForResource(related)"
tal:condition="icon"
tal:attributes="src icon" />
<span tal:content="structure related/render" />
</div>
</div> </div>
<div class="span-2 last" style="padding-top: 0.4em"> <div class="span-2 last" style="padding-top: 0.4em">
<div class="object-actions" style="padding-top: 0" <div class="object-actions" style="padding-top: 0"
@ -73,30 +93,40 @@
<img tal:attributes="src image/src; <img tal:attributes="src image/src;
alt image/title" /></a> alt image/title" /></a>
</div> </div>
<!-- TODO: links to files -->
</div> </div>
</div> </div>
</metal:text>
<br style="clear: both" /> <br style="clear: both" />
<metal:navigation use-macro="item/book_macros/navigation" /> <metal:navigation use-macro="item/book_macros/navigation" />
<br /> <br />
</metal:section> </metal:section>
<!-- layout part macros - obsolete? --> <metal:topic define-macro="topic">
<metal:info use-macro="view/concept_macros/concepttitle" />
<metal:part define-macro="headline"> <h2 i18n:translate=""
<div tal:define="cell part/getView"> tal:condition="python: list(item.children())">Children</h2>
<metal:headline use-macro="item/macros/headline" /> <metal:children use-macro="item/book_macros/children" />
<h2 i18n:translate=""
tal:condition="item/textResources">Text Elements</h2>
<div>
<div tal:repeat="related item/textResources"
tal:define="level python:level + 1"
tal:attributes="class string:content-$level">
<h3>
<a tal:attributes="href python:view.getUrlForTarget(related.context)"
tal:content="related/title" />
</h3>
<div>
<div tal:replace="structure related/renderShortText" />
<p>
<a i18n:translate=""
tal:attributes="href python:view.getUrlForTarget(related.context)">
more...</a></p>
</div> </div>
</metal:part>
<metal:part define-macro="text">
<tal:cell repeat="cell part/getResources">
<div tal:attributes="class cell/cssClass">
<h3 tal:content="cell/title" />
<span tal:content="structure cell/view/render" />
</div> </div>
</tal:cell> </metal:topic>
</metal:part>
</html> </html>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,14 +19,12 @@
""" """
Definition of basic view classes and other browser related stuff for the Definition of basic view classes and other browser related stuff for the
loops.expert package. loops.expert package.
$Id$
""" """
from zope import interface, component from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName, getParent from zope.traversing.api import getName, getParent, traverse
from cybertools.browser.form import FormController from cybertools.browser.form import FormController
from cybertools.stateful.interfaces import IStateful, IStatesDefinition from cybertools.stateful.interfaces import IStateful, IStatesDefinition
@ -150,12 +148,16 @@ class Search(ConceptView):
if not isinstance(types, (list, tuple)): if not isinstance(types, (list, tuple)):
types = [types] types = [types]
for type in 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, result = self.executeQuery(title=title or None, type=type,
exclude=('hidden',)) exclude=('hidden',))
fv = FilterView(self.context, self.request) fv = FilterView(self.context, self.request)
result = fv.apply(result) result = fv.apply(result)
for o in result: for o in result:
if o.getLoopsRoot() == self.loopsRoot: if o.getLoopsRoot() == site:
adObj = adapted(o, self.languageInfo) adObj = adapted(o, self.languageInfo)
if filterMethod is not None and not filterMethod(adObj): if filterMethod is not None and not filterMethod(adObj):
continue continue

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Query concepts management stuff. Query concepts management stuff.
$Id$
""" """
from BTrees.IOBTree import IOBTree 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.catalog.interfaces import ICatalog
from zope.app.intid.interfaces import IIntIds from zope.app.intid.interfaces import IIntIds
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.traversing.api import traverse
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from loops.common import AdapterBase from loops.common import AdapterBase
@ -66,6 +65,11 @@ class BaseQuery(object):
return self.context.context.getLoopsRoot() return self.context.context.getLoopsRoot()
def queryConcepts(self, title=None, type=None, **kw): 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('*'): if type.endswith('*'):
start = type[:-1] start = type[:-1]
end = start + '\x7f' end = start + '\x7f'
@ -76,7 +80,7 @@ class BaseQuery(object):
result = cat.searchResults(loops_type=(start, end), loops_title=title) result = cat.searchResults(loops_type=(start, end), loops_title=title)
else: else:
result = cat.searchResults(loops_type=(start, end)) 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)) and canListObject(r))
if 'exclude' in kw: if 'exclude' in kw:
r1 = set() r1 = set()

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -78,28 +78,28 @@ class OfficeFile(ExternalFileAdapter):
@Lazy @Lazy
def docPropertyDom(self): def docPropertyDom(self):
fn = self.docFilename fn = self.docFilename
dummy = dict(core=[], custom=[]) result = dict(core=[], custom=[])
root, ext = os.path.splitext(fn) root, ext = os.path.splitext(fn)
if not ext.lower() in self.fileExtensions: if not ext.lower() in self.fileExtensions:
return dummy return result
try: try:
zf = ZipFile(fn, 'r') zf = ZipFile(fn, 'r')
except IOError, e: except IOError, e:
from logging import getLogger from logging import getLogger
self.logger.warn(e) self.logger.warn(e)
return dummy return result
if self.corePropFileName not in zf.namelist(): if self.corePropFileName not in zf.namelist():
self.logger.warn('Core properties not found in file %s.' % self.logger.warn('Core properties not found in file %s.' %
self.externalAddress) self.externalAddress)
else:
result['core'] = etree.fromstring(zf.read(self.corePropFileName))
if self.propFileName not in zf.namelist(): if self.propFileName not in zf.namelist():
self.logger.warn('Custom properties not found in file %s.' % self.logger.warn('Custom properties not found in file %s.' %
self.externalAddress) self.externalAddress)
propsXml = zf.read(self.propFileName) else:
corePropsXml = zf.read(self.corePropFileName) result['custom'] = etree.fromstring(zf.read(self.propFileName))
# TODO: read core.xml, return both trees in dictionary
zf.close() zf.close()
return {'custom': etree.fromstring(propsXml), return result
'core': etree.fromstring(corePropsXml)}
def getDocProperty(self, pname): def getDocProperty(self, pname):
for p in self.docPropertyDom['custom']: for p in self.docPropertyDom['custom']:

View file

@ -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 # types stuff
class ITypeConcept(IConceptSchema, ILoopsAdapter): class ITypeConcept(IConceptSchema, ILoopsAdapter, IOptions):
""" Concepts of type 'type' should be adaptable to this interface. """ Concepts of type 'type' should be adaptable to this interface.
""" """
@ -725,13 +737,6 @@ class ITypeConcept(IConceptSchema, ILoopsAdapter):
default=u'', default=u'',
required=False) required=False)
options = schema.List(
title=_(u'Options'),
description=_(u'Additional settings.'),
value_type=schema.TextLine(),
default=[],
required=False)
# storage = schema.Choice() # storage = schema.Choice()

View file

@ -26,6 +26,7 @@ from zope.component import adapts
from zope.interface import implementer, implements from zope.interface import implementer, implements
from loops.common import AdapterBase from loops.common import AdapterBase
from loops.interfaces import IConcept
from loops.knowledge.qualification.interfaces import ICompetence from loops.knowledge.qualification.interfaces import ICompetence
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList

View file

@ -4,7 +4,14 @@
i18n_domain="loops"> i18n_domain="loops">
<zope:adapter <zope:adapter
factory="loops.knowledge.qualification.base.Competence" /> factory="loops.knowledge.qualification.base.Competence"
trusted="True" />
<zope:class class="loops.knowledge.qualification.base.Competence">
<require permission="zope.View"
interface="loops.knowledge.qualification.interfaces.ICompetence" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.qualification.interfaces.ICompetence" />
</zope:class>
<!-- views --> <!-- views -->

View file

@ -23,11 +23,11 @@ Interfaces for knowledge management and elearning with loops.
from zope.interface import Interface, Attribute from zope.interface import Interface, Attribute
from zope import interface, component, schema from zope import interface, component, schema
from loops.interfaces import IConceptSchema from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _ from loops.util import _
class ICompetence(IConceptSchema): class ICompetence(ILoopsAdapter):
""" The competence of a person. """ The competence of a person.
Maybe assigned to the person via a 'knows' relation or Maybe assigned to the person via a 'knows' relation or

View file

@ -62,7 +62,7 @@ class QuestionGroup(AdapterBase, QuestionGroup):
_contextAttributes = list(IQuestionGroup) _contextAttributes = list(IQuestionGroup)
_adapterAttributes = AdapterBase._adapterAttributes + ( _adapterAttributes = AdapterBase._adapterAttributes + (
'questionnaire', 'questions', 'feedbackItems',) 'questionnaire', 'questions', 'feedbackItems')
_noexportAttributes = _adapterAttributes _noexportAttributes = _adapterAttributes
@property @property
@ -109,9 +109,6 @@ class Question(AdapterBase, Question):
def questionnaire(self): def questionnaire(self):
return self.questionGroup.questionnaire return self.questionGroup.questionnaire
def __hash__(self):
return hash(self.context)
class FeedbackItem(AdapterBase, FeedbackItem): class FeedbackItem(AdapterBase, FeedbackItem):
@ -125,4 +122,3 @@ class FeedbackItem(AdapterBase, FeedbackItem):
@property @property
def text(self): def text(self):
return self.context.description return self.context.description

View file

@ -21,26 +21,40 @@ Definition of view classes and other browser related stuff for
surveys and self-assessments. surveys and self-assessments.
""" """
import csv
from cStringIO import StringIO
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from cybertools.knowledge.survey.questionnaire import Response from cybertools.knowledge.survey.questionnaire import Response
from cybertools.util.date import formatTimeStamp
from loops.browser.concept import ConceptView from loops.browser.concept import ConceptView
from loops.browser.node import NodeView
from loops.common import adapted from loops.common import adapted
from loops.knowledge.survey.response import Responses
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
from loops.util import getObjectForUid
from loops.util import _
template = ViewPageTemplateFile('view_macros.pt') template = ViewPageTemplateFile('view_macros.pt')
class SurveyView(ConceptView): class SurveyView(ConceptView):
tabview = 'index.html'
data = None data = None
errors = None
@Lazy @Lazy
def macro(self): def macro(self):
self.registerDojo()
return template.macros['survey'] return template.macros['survey']
@Lazy
def tabview(self):
if self.editable:
return 'index.html'
def results(self): def results(self):
result = [] result = []
response = None response = None
@ -52,22 +66,114 @@ class SurveyView(ConceptView):
if key.startswith('question_'): if key.startswith('question_'):
uid = key[len('question_'):] uid = key[len('question_'):]
question = adapted(self.getObjectForUid(uid)) question = adapted(self.getObjectForUid(uid))
if value != 'none':
value = int(value) value = int(value)
self.data[uid] = value self.data[uid] = value
response.values[question] = value response.values[question] = value
# TODO: store self.data in track Responses(self.context).save(self.data)
# else: self.errors = self.check(response)
# get response from track if self.errors:
return []
if response is not None: if response is not None:
result = response.getGroupedResult() result = response.getGroupedResult()
return [dict(category=r[0].title, text=r[1].text, return [dict(category=r[0].title, text=r[1].text,
score=int(round(r[2] * 100))) score=int(round(r[2] * 100)))
for r in result] for r in result]
def getValues(self, question): def check(self, response):
setting = 0 errors = []
if self.data is not None: values = response.values
setting = self.data.get(question.uid) or 0 for qu in self.adapted.questions:
return [dict(value=i, checked=(i == setting)) if qu.required and qu not in values:
for i in range(question.answerRange)] 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'<i>%s</i><br />(%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')

View file

@ -7,19 +7,56 @@
<zope:adapter <zope:adapter
factory="loops.knowledge.survey.base.Questionnaire" factory="loops.knowledge.survey.base.Questionnaire"
provides="loops.knowledge.survey.interfaces.IQuestionnaire" /> provides="loops.knowledge.survey.interfaces.IQuestionnaire"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.Questionnaire">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionnaire" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionnaire" />
</zope:class>
<zope:adapter <zope:adapter
factory="loops.knowledge.survey.base.QuestionGroup" factory="loops.knowledge.survey.base.QuestionGroup"
provides="loops.knowledge.survey.interfaces.IQuestionGroup" /> provides="loops.knowledge.survey.interfaces.IQuestionGroup"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.QuestionGroup">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestionGroup" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestionGroup" />
</zope:class>
<zope:adapter <zope:adapter
factory="loops.knowledge.survey.base.Question" factory="loops.knowledge.survey.base.Question"
provides="loops.knowledge.survey.interfaces.IQuestion" /> provides="loops.knowledge.survey.interfaces.IQuestion"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.Question">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IQuestion" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IQuestion" />
</zope:class>
<zope:adapter <zope:adapter
factory="loops.knowledge.survey.base.FeedbackItem" factory="loops.knowledge.survey.base.FeedbackItem"
provides="loops.knowledge.survey.interfaces.IFeedbackItem" /> provides="loops.knowledge.survey.interfaces.IFeedbackItem"
trusted="True" />
<zope:class class="loops.knowledge.survey.base.FeedbackItem">
<require permission="zope.View"
interface="loops.knowledge.survey.interfaces.IFeedbackItem" />
<require permission="zope.ManageContent"
set_schema="loops.knowledge.survey.interfaces.IFeedbackItem" />
</zope:class>
<!-- track -->
<zope:class class="loops.knowledge.survey.response.Response">
<require permission="zope.View"
interface="cybertools.tracking.interfaces.ITrack" />
<require permission="zope.ManageContent"
set_schema="cybertools.tracking.interfaces.ITrack" />
</zope:class>
<!-- views --> <!-- views -->
@ -31,4 +68,9 @@
factory="loops.knowledge.survey.browser.SurveyView" factory="loops.knowledge.survey.browser.SurveyView"
permission="zope.View" /> permission="zope.View" />
<browser:page name="survey_data.csv"
for="loops.interfaces.IView"
class="loops.knowledge.survey.browser.SurveyCsvExport"
permission="zope.View" />
</configure> </configure>

View file

@ -24,7 +24,7 @@ from zope.interface import Interface, Attribute
from zope import interface, component, schema from zope import interface, component, schema
from cybertools.knowledge.survey import interfaces from cybertools.knowledge.survey import interfaces
from loops.interfaces import IConceptSchema from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _ from loops.util import _
@ -38,16 +38,43 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
default=4, default=4,
required=True) 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): class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
""" A group of questions within a questionnaire. """ 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): class IQuestion(IConceptSchema, interfaces.IQuestion):
""" A single question within a questionnaire. """ 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( revertAnswerOptions = schema.Bool(
title=_(u'Negative'), title=_(u'Negative'),
description=_(u'Value inversion: High selection means low value.'), description=_(u'Value inversion: High selection means low value.'),

View file

@ -29,20 +29,34 @@ from loops.knowledge.survey.interfaces import IResponse, IResponses
from loops.organize.tracking.base import BaseRecordManager 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): class Response(Track):
""" A survey response.
"""
implements(IResponse) implements(IResponse)
typeName = 'Response' typeName = 'Response'
typeInterface = IResponse
class Responses(BaseRecordManager):
""" A tracking storage adapter for survey responses.
"""
implements(IResponses)
adapts(ITrackingStorage)

View file

@ -3,11 +3,18 @@
<metal:block define-macro="survey" <metal:block define-macro="survey"
tal:define="feedback item/results"> tal:define="feedback item/results;
<metal:title use-macro="item/conceptMacros/concepttitle" /> errors item/errors">
<metal:title use-macro="item/conceptMacros/concepttitle_only" />
<tal:description condition="not:feedback">
<metal:title use-macro="item/conceptMacros/conceptdescription" />
</tal:description>
<div tal:condition="feedback"> <div tal:condition="feedback">
<h3 i18n:translate="">Feedback</h3> <h3 i18n:translate="">Feedback</h3>
<table> <div tal:define="header item/adapted/feedbackHeader"
tal:condition="header"
tal:content="structure python:item.renderText(header, 'text/restructured')" />
<table class="listing">
<tr> <tr>
<th i18n:translate="">Category</th> <th i18n:translate="">Category</th>
<th i18n:translate="">Response</th> <th i18n:translate="">Response</th>
@ -19,42 +26,74 @@
<td tal:content="fbitem/score" /> <td tal:content="fbitem/score" />
</tr> </tr>
</table> </table>
<div class="button" id="show_questionnaire">
<a href="" onclick="back(); return false"
i18n:translate="">
Back to Questionnaire</a>
<br /> <br />
</div> </div>
<div tal:define="footer item/adapted/feedbackFooter"
tal:condition="footer"
tal:content="structure python:item.renderText(footer, 'text/restructured')" />
</div>
<div id="questionnaire"
tal:condition="not:feedback">
<h3 i18n:translate="">Questionnaire</h3> <h3 i18n:translate="">Questionnaire</h3>
<div class="error"
tal:condition="errors">
<div tal:repeat="error errors">
<span i18n:translate=""
tal:content="error" />
</div>
</div>
<form method="post"> <form method="post">
<table> <table class="listing">
<tr> <tal:qugroup repeat="qugroup item/adapted/questionGroups">
<th></th> <tr><td colspan="6">&nbsp;</td></tr>
<th> <tr class="vpad">
<table> <td tal:define="infoText python:item.getInfoText(qugroup)">
<tr> <b tal:content="qugroup/title" />
<td i18n:translate="">Does not apply</td> <div class="infotext"
<td style="text-align: right" tal:condition="infoText">
<span tal:content="structure infoText" />
</div>
</td>
<td style="text-align: center"
i18n:translate="">No answer</td>
<td colspan="2"
i18n:translate="">Fully applies</td> i18n:translate="">Fully applies</td>
<td colspan="2"
style="text-align: right"
i18n:translate="">Does not apply</td>
</tr> </tr>
</table> <tr class="vpad"
</th> tal:repeat="question qugroup/questions">
</tr>
<tr tal:repeat="question item/adapted/questions">
<td tal:content="question/text" /> <td tal:content="question/text" />
<td style="white-space: nowrap"> <td style="white-space: nowrap; text-align: center"
<span tal:repeat="value python:item.getValues(question)">&nbsp; tal:repeat="value python:item.getValues(question)">
<input type="radio" <input type="radio"
i18n:attributes="title" i18n:attributes="title"
tal:condition="value/radio"
tal:attributes=" tal:attributes="
name string:question_${question/uid}; name string:question_${question/uid};
value value/value; value value/value;
checked value/checked; checked value/checked;
title string:survey_value_${value/value}" />&nbsp; title string:survey_value_${value/value}" />
<span tal:condition="not:value/radio"
title="Obligatory question, must be answered"
i18n:attributes="title">***
</span> </span>
</td> </td>
</tr> </tr>
</tal:qugroup>
</table> </table>
<br />
<input type="submit" name="submit" value="Evaluate Questionnaire" <input type="submit" name="submit" value="Evaluate Questionnaire"
i18n:attributes="value" /> i18n:attributes="value" />
<input type="button" name="reset_responses" value="Reset Responses Entered"
i18n:attributes="value"
onclick="setRadioButtons('none'); return false" />
</form> </form>
</div>
</metal:block> </metal:block>

View file

@ -158,4 +158,5 @@ class TargetLayoutInstance(NodeLayoutInstance):
target = self.viewAnnotations.get('target') target = self.viewAnnotations.get('target')
if target is None: if target is None:
target = adapted(self.context.target) target = adapted(self.context.target)
#self.viewAnnotations['target'] = target # TODO: has to be tested!
return target return target

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Base classes for layout-based views. Base classes for layout-based views.
$Id$
""" """
from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.app.security.interfaces import IUnauthenticatedPrincipal
@ -29,6 +27,7 @@ from zope.proxy import removeAllProxies
from zope.security.proxy import removeSecurityProxy from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL from zope.traversing.browser import absoluteURL
from cybertools.meta.interfaces import IOptions
from cybertools.util import format from cybertools.util import format
from loops.common import adapted from loops.common import adapted
from loops.i18n.browser import LanguageInfo from loops.i18n.browser import LanguageInfo
@ -170,3 +169,7 @@ class BaseView(object):
def getMetaDescription(self): def getMetaDescription(self):
return self.context.title return self.context.title
@Lazy
def globalOptions(self):
return IOptions(self.loopsRoot)

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Layout node views. Layout node views.
$Id$
""" """
from zope.app.security.interfaces import IUnauthenticatedPrincipal from zope.app.security.interfaces import IUnauthenticatedPrincipal
@ -66,6 +64,9 @@ class LayoutNodeView(Page, BaseView):
if self.target is not None: if self.target is not None:
targetView = component.getMultiAdapter((self.target, self.request), targetView = component.getMultiAdapter((self.target, self.request),
name='layout') name='layout')
return ' - '.join((self.context.title, targetView.title)) parts = [self.context.title, targetView.title]
else: else:
return self.context.title parts = [self.context.title]
if self.globalOptions('reverseHeadTitle'):
parts.reverse()
return ' - '.join(parts)

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: 0.13.0\n" "Project-Id-Version: 0.13.0\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\n" "POT-Creation-Date: 2007-05-22 12:00 CET\n"
"PO-Revision-Date: 2013-03-07 12:00 CET\n" "PO-Revision-Date: 2013-07-15 12:00 CET\n"
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n" "Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n" "Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -86,6 +86,9 @@ msgstr "Thema bearbeiten..."
msgid "Modify topic." msgid "Modify topic."
msgstr "Thema ändern" msgstr "Thema ändern"
msgid "Please correct the indicated errors."
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
# blog # blog
msgid "Edit Blog Post..." msgid "Edit Blog Post..."
@ -175,11 +178,29 @@ msgstr "Glossareintrag anlegen."
msgid "Answer Range" msgid "Answer Range"
msgstr "Abstufung Bewertungen" 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." msgid "Number of items (answer options) to select from."
msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann." msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann."
msgid "Negativ" msgid "Minimum Number of Answers"
msgstr "Negativbewertung" 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." msgid "Value inversion: High selection means low value."
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert." msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
@ -196,27 +217,54 @@ msgstr "Kategorie"
msgid "Response" msgid "Response"
msgstr "Beurteilung" msgstr "Beurteilung"
msgid "No answer"
msgstr "Keine Antwort"
msgid "Does not apply" msgid "Does not apply"
msgstr "Trifft nicht zu" msgstr "Trifft nicht zu"
msgid "Fully applies" msgid "Fully applies"
msgstr "Trifft voll zu" msgstr "Trifft voll zu"
msgid "survey_value_none"
msgstr "Keine Antwort"
msgid "survey_value_0" 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" msgid "survey_value_1"
msgstr "trifft eher nicht zu" msgstr "Trifft eher nicht zu"
msgid "survey_value_2" msgid "survey_value_2"
msgstr "trifft eher zu" msgstr "Trifft eher zu"
msgid "survey_value_3" 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" msgid "Evaluate Questionnaire"
msgstr "Fragebogen auswerten" 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) # competence (qualification)
msgid "Validity Period (Months)" msgid "Validity Period (Months)"
@ -495,6 +543,9 @@ msgstr "Unterbegriffe"
msgid "Resources" msgid "Resources"
msgstr "Ressourcen" msgstr "Ressourcen"
msgid "Text Elements"
msgstr "Texte"
msgid "Title" msgid "Title"
msgstr "Titel" msgstr "Titel"
@ -660,6 +711,9 @@ msgstr "Zugeordnete Begriffe"
msgid "more..." msgid "more..."
msgstr "Mehr..." msgstr "Mehr..."
msgid "More..."
msgstr "Mehr..."
msgid "Versioning" msgid "Versioning"
msgstr "Versionierung" msgstr "Versionierung"
@ -708,12 +762,27 @@ msgstr "Teilnehmerregistrierung"
msgid "Register" msgid "Register"
msgstr "Benutzer registrieren" 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." msgid "Your old password was not entered correctly."
msgstr "Sie haben Ihr altes Passwort nicht korrekt eingegeben." msgstr "Sie haben Ihr altes Passwort nicht korrekt eingegeben."
msgid "Password and password confirmation do not match." msgid "Password and password confirmation do not match."
msgstr "Die Passwort-Wiederholung stimmt nicht mit dem eingegebenen Passwort überein." 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." msgid "Your password has been changed."
msgstr "Ihr Passwort wurde geändert." msgstr "Ihr Passwort wurde geändert."
@ -893,6 +962,9 @@ msgstr "Kalender"
msgid "Work Items" msgid "Work Items"
msgstr "Aktivitäten" msgstr "Aktivitäten"
msgid "Work Items for $title"
msgstr "Aktivitäten für $title"
msgid "Day" msgid "Day"
msgstr "Tag" msgstr "Tag"
@ -956,14 +1028,26 @@ msgid "Restrict to objects with certain states"
msgstr "Auf Objekte mit bestimmtem Status beschränken" msgstr "Auf Objekte mit bestimmtem Status beschränken"
msgid "Workflow" msgid "Workflow"
msgstr "Statusdefinition/Workflow" msgstr "Workflow"
msgid "States" msgid "States"
msgstr "Statuswerte" msgstr "Statuswerte"
msgid "States Definition"
msgstr "Workflowdefinition"
msgid "State Transition"
msgstr "Workflow-Statusänderung"
msgid "Transition"
msgstr "Aktion"
msgid "State information for $definition: $title" msgid "State information for $definition: $title"
msgstr "Status ($definition): $title" msgstr "Status ($definition): $title"
msgid "Available Transitions"
msgstr "Übergänge"
msgid "classification_quality" msgid "classification_quality"
msgstr "Klassifizierung" msgstr "Klassifizierung"
@ -976,6 +1060,12 @@ msgstr "Aufgabe"
msgid "publishable_task" msgid "publishable_task"
msgstr "Aufgabe/Zugriff" msgstr "Aufgabe/Zugriff"
msgid "label_transition_comments"
msgstr "Bemerkung"
msgid "desc_transition_comments"
msgstr "Notizen zum Statusübergang."
# state names # state names
msgid "accepted" msgid "accepted"

View file

@ -185,7 +185,7 @@ sure that a principal object can be served by a corresponding factory):
... 'lastName': u'Sawyer', ... 'lastName': u'Sawyer',
... 'firstName': u'Tom', ... 'firstName': u'Tom',
... 'email': u'tommy@sawyer.com', ... 'email': u'tommy@sawyer.com',
... 'action': 'update',} ... 'form.action': 'update',}
and register it. and register it.

View file

@ -27,6 +27,18 @@
class="loops.organize.browser.member.MemberRegistration" class="loops.organize.browser.member.MemberRegistration"
permission="zope.View" /> permission="zope.View" />
<browser:page
for="loops.interfaces.INode"
name="selfservice_registration.html"
class="loops.organize.browser.member.SecureMemberRegistration"
permission="zope.View" />
<browser:page
for="loops.interfaces.INode"
name="selfservice_confirmation.html"
class="loops.organize.browser.member.ConfirmMemberRegistration"
permission="zope.View" />
<browser:page <browser:page
for="loops.interfaces.INode" for="loops.interfaces.INode"
name="change_password.html" name="change_password.html"

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -19,10 +19,10 @@
""" """
Definition of view classes and other browser related stuff for Definition of view classes and other browser related stuff for
members (persons). members (persons).
$Id$
""" """
from datetime import datetime
from email.MIMEText import MIMEText
from zope import interface, component from zope import interface, component
from zope.app.authentication.principalfolder import InternalPrincipal from zope.app.authentication.principalfolder import InternalPrincipal
from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget
@ -31,6 +31,8 @@ from zope.app.principalannotation import annotations
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.i18nmessageid import MessageFactory from zope.i18nmessageid import MessageFactory
from zope.security import checkPermission from zope.security import checkPermission
from zope.sendmail.interfaces import IMailDelivery
from zope.traversing.browser import absoluteURL
from cybertools.composer.interfaces import IInstance from cybertools.composer.interfaces import IInstance
from cybertools.composer.schema.browser.common import schema_macros from cybertools.composer.schema.browser.common import schema_macros
@ -38,7 +40,8 @@ from cybertools.composer.schema.browser.form import Form, CreateForm
from cybertools.composer.schema.schema import FormState, FormError from cybertools.composer.schema.schema import FormState, FormError
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from loops.browser.common import concept_macros from cybertools.util.randomname import generateName
from loops.browser.common import concept_macros, form_macros
from loops.browser.concept import ConceptView, ConceptRelationView from loops.browser.concept import ConceptView, ConceptRelationView
from loops.browser.node import NodeView from loops.browser.node import NodeView
from loops.common import adapted from loops.common import adapted
@ -46,7 +49,7 @@ from loops.concept import Concept
from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager
from loops.organize.interfaces import IMemberRegistration, IPasswordChange from loops.organize.interfaces import IMemberRegistration, IPasswordChange
from loops.organize.party import getPersonForUser, Person from loops.organize.party import getPersonForUser, Person
from loops.organize.util import getInternalPrincipal from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
import loops.browser.util import loops.browser.util
from loops.util import _ from loops.util import _
@ -76,10 +79,11 @@ class PersonalInfo(ConceptView):
return self return self
class MemberRegistration(NodeView, CreateForm): class BaseMemberRegistration(NodeView):
interface = IMemberRegistration interface = IMemberRegistration # TODO: add company, create institution
message = _(u'The user account has been created.') message = _(u'The user account has been created.')
template = form_macros
formErrors = dict( formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')), confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')),
@ -88,10 +92,23 @@ class MemberRegistration(NodeView, CreateForm):
label = _(u'Member Registration') label = _(u'Member Registration')
label_submit = _(u'Register') label_submit = _(u'Register')
title = _('Member Registration')
permissions_key = u'registration.permissions' permissions_key = u'registration.permissions'
roles_key = u'registration.roles' roles_key = u'registration.roles'
registration_adapter_key = u'registration.adapter' registration_adapter_key = u'registration.adapter'
text_names_prefix = 'organize.member.registration'
# texts: reg_info, reg_feedback, conf_mail, conf_info, conf_feedback
info_key = 'reg_info'
feedback_key = 'reg_feedback'
isInnerHtml = False
showAssignments = False
form_action = 'register'
versionInfo = None
def closeAction(self, submit=True):
return u''
@Lazy @Lazy
def macro(self): def macro(self):
@ -99,7 +116,7 @@ class MemberRegistration(NodeView, CreateForm):
def checkPermissions(self): def checkPermissions(self):
personType = adapted(self.conceptManager['person']) personType = adapted(self.conceptManager['person'])
perms = IOptions(personType)('registration.permission') perms = IOptions(personType)(self.permissions_key)
if perms: if perms:
return checkPermission(perms[0], self.context) return checkPermission(perms[0], self.context)
return checkPermission('loops.ManageSite', self.context) return checkPermission('loops.ManageSite', self.context)
@ -108,12 +125,38 @@ class MemberRegistration(NodeView, CreateForm):
def item(self): def item(self):
return self return self
@Lazy
def data(self):
return self.request.form
def getPrincipalAnnotation(self, principal):
return annotations(principal).get(ANNOTATION_KEY, None)
@Lazy
def infoText(self):
name = '.'.join((self.text_names_prefix, self.info_key))
text = self.resourceManager.get(name)
if text:
return self.renderText(text.data)
return u''
@Lazy
def feedbackUrl(self):
name = '.'.join((self.text_names_prefix, self.feedback_key))
text = self.resourceManager.get(name)
if text:
return self.getUrlForTarget(text)
class MemberRegistration(BaseMemberRegistration, CreateForm):
@Lazy @Lazy
def schema(self): def schema(self):
schema = super(MemberRegistration, self).schema schema = super(MemberRegistration, self).schema
schema.fields.remove('birthDate') schema.fields.remove('birthDate')
schema.fields.reorder(-2, 'loginName') schema.fields.reorder(-2, 'loginName')
return schema return schema
# TODO: add company, create institution
@Lazy @Lazy
def object(self): def object(self):
@ -121,7 +164,7 @@ class MemberRegistration(NodeView, CreateForm):
def update(self): def update(self):
form = self.request.form form = self.request.form
if not form.get('action'): if not form.get('form.action'):
return True return True
instance = component.getAdapter(self.object, IInstance, name='editor') instance = component.getAdapter(self.object, IInstance, name='editor')
instance.template = self.schema instance.template = self.schema
@ -157,6 +200,165 @@ class MemberRegistration(NodeView, CreateForm):
return False return False
class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
permissions_key = u'secure_registration.permissions'
roles_key = u'secure_registration.roles'
email_key = 'reg_email'
@Lazy
def schema(self):
schema = super(SecureMemberRegistration, self).schema
schema.fields.remove('birthDate')
schema.fields.remove('password')
schema.fields.remove('passwordConfirm')
schema.fields.remove('phoneNumbers')
#schema.fields.reorder(-2, 'loginName')
return schema
@Lazy
def macro(self):
return organize_macros.macros['register']
@Lazy
def object(self):
return Person(Concept())
def update(self):
form = self.request.form
if not form.get('form.action'):
return True
instance = component.getAdapter(self.object, IInstance, name='editor')
instance.template = self.schema
self.formState = formState = instance.applyTemplate(data=form,
fieldHandlers=self.fieldHandlers)
if formState.severity > 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): class PasswordChange(NodeView, Form):
interface = IPasswordChange interface = IPasswordChange

View file

@ -1,5 +1,42 @@
<html i18n:domain="loops"> <html i18n:domain="loops">
<metal:block define-macro="register">
<metal:data use-macro="view/form_macros/edit">
<metal:custom fill-slot="custom_header">
<tbody>
<tr><td colspan="5">
<tal:info content="structure item/infoText" />
</td></tr>
</tbody>
</metal:custom>
</metal:data>
</metal:block>
<metal:block define-macro="confirm">
<metal:data use-macro="view/form_macros/edit">
<metal:custom fill-slot="custom_header">
<tbody>
<tr><td colspan="5">
<tal:info content="structure item/infoText" />
</td></tr>
<tr><td colspan="5">
<input type="hidden" name="login"
tal:attributes="value item/data/loginName" />
<input type="hidden" name="id"
tal:attributes="value item/data/id" />
<table><tr>
<td i18n:translate="">Login Name</td>
<td tal:content="item/data/loginName" />
</tr></table>
</td></tr>
</tbody>
</metal:custom>
</metal:data>
</metal:block>
<metal:task define-macro="task"> <metal:task define-macro="task">
<metal:data use-macro="view/concept_macros/conceptdata"> <metal:data use-macro="view/concept_macros/conceptdata">
</metal:data> </metal:data>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -62,19 +62,29 @@ class MemberRegistrationManager(object):
def __init__(self, context): def __init__(self, context):
self.context = 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'', def register(self, userId, password, lastName, firstName=u'',
groups=[], useExisting=False, pfName=None, **kw): groups=[], useExisting=False, pfName=None, **kw):
concepts = self.context.getConceptManager() options = IOptions(self.personType)
personType = adapted(concepts[self.person_typeName])
options = IOptions(personType)
if pfName is None: if pfName is None:
pfName = options(self.principalfolder_key, pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0] (self.default_principalfolder,))[0]
self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting) self.createPrincipal(pfName, userId, password, lastName, firstName,
if len(groups)==0: useExisting=useExisting)
if not groups:
groups = options(self.groups_key, ()) groups = options(self.groups_key, ())
self.setGroupsForPrincipal(pfName, userId, groups=groups) self.setGroupsForPrincipal(pfName, userId, groups=groups)
self.createPersonForPrincipal(pfName, userId, lastName, firstName, return self.createPersonForPrincipal(pfName, userId, lastName, firstName,
useExisting, **kw) useExisting, **kw)
def createPrincipal(self, pfName, userId, password, lastName, def createPrincipal(self, pfName, userId, password, lastName,

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Basic implementations for stateful objects and adapters. Basic implementations for stateful objects and adapters.
$Id$
""" """
from zope.app.catalog.interfaces import ICatalog from zope.app.catalog.interfaces import ICatalog
@ -27,6 +25,7 @@ from zope.cachedescriptors.property import Lazy
from zope import component from zope import component
from zope.component import adapts, adapter from zope.component import adapts, adapter
from cybertools.composer.schema.field import Field
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.stateful.base import Stateful as BaseStateful from cybertools.stateful.base import Stateful as BaseStateful
from cybertools.stateful.base import StatefulAdapter, IndexInfo 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.common import adapted
from loops.interfaces import ILoopsObject, IConcept, IResource from loops.interfaces import ILoopsObject, IConcept, IResource
from loops import util from loops import util
from loops.util import _
class Stateful(BaseStateful): class Stateful(BaseStateful):
@ -93,3 +93,10 @@ def handleTransition(obj, event):
if next != previous: if next != previous:
cat = component.getUtility(ICatalog) cat = component.getUtility(ICatalog)
cat.index_doc(int(util.getUidForObject(obj)), obj) 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)

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -23,12 +23,16 @@ Views and actions for states management.
from zope import component from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.event import notify
from zope.i18n import translate from zope.i18n import translate
from zope.lifecycleevent import ObjectModifiedEvent, Attributes
from cybertools.browser.action import Action, actions from cybertools.browser.action import Action, actions
from cybertools.composer.schema.schema import Schema
from cybertools.stateful.interfaces import IStateful, IStatesDefinition from cybertools.stateful.interfaces import IStateful, IStatesDefinition
from loops.browser.common import BaseView from loops.browser.common import BaseView
from loops.browser.concept import ConceptView 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.query import And, Or, State, Type, getObjects
from loops.expert.browser.search import search_template from loops.expert.browser.search import search_template
from loops.security.common import checkPermission from loops.security.common import checkPermission
@ -43,6 +47,16 @@ statefulActions = ('classification_quality',
'publishable_task',) '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): class StateAction(Action):
url = None url = None
@ -67,22 +81,92 @@ class StateAction(Action):
@Lazy @Lazy
def icon(self): def icon(self):
icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color return self.stateObject.stateIcon
return 'cybertools.icons/' + icon #icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color
#return 'cybertools.icons/' + icon
for std in statefulActions: for std in statefulActions:
actions.register('state.' + std, 'object', StateAction, actions.register('state.' + std, 'object', StateAction,
definition = std, definition=std,
cssClass='icon-action', 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(ConceptView):
class StateQuery(BaseView): class StateQuery(BaseView):
template = template template = template
form_action = 'execute_search_action' form_action = 'execute_search_action'
@Lazy @Lazy

View file

@ -77,7 +77,7 @@
set_schema="cybertools.stateful.interfaces.IStateful" /> set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class> </zope:class>
<!-- views --> <!-- views and form controllers -->
<browser:page <browser:page
for="loops.interfaces.IConcept" for="loops.interfaces.IConcept"
@ -91,6 +91,19 @@
class="loops.organize.stateful.browser.FilterAllStates" class="loops.organize.stateful.browser.FilterAllStates"
permission="zope.View" /> permission="zope.View" />
<browser:page
name="change_state.html"
for="loops.interfaces.INode"
class="loops.organize.stateful.browser.ChangeStateForm"
permission="zope.ManageContent" />
<zope:adapter
name="change_state"
for="loops.browser.node.NodeView
zope.publisher.interfaces.browser.IBrowserRequest"
factory="loops.organize.stateful.browser.ChangeState"
permission="zope.ManageContent" />
<!-- event handlers --> <!-- event handlers -->
<zope:subscriber handler="loops.organize.stateful.base.handleTransition" /> <zope:subscriber handler="loops.organize.stateful.base.handleTransition" />

View file

@ -26,12 +26,15 @@ from zope.component import adapter
from zope.interface import implementer from zope.interface import implementer
from zope.traversing.api import getName from zope.traversing.api import getName
from cybertools.composer.schema.schema import Schema
from cybertools.stateful.definition import StatesDefinition from cybertools.stateful.definition import StatesDefinition
from cybertools.stateful.definition import State, Transition from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition, IStateful from cybertools.stateful.interfaces import IStatesDefinition, IStateful
from loops.common import adapted from loops.common import adapted
from loops.organize.stateful.base import commentsField
from loops.organize.stateful.base import StatefulLoopsObject from loops.organize.stateful.base import StatefulLoopsObject
from loops.security.interfaces import ISecuritySetter from loops.security.interfaces import ISecuritySetter
from loops.util import _
def setPermissionsForRoles(settings): def setPermissionsForRoles(settings):
@ -42,6 +45,10 @@ def setPermissionsForRoles(settings):
return setSecurity return setSecurity
defaultSchema = Schema(commentsField,
name='change_state')
@implementer(IStatesDefinition) @implementer(IStatesDefinition)
def taskStates(): def taskStates():
return StatesDefinition('task_states', return StatesDefinition('task_states',
@ -55,10 +62,11 @@ def taskStates():
color='x'), color='x'),
State('archived', 'archived', ('reopen',), State('archived', 'archived', ('reopen',),
color='grey'), color='grey'),
Transition('release', 'release', 'active'), Transition('release', 'release', 'active', schema=defaultSchema),
Transition('finish', 'finish', 'finished'), Transition('finish', 'finish', 'finished', schema=defaultSchema),
Transition('cancel', 'cancel', 'cancelled'), Transition('cancel', 'cancel', 'cancelled', schema=defaultSchema),
Transition('reopen', 're-open', 'draft'), Transition('reopen', 're-open', 'draft', schema=defaultSchema),
Transition('archive', 'archive', 'archived', schema=defaultSchema),
initialState='draft') initialState='draft')

View file

@ -68,4 +68,82 @@
</metal:query> </metal:query>
<metal:actions define-macro="portlet_states">
<div tal:repeat="stf macro/stfs">
<div tal:condition="python:len(macro.stfs) > 1">
<span i18n:translate="">States Definition</span>
<span i18n:translate=""
tal:content="stf/statesDefinition" />
</div>
<div>
<b i18n:translate="">State</b>:
<span i18n:translate=""
tal:content="stf/state" />
<img style="margin-bottom: -3px"
tal:define="stateObject stf/getStateObject"
tal:attributes="src string:$resourceBase/${stateObject/stateIcon}" />
</div>
<div><b i18n:translate="">Available Transitions</b>:
<ul>
<li tal:repeat="action stf/getAvailableTransitionsForUser">
<a i18n:translate=""
tal:define="baseUrl view/virtualTargetUrl;
url string:$baseUrl/change_state.html?action=${action/name}&stdef=${stf/statesDefinition}"
tal:attributes="href url;
onClick string:objectDialog('change_state', '$url');;
return false;"
tal:content="action/title" />
</li>
</ul>
</div>
</div>
</metal:actions>
<metal:dialog define-macro="change_state">
<form name="stateful_changeState" method="post">
<div dojoType="dijit.layout.BorderContainer"
style="width: 70em; height: 600px">
<div dojoType="dijit.layout.ContentPane" region="top">
<h1><span i18n:translate="">State Transition</span> -
<span tal:content="view/title" />
</h1>
<div>
<span i18n:translate="">State</span>:
<span i18n:translate=""
tal:define="stateObject view/stateful/getStateObject"
tal:content="stateObject/title" /> -
<span i18n:translate="">Transition</span>:
<span i18n:translate=""
tal:content="view/transition/title" />
</div>
<input type="hidden" name="form.action" value="change_state">
<input type="hidden" name="stdef"
tal:attributes="value request/form/stdef|nothing">
<input type="hidden" name="action"
tal:attributes="value request/form/action|nothing">
</div>
<div dojoType="dijit.layout.ContentPane" region="center">
<table cellpadding="3" class="form">
<tbody><tr><td colspan="5" style="padding-right: 15px">
<div id="form.fields">
<metal:fields use-macro="view/fieldRenderers/fields" />
</div>
</td></tr></tbody>
</table>
</div>
<div dojoType="dijit.layout.ContentPane" region="bottom">
<metal:buttons define-slot="buttons">
<input value="Save" type="submit"
onClick="submit();; return false"
i18n:attributes="value">
<input type="button" value="Cancel" onClick="dialog.hide();"
i18n:attributes="value">
</metal:buttons>
</div>
</div>
</form>
</metal:dialog>
</html> </html>

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,10 +18,9 @@
""" """
Base class(es) for track/record managers. Base class(es) for track/record managers.
$Id$
""" """
from zope.app.security.interfaces import IUnauthenticatedPrincipal
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
@ -46,6 +45,10 @@ class BaseRecordManager(object):
def loopsRoot(self): def loopsRoot(self):
return self.context.getLoopsRoot() return self.context.getLoopsRoot()
@Lazy
def uid(self):
return util.getUidForObject(self.context)
@Lazy @Lazy
def storage(self): def storage(self):
records = self.loopsRoot.getRecordManager() records = self.loopsRoot.getRecordManager()
@ -63,6 +66,8 @@ class BaseRecordManager(object):
else: else:
principal = getPrincipalForUserId(userId, context=self.context) principal = getPrincipalForUserId(userId, context=self.context)
if principal is not None: if principal is not None:
if IUnauthenticatedPrincipal.providedBy(principal):
return None
person = getPersonForUser(self.context, principal=principal) person = getPersonForUser(self.context, principal=principal)
if person is None: if person is None:
return principal.id return principal.id

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Recording changes to loops objects. Recording changes to loops objects.
$Id$
""" """
from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent from zope.app.container.interfaces import IObjectAddedEvent, IObjectRemovedEvent
@ -53,6 +51,9 @@ class ChangeManager(BaseRecordManager):
@Lazy @Lazy
def valid(self): def valid(self):
req = util.getRequest()
if req and req.form.get('organize.suppress_tracking'):
return False
return (not (self.context is None or return (not (self.context is None or
self.storage is None or self.storage is None or
self.personId is None) self.personId is None)
@ -70,6 +71,12 @@ class ChangeManager(BaseRecordManager):
if relation is not None: if relation is not None:
data['predicate'] = util.getUidForObject(relation.predicate) data['predicate'] = util.getUidForObject(relation.predicate)
data['second'] = util.getUidForObject(relation.second) 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: if update:
self.storage.updateTrack(last, data) self.storage.updateTrack(last, data)
else: else:
@ -90,16 +97,18 @@ class ChangeRecord(Track):
@adapter(ILoopsObject, IObjectModifiedEvent) @adapter(ILoopsObject, IObjectModifiedEvent)
def recordModification(obj, event): def recordModification(obj, event):
ChangeManager(obj).recordModification() ChangeManager(obj).recordModification(event=event)
@adapter(ILoopsObject, IObjectAddedEvent) @adapter(ILoopsObject, IObjectAddedEvent)
def recordAdding(obj, event): def recordAdding(obj, event):
ChangeManager(obj).recordModification('add') ChangeManager(obj).recordModification('add', event=event)
@adapter(ILoopsObject, IAssignmentEvent) @adapter(ILoopsObject, IAssignmentEvent)
def recordAssignment(obj, event): def recordAssignment(obj, event):
ChangeManager(obj).recordModification('assign', relation=event.relation) ChangeManager(obj).recordModification('assign',
event=event, relation=event.relation)
@adapter(ILoopsObject, IDeassignmentEvent) @adapter(ILoopsObject, IDeassignmentEvent)
def recordDeassignment(obj, event): def recordDeassignment(obj, event):
ChangeManager(obj).recordModification('deassign', relation=event.relation) ChangeManager(obj).recordModification('deassign',
event=event, relation=event.relation)

View file

@ -43,13 +43,14 @@ from loops.browser.concept import ConceptView
from loops.browser.form import ObjectForm, EditObject from loops.browser.form import ObjectForm, EditObject
from loops.browser.node import NodeView from loops.browser.node import NodeView
from loops.common import adapted from loops.common import adapted
from loops.organize.interfaces import IPerson
from loops.organize.party import getPersonForUser from loops.organize.party import getPersonForUser
from loops.organize.stateful.browser import StateAction from loops.organize.stateful.browser import StateAction
from loops.organize.tracking.browser import BaseTrackView from loops.organize.tracking.browser import BaseTrackView
from loops.organize.tracking.report import TrackDetails from loops.organize.tracking.report import TrackDetails
from loops.organize.work.base import WorkItem from loops.organize.work.base import WorkItem
from loops.security.common import canAccessObject, canListObject, canWriteObject 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 import util
from loops.util import _ from loops.util import _
@ -228,6 +229,10 @@ class BaseWorkItemsView(object):
def macro(self): def macro(self):
return self.work_macros['workitems_query'] return self.work_macros['workitems_query']
@Lazy
def title(self):
return _(u'Work Items for $title', mapping=dict(title=self.context.title))
@Lazy @Lazy
def workItems(self): def workItems(self):
rm = self.loopsRoot.getRecordManager() rm = self.loopsRoot.getRecordManager()
@ -312,18 +317,24 @@ class RelatedTaskWorkItems(AllWorkItems):
class PersonWorkItems(BaseWorkItemsView, ConceptView): 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']) columns = set(['Task', 'Title', 'Day', 'Start', 'End', 'Duration', 'Info'])
def checkPermissions(self):
return canAccessRestricted(self.context)
def getCriteria(self): def getCriteria(self):
return self.baseCriteria return self.baseCriteria
def listWorkItems(self): def listWorkItems(self):
criteria = self.getCriteria() criteria = self.getCriteria()
for target in self.context.getParents([self.defaultPredicate]):
un = criteria.setdefault('userName', []) 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)) un.append(util.getUidForObject(target))
return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp) return sorted(self.query(**criteria), key=lambda x: x.track.timeStamp)

View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Query management stuff. Query management stuff.
$Id$
""" """
from BTrees.IOBTree import IOBTree from BTrees.IOBTree import IOBTree
@ -33,6 +31,7 @@ from zope.cachedescriptors.property import Lazy
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from loops.common import AdapterBase from loops.common import AdapterBase
from loops.interfaces import IConcept, IConceptSchema, ILoopsAdapter from loops.interfaces import IConcept, IConceptSchema, ILoopsAdapter
from loops.interfaces import IOptions
from loops.security.common import canListObject from loops.security.common import canListObject
from loops.type import TypeInterfaceSourceList from loops.type import TypeInterfaceSourceList
from loops.versioning.util import getVersion from loops.versioning.util import getVersion
@ -182,7 +181,7 @@ class ConceptQuery(BaseQuery):
# QueryConcept: concept objects that allow querying the database. # QueryConcept: concept objects that allow querying the database.
class IQueryConcept(IConceptSchema, ILoopsAdapter): class IQueryConcept(IConceptSchema, ILoopsAdapter, IOptions):
""" The schema for the query type. """ The schema for the query type.
""" """
@ -194,13 +193,6 @@ class IQueryConcept(IConceptSchema, ILoopsAdapter):
default=u'', default=u'',
required=False) required=False)
options = schema.List(
title=_(u'Options'),
description=_(u'Additional settings.'),
value_type=schema.TextLine(),
default=[],
required=False)
class QueryConcept(AdapterBase): class QueryConcept(AdapterBase):

View file

@ -74,6 +74,9 @@ def canListObject(obj, noCheck=False):
return True return True
return canAccess(obj, 'title') return canAccess(obj, 'title')
def canAccessRestricted(obj):
return checkPermission('loops.ViewRestricted', obj)
def canWriteObject(obj): def canWriteObject(obj):
return canWrite(obj, 'title') or canAssignAsParent(obj) return canWrite(obj, 'title') or canAssignAsParent(obj)

23
type.py
View file

@ -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 # This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
""" """
Type management stuff. Type management stuff.
$Id$
""" """
from zope import component, schema from zope import component, schema
@ -28,12 +26,13 @@ from zope.interface import implements
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.dottedname.resolve import resolve from zope.dottedname.resolve import resolve
from zope.security.proxy import removeSecurityProxy 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.type import BaseType, TypeManager
from cybertools.typology.interfaces import ITypeManager from cybertools.typology.interfaces import ITypeManager
from loops.interfaces import ILoopsObject, IConcept, IResource from loops.interfaces import ILoopsObject, IConcept, IResource
from loops.interfaces import ITypeConcept from loops.interfaces import ITypeConcept
from loops.interfaces import IOptions
from loops.interfaces import IResourceAdapter, IFile, IExternalFile, IImage from loops.interfaces import IResourceAdapter, IFile, IExternalFile, IImage
from loops.interfaces import ITextDocument, INote from loops.interfaces import ITextDocument, INote
from loops.concept import Concept from loops.concept import Concept
@ -49,10 +48,15 @@ class LoopsType(BaseType):
#document=Document) #document=Document)
containerMapping = dict(concept='concepts', resource='resources') containerMapping = dict(concept='concepts', resource='resources')
isForeignReference = False
@Lazy @Lazy
def title(self): def title(self):
tp = self.typeProvider 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 @Lazy
def token(self): def token(self):
@ -63,7 +67,11 @@ class LoopsType(BaseType):
def tokenForSearch(self): def tokenForSearch(self):
tp = self.typeProvider tp = self.typeProvider
typeName = tp is None and 'unknown' or str(getName(tp)) 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 @Lazy
def typeInterface(self): def typeInterface(self):
@ -272,7 +280,8 @@ class TypeInterfaceSourceList(object):
implements(schema.interfaces.IIterableSource) implements(schema.interfaces.IIterableSource)
typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote) typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote,
IOptions)
def __init__(self, context): def __init__(self, context):
self.context = context self.context = context

View file

@ -141,4 +141,7 @@ def saveRequest(request):
local_data.request = request local_data.request = request
def getRequest(): def getRequest():
try:
return local_data.request return local_data.request
except AttributeError:
return None