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.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL
from zope.traversing.api import getName, getParent
from zope.traversing.api import getName, getParent, traverse
from cybertools.ajax.dojo import dojoMacroTemplate
from cybertools.browser.view import GenericView
@ -70,7 +70,7 @@ from loops.organize.tracking import access
from loops.resource import Resource
from loops.security.common import checkPermission
from loops.security.common import canAccessObject, canListObject, canWriteObject
from loops.type import ITypeConcept
from loops.type import ITypeConcept, LoopsTypeInfo
from loops import util
from loops.util import _, saveRequest
from loops import version
@ -481,7 +481,7 @@ class BaseView(GenericView, I18NView):
return absoluteURL(provider, self.request)
return None
def renderText(self, text, contentType):
def renderText(self, text, contentType='text/restructured'):
text = util.toUnicode(text)
typeKey = util.renderingFactories.get(contentType, None)
if typeKey is None:
@ -531,11 +531,29 @@ class BaseView(GenericView, I18NView):
def conceptTypes(self):
return util.KeywordVocabulary(self.listTypes(('concept',), ('hidden',)))
def parentTypesFromOtherSites(self):
result = []
typeNames = self.typeOptions('foreign_parent_types') or []
for path in self.typeOptions('foreign_parent_sites') or []:
site = traverse(self.loopsRoot, path, None)
if site is None:
continue
cm = site.getConceptManager()
for tname in typeNames:
t = cm.get(tname)
if t is not None:
type = LoopsTypeInfo(t)
type.isForeignReference = True
result.append(type)
return result
def listTypesForSearch(self, include=None, exclude=None, sortOn='title'):
types = [dict(token=t.tokenForSearch, title=t.title)
for t in ITypeManager(self.context).listTypes(include, exclude)]
if sortOn:
types.sort(key=lambda x: x[sortOn])
for t in self.parentTypesFromOtherSites():
types.append(dict(token=t.tokenForSearch, title=t.title))
return types
def typesForSearch(self):

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
# it under the terms of the GNU General Public License as published by
@ -50,6 +50,7 @@ from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType, ITypeManager
from cybertools.util.jeep import Jeep
from loops.browser.common import EditForm, BaseView, LoopsTerms, concept_macros
from loops.browser.common import ViewMode
from loops.common import adapted
from loops.concept import Concept, ConceptTypeSourceList, PredicateSourceList
from loops.i18n.browser import I18NView
@ -196,6 +197,12 @@ class BaseRelationView(BaseView):
return u''
return self.predicateTitle
@Lazy
def relationInfo(self):
predInfo = ', ' .join(p.title for p in self.predicates
if p != self.defaultPredicate)
return ' | '.join(t for t in (self.description, predInfo) if t)
class ConceptView(BaseView):
@ -228,6 +235,7 @@ class ConceptView(BaseView):
subMacro=concept_macros.macros['parents'],
priority=20, info=self)
# the part-based layout is now implemented in loops.browser.compound
def getParts(self):
parts = (self.params.get('parts') or []) # deprecated!
if not parts:
@ -740,3 +748,30 @@ class ListTypeInstances(ListChildren):
noDuplicates, useFilter, [self.typePredicate]):
yield c
class TabbedPage(ConceptView):
@Lazy
def subpagePredicates(self):
pred = self.conceptManager.get('issubpage')
if pred is None:
pred = self.isPartOfPredicate
return [pred]
def viewModes(self):
modes = Jeep()
for s in self.getSiblings(self.subpagePredicates):
url = self.nodeView.getUrlForTarget(s)
modes.append(ViewMode(getName(s), s.title, url))
if not modes:
return modes
modes[getName(self.context)].active = True
return modes
def getSiblings(self, preds):
for p in self.context.getParents(preds):
parent = p
break
else:
return []
return p.getChildren(preds)

View file

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

View file

@ -553,6 +553,14 @@
factory="loops.browser.concept.ListTypeInstances"
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) -->
<page
@ -757,6 +765,7 @@
attribute="cleanup"
permission="zope.ManageSite" />
<include package=".compound" />
<include package=".skin" />
<include package=".lobo" />
<include package=".mobile" />

View file

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

View file

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

View file

@ -1,4 +1,4 @@
/* $Id$ */
/* loops.js */
function openEditWindow(url) {
zmi = window.open(url, 'zmi');
@ -19,6 +19,12 @@ function toggleCheckBoxes(toggle, fieldName) {
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) {
// (work in progress) - may be used for onBlur event handler
var w = dojo.byId(nodeName);

View file

@ -195,7 +195,11 @@ class NodeView(BaseView):
subMacro=calendar_macros.macros['main'],
priority=90)
# 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
def usersPresent(self):
@ -381,6 +385,8 @@ class NodeView(BaseView):
ht = super(NodeView, self).headTitle
if ht not in parts:
parts.append(ht)
if self.globalOptions('reverseHeadTitle'):
parts.reverse()
return ' - ' .join(parts)
@Lazy

View file

@ -314,8 +314,13 @@
<metal:login define-macro="login">
<div><a href="login.html"
<div>
<a href="login.html"
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>

View file

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

View file

@ -30,7 +30,13 @@
<metal:tabs use-macro="views/node_macros/breadcrumbs" />
</metal:breadcrumbs>
<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:content define-slot="content">
<tal:content define="item nocall:view/item;

View file

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

View file

@ -113,6 +113,9 @@ class AdapterBase(object):
self.context = context
self.__parent__ = context # to get the permission stuff right
def __hash__(self):
return hash(self.context)
def __getattr__(self, attr):
self.checkAttr(attr)
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')
>>> 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

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
# 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.traversing.api import getName
from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType
from loops.browser.lobo import standard
from loops.browser.concept import ConceptView
@ -45,10 +46,22 @@ class Base(object):
def book_macros(self):
return book_template.macros
@Lazy
def documentTypeType(self):
return self.conceptManager['documenttype']
@Lazy
def sectionType(self):
return self.conceptManager['section']
@Lazy
def isPartOfPredicate(self):
return self.conceptManager['ispartof']
@Lazy
def showNavigation(self):
return self.typeOptions.show_navigation
@Lazy
def breadcrumbsParent(self):
for p in self.context.getParents([self.isPartOfPredicate]):
@ -82,34 +95,8 @@ class Base(object):
if self.editable:
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):
relViews = super(SectionView, self).getResources()
relViews = super(Base, self).getResources()
return relViews
@Lazy
@ -132,11 +119,36 @@ class SectionView(Base, ConceptView):
self.images[idx].append(img)
return result
def getCssClassForResource(self, r):
def getDocumentTypeForResource(self, r):
for c in r.context.getConcepts([self.defaultPredicate]):
if c.conceptType == self.documentTypeType:
return getName(c)
return 'textelement'
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'
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):
for c in r.context.getConcepts([self.defaultPredicate]):
@ -144,64 +156,23 @@ class SectionView(Base, ConceptView):
yield c
# layout parts - probably obsolete:
class BookView(Base, ConceptView):
class PageLayout(Base, standard.Layout):
def getParts(self):
parts = ['headline', 'keyquestions', 'quote', 'maintext',
'story', 'tip', 'usecase']
return self.getPartViews(parts)
@Lazy
def macro(self):
return book_template.macros['book']
class PagePart(object):
class SectionView(Base, ConceptView):
template = book_template
templateName = 'compound.book'
macroName = 'text'
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
@Lazy
def macro(self):
return book_template.macros['section']
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"
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 -->
<zope:adapter
@ -22,7 +10,7 @@
for="loops.interfaces.IConcept
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.BookOverview"
factory="loops.compound.book.browser.BookView"
permission="zope.View" />
<zope:adapter
@ -34,69 +22,11 @@
permission="zope.View" />
<zope:adapter
name="page_layout"
name="book_topic_view"
for="loops.interfaces.IConcept
loops.browser.skin.Lobo"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.PageLayout"
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"
factory="loops.compound.book.browser.TopicView"
permission="zope.View" />
</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',
typeInterface=u'loops.interfaces.IOptions',
viewName=u'')
# book types
type(u'book', u'Buch', viewName=u'book_overview', typeInterface=u'',
options=u'action.portlet:create_subtype,edit_concept')
#type(u'page', u'Seite', viewName=u'page_layout',
# typeInterface=u'loops.compound.book.interfaces.IPage',
# options=u'action.portlet:edit_concept')
type(u'section', u'Kapitel', viewName=u'section_view', typeInterface=u'',
options=u'action.portlet:create_subtype,edit_concept')
#type(u'topic', u'Thema', viewName=u'book_topic_view',
# typeInterface=u'loops.knowledge.interfaces.ITopic',
# options=u'action.portlet:create_topic,edit_topic')
concept(u'system', u'System', u'domain')
@ -26,8 +27,8 @@ concept(u'quote', u'Zitat', u'documenttype')
concept(u'story', u'Geschichte', u'documenttype')
concept(u'tip', u'Tipp', u'documenttype')
concept(u'usecase', u'Fallbeispiel', u'documenttype')
concept(u'warning', u'Warnung', u'documenttype')
# book structure
child(u'book', u'section', u'issubtype', usePredicate=u'ispartof')
child(u'section', u'section', u'issubtype', usePredicate=u'ispartof')
#child(u'section', u'page', u'issubtype', usePredicate=u'ispartof')

View file

@ -1,14 +1,22 @@
<html i18n:domain="loops">
<metal:book define-macro="book">
<metal:info use-macro="view/concept_macros/concepttitle" />
<div tal:repeat="related item/children">
<metal:children define-macro="children">
<div tal:repeat="related item/children"
tal:define="level python:level + 1"
tal:attributes="class string:content-$level">
<h3>
<a tal:attributes="href python:view.getUrlForTarget(related)"
tal:content="related/title" /></h3>
tal:content="related/title" />
</h3>
<div tal:content="structure related/renderedDescription" />
<!-- TODO: show next level (+/-) -->
</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>
@ -32,71 +40,93 @@
</div>
</metal:navigation>
<metal:info use-macro="view/concept_macros/concepttitle" />
<div tal:repeat="related item/textResources">
<div class="span-4">
<div tal:attributes="class python:
item.getCssClassForResource(related)"
tal:content="structure related/render" />
</div>
<div class="span-2 last" style="padding-top: 0.4em">
<div class="object-actions" style="padding-top: 0"
tal:define="url python:view.getUrlForTarget(related.context)"
tal:condition="related/editable">
<a i18n:translate="" i18n:attributes="title"
title="Edit"
tal:define="targetUid python:view.getUidForObject(related.context);
url
string:$url/edit_object.html?version=this&targetUid=$targetUid"
tal:attributes="href url;
onclick string:objectDialog('edit', '$url');;
return false">
<img tal:attributes="src
string:$resourceBase/cybertools.icons/vcard_edit.png" /></a>
<a i18n:translate="" i18n:attributes="title"
title="Edit with external editor."
xxtal:condition="related/xeditable"
tal:condition="nothing"
tal:attributes="href string:$url/external_edit?version=this">
<img tal:attributes="src
string:$resourceBase/cybertools.icons/application_edit.png" /></a>
<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 metal:define-macro="default_text"
tal:attributes="class python:
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 tal:repeat="parent python:item.getParentsForResource(related)">
<a tal:content="parent/title"
tal:attributes="href python:view.getUrlForTarget(parent)" />
</div>
<div tal:repeat="image python:
item.images[repeat['related'].index() + 1]">
<a dojoType="dojox.image.Lightbox" group="mediasset"
i18n:attributes="title"
tal:attributes="href image/fullImageUrl;
title image/title">
<img tal:attributes="src image/src;
alt image/title" /></a>
<div class="span-2 last" style="padding-top: 0.4em">
<div class="object-actions" style="padding-top: 0"
tal:define="url python:view.getUrlForTarget(related.context)"
tal:condition="related/editable">
<a i18n:translate="" i18n:attributes="title"
title="Edit"
tal:define="targetUid python:view.getUidForObject(related.context);
url
string:$url/edit_object.html?version=this&targetUid=$targetUid"
tal:attributes="href url;
onclick string:objectDialog('edit', '$url');;
return false">
<img tal:attributes="src
string:$resourceBase/cybertools.icons/vcard_edit.png" /></a>
<a i18n:translate="" i18n:attributes="title"
title="Edit with external editor."
xxtal:condition="related/xeditable"
tal:condition="nothing"
tal:attributes="href string:$url/external_edit?version=this">
<img tal:attributes="src
string:$resourceBase/cybertools.icons/application_edit.png" /></a>
</div>
<div tal:repeat="parent python:item.getParentsForResource(related)">
<a tal:content="parent/title"
tal:attributes="href python:view.getUrlForTarget(parent)" />
</div>
<div tal:repeat="image python:
item.images[repeat['related'].index() + 1]">
<a dojoType="dojox.image.Lightbox" group="mediasset"
i18n:attributes="title"
tal:attributes="href image/fullImageUrl;
title image/title">
<img tal:attributes="src image/src;
alt image/title" /></a>
</div>
<!-- TODO: links to files -->
</div>
</div>
</div>
</metal:text>
<br style="clear: both" />
<metal:navigation use-macro="item/book_macros/navigation" />
<br />
</metal:section>
<!-- layout part macros - obsolete? -->
<metal:part define-macro="headline">
<div tal:define="cell part/getView">
<metal:headline use-macro="item/macros/headline" />
</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" />
<metal:topic define-macro="topic">
<metal:info use-macro="view/concept_macros/concepttitle" />
<h2 i18n:translate=""
tal:condition="python: list(item.children())">Children</h2>
<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>
</tal:cell>
</metal:part>
</div>
</metal:topic>
</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
# it under the terms of the GNU General Public License as published by
@ -19,14 +19,12 @@
"""
Definition of basic view classes and other browser related stuff for the
loops.expert package.
$Id$
"""
from zope import interface, component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.traversing.api import getName, getParent
from zope.traversing.api import getName, getParent, traverse
from cybertools.browser.form import FormController
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
@ -150,12 +148,16 @@ class Search(ConceptView):
if not isinstance(types, (list, tuple)):
types = [types]
for type in types:
site = self.loopsRoot
if type.startswith('/'):
parts = type.split(':')
site = traverse(self.loopsRoot, parts[0], site)
result = self.executeQuery(title=title or None, type=type,
exclude=('hidden',))
fv = FilterView(self.context, self.request)
result = fv.apply(result)
for o in result:
if o.getLoopsRoot() == self.loopsRoot:
if o.getLoopsRoot() == site:
adObj = adapted(o, self.languageInfo)
if filterMethod is not None and not filterMethod(adObj):
continue

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

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

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

View file

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

View file

@ -4,7 +4,14 @@
i18n_domain="loops">
<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 -->

View file

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

View file

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

View file

@ -21,26 +21,40 @@ Definition of view classes and other browser related stuff for
surveys and self-assessments.
"""
import csv
from cStringIO import StringIO
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from cybertools.knowledge.survey.questionnaire import Response
from cybertools.util.date import formatTimeStamp
from loops.browser.concept import ConceptView
from loops.browser.node import NodeView
from loops.common import adapted
from loops.knowledge.survey.response import Responses
from loops.organize.party import getPersonForUser
from loops.util import getObjectForUid
from loops.util import _
template = ViewPageTemplateFile('view_macros.pt')
class SurveyView(ConceptView):
tabview = 'index.html'
data = None
errors = None
@Lazy
def macro(self):
self.registerDojo()
return template.macros['survey']
@Lazy
def tabview(self):
if self.editable:
return 'index.html'
def results(self):
result = []
response = None
@ -52,22 +66,114 @@ class SurveyView(ConceptView):
if key.startswith('question_'):
uid = key[len('question_'):]
question = adapted(self.getObjectForUid(uid))
value = int(value)
self.data[uid] = value
response.values[question] = value
# TODO: store self.data in track
# else:
# get response from track
if value != 'none':
value = int(value)
self.data[uid] = value
response.values[question] = value
Responses(self.context).save(self.data)
self.errors = self.check(response)
if self.errors:
return []
if response is not None:
result = response.getGroupedResult()
return [dict(category=r[0].title, text=r[1].text,
score=int(round(r[2] * 100)))
for r in result]
def getValues(self, question):
setting = 0
if self.data is not None:
setting = self.data.get(question.uid) or 0
return [dict(value=i, checked=(i == setting))
for i in range(question.answerRange)]
def check(self, response):
errors = []
values = response.values
for qu in self.adapted.questions:
if qu.required and qu not in values:
errors.append('Please answer the obligatory questions.')
break
qugroups = {}
for qugroup in self.adapted.questionGroups:
qugroups[qugroup] = 0
for qu in values:
qugroups[qu.questionGroup] += 1
for qugroup, count in qugroups.items():
minAnswers = qugroup.minAnswers
if minAnswers in (u'', None):
minAnswers = len(qugroup.questions)
if count < minAnswers:
errors.append('Please answer the minimum number of questions.')
break
return errors
def getInfoText(self, qugroup):
lang = self.languageInfo.language
text = qugroup.description
info = None
if qugroup.minAnswers in (u'', None):
info = translate(_(u'Please answer all questions.'), target_language=lang)
elif qugroup.minAnswers > 0:
info = translate(_(u'Please answer at least $minAnswers questions.',
mapping=dict(minAnswers=qugroup.minAnswers)),
target_language=lang)
if info:
text = u'<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
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
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
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
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 -->
@ -31,4 +68,9 @@
factory="loops.knowledge.survey.browser.SurveyView"
permission="zope.View" />
<browser:page name="survey_data.csv"
for="loops.interfaces.IView"
class="loops.knowledge.survey.browser.SurveyCsvExport"
permission="zope.View" />
</configure>

View file

@ -24,7 +24,7 @@ from zope.interface import Interface, Attribute
from zope import interface, component, schema
from cybertools.knowledge.survey import interfaces
from loops.interfaces import IConceptSchema
from loops.interfaces import IConceptSchema, ILoopsAdapter
from loops.util import _
@ -38,16 +38,43 @@ class IQuestionnaire(IConceptSchema, interfaces.IQuestionnaire):
default=4,
required=True)
feedbackHeader = schema.Text(
title=_(u'Feedback Header'),
description=_(u'Text that will appear at the top of the feedback page.'),
default=u'',
missing_value=u'',
required=False)
feedbackFooter = schema.Text(
title=_(u'Feedback Footer'),
description=_(u'Text that will appear at the end of the feedback page.'),
default=u'',
missing_value=u'',
required=False)
class IQuestionGroup(IConceptSchema, interfaces.IQuestionGroup):
""" A group of questions within a questionnaire.
"""
minAnswers = schema.Int(
title=_(u'Minimum Number of Answers'),
description=_(u'Minumum number of questions that have to be answered. '
'Empty means all questions have to be answered.'),
default=None,
required=False)
class IQuestion(IConceptSchema, interfaces.IQuestion):
""" A single question within a questionnaire.
"""
required = schema.Bool(
title=_(u'Required'),
description=_(u'Question must be answered.'),
default=False,
required=False)
revertAnswerOptions = schema.Bool(
title=_(u'Negative'),
description=_(u'Value inversion: High selection means low value.'),

View file

@ -29,20 +29,34 @@ from loops.knowledge.survey.interfaces import IResponse, IResponses
from loops.organize.tracking.base import BaseRecordManager
class Responses(BaseRecordManager):
implements(IResponses)
storageName = 'survey_responses'
def __init__(self, context):
self.context = context
def save(self, data):
if self.personId:
self.storage.saveUserTrack(self.uid, 0, self.personId, data,
update=True, overwrite=True)
def load(self):
if self.personId:
tracks = self.storage.getUserTracks(self.uid, 0, self.personId)
if tracks:
return tracks[0].data
return {}
def getAllTracks(self):
return self.storage.query(taskId=self.uid)
class Response(Track):
""" A survey response.
"""
implements(IResponse)
typeName = 'Response'
typeInterface = IResponse
class Responses(BaseRecordManager):
""" A tracking storage adapter for survey responses.
"""
implements(IResponses)
adapts(ITrackingStorage)

View file

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

View file

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

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

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

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: 0.13.0\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
"PO-Revision-Date: 2013-03-07 12:00 CET\n"
"PO-Revision-Date: 2013-07-15 12:00 CET\n"
"Last-Translator: Helmut Merz <helmutm@cy55.de>\n"
"Language-Team: loops developers <helmutm@cy55.de>\n"
"MIME-Version: 1.0\n"
@ -86,6 +86,9 @@ msgstr "Thema bearbeiten..."
msgid "Modify topic."
msgstr "Thema ändern"
msgid "Please correct the indicated errors."
msgstr "Bitte berichtigen Sie die angezeigten Fehler."
# blog
msgid "Edit Blog Post..."
@ -175,11 +178,29 @@ msgstr "Glossareintrag anlegen."
msgid "Answer Range"
msgstr "Abstufung Bewertungen"
msgid "Feedback Footer"
msgstr "Auswertungs-Hinweis"
msgid "Text that will appear at the end of the feedback page."
msgstr "Text, der am Ende der Auswertungsseite erscheinen soll."
msgid "Number of items (answer options) to select from."
msgstr "Anzahl der Abstufungen, aus denen bei der Antwort gewählt werden kann."
msgid "Negativ"
msgstr "Negativbewertung"
msgid "Minimum Number of Answers"
msgstr "Mindestanzahl an Antworten"
msgid "Minumum number of questions that have to be answered. Empty means all questions have to be answered."
msgstr "Anzahl der Fragen, die mindestens beantwortet werden müssen. Keine Angabe: Es müssen alle Fragen beantwortet werden."
msgid "Required"
msgstr "Pflichtfrage"
msgid "Question must be answered."
msgstr "Frage muss unbedingt beantwortet werden."
msgid "Negative"
msgstr "Negative Polarität"
msgid "Value inversion: High selection means low value."
msgstr "Invertierung der Bewertung: Hohe gewählte Stufe bedeutet niedriger Wert."
@ -196,27 +217,54 @@ msgstr "Kategorie"
msgid "Response"
msgstr "Beurteilung"
msgid "No answer"
msgstr "Keine Antwort"
msgid "Does not apply"
msgstr "Trifft nicht zu"
msgid "Fully applies"
msgstr "Trifft voll zu"
msgid "survey_value_none"
msgstr "Keine Antwort"
msgid "survey_value_0"
msgstr "trifft für unser Unternehmen überhaupt nicht zu"
msgstr "Trifft für unser Unternehmen überhaupt nicht zu"
msgid "survey_value_1"
msgstr "trifft eher nicht zu"
msgstr "Trifft eher nicht zu"
msgid "survey_value_2"
msgstr "trifft eher zu"
msgstr "Trifft eher zu"
msgid "survey_value_3"
msgstr "trifft für unser Unternehmen voll und ganz zu"
msgstr "Trifft für unser Unternehmen voll und ganz zu"
msgid "Evaluate Questionnaire"
msgstr "Fragebogen auswerten"
msgid "Reset Responses Entered"
msgstr "Eingaben zurücksetzen"
msgid "Back to Questionnaire"
msgstr "Zurück zum Fragebogen"
msgid "Please answer at least $minAnswers questions."
msgstr "Bitte beantworten Sie mindestens $minAnswers Fragen."
msgid "Please answer all questions."
msgstr "Bitte beantworten Sie alle Fragen."
msgid "Please answer the obligatory questions."
msgstr "Bitte beantworten Sie die Pflichtfragen."
msgid "Please answer the minimum number of questions."
msgstr "Bitte beantworten Sie die angegebene Mindestanzahl an Fragen je Fragengruppe."
msgid "Obligatory question, must be answered"
msgstr "Pflichtfrage, muss beantwortet werden"
# competence (qualification)
msgid "Validity Period (Months)"
@ -495,6 +543,9 @@ msgstr "Unterbegriffe"
msgid "Resources"
msgstr "Ressourcen"
msgid "Text Elements"
msgstr "Texte"
msgid "Title"
msgstr "Titel"
@ -660,6 +711,9 @@ msgstr "Zugeordnete Begriffe"
msgid "more..."
msgstr "Mehr..."
msgid "More..."
msgstr "Mehr..."
msgid "Versioning"
msgstr "Versionierung"
@ -708,12 +762,27 @@ msgstr "Teilnehmerregistrierung"
msgid "Register"
msgstr "Benutzer registrieren"
msgid "Register new member"
msgstr "Neu registrieren"
msgid "Login name already taken."
msgstr "Die von Ihnen eingegebene Benutzerkennung ist schon vergeben."
msgid "Your old password was not entered correctly."
msgstr "Sie haben Ihr altes Passwort nicht korrekt eingegeben."
msgid "Password and password confirmation do not match."
msgstr "Die Passwort-Wiederholung stimmt nicht mit dem eingegebenen Passwort überein."
msgid "confirmation_mail_subject"
msgstr "Benutzer-Registrierung"
msgid "confirmation_mail_text"
msgstr "Bitte clicken Sie auf den folgenden Link, um die Anmeldung abzuschließen."
msgid "The user account has been created."
msgstr "Ihr Benutzerkonto wurde eingerichtet."
msgid "Your password has been changed."
msgstr "Ihr Passwort wurde geändert."
@ -893,6 +962,9 @@ msgstr "Kalender"
msgid "Work Items"
msgstr "Aktivitäten"
msgid "Work Items for $title"
msgstr "Aktivitäten für $title"
msgid "Day"
msgstr "Tag"
@ -956,14 +1028,26 @@ msgid "Restrict to objects with certain states"
msgstr "Auf Objekte mit bestimmtem Status beschränken"
msgid "Workflow"
msgstr "Statusdefinition/Workflow"
msgstr "Workflow"
msgid "States"
msgstr "Statuswerte"
msgid "States Definition"
msgstr "Workflowdefinition"
msgid "State Transition"
msgstr "Workflow-Statusänderung"
msgid "Transition"
msgstr "Aktion"
msgid "State information for $definition: $title"
msgstr "Status ($definition): $title"
msgid "Available Transitions"
msgstr "Übergänge"
msgid "classification_quality"
msgstr "Klassifizierung"
@ -976,6 +1060,12 @@ msgstr "Aufgabe"
msgid "publishable_task"
msgstr "Aufgabe/Zugriff"
msgid "label_transition_comments"
msgstr "Bemerkung"
msgid "desc_transition_comments"
msgstr "Notizen zum Statusübergang."
# state names
msgid "accepted"

View file

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

View file

@ -27,6 +27,18 @@
class="loops.organize.browser.member.MemberRegistration"
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
for="loops.interfaces.INode"
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
# 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
members (persons).
$Id$
"""
from datetime import datetime
from email.MIMEText import MIMEText
from zope import interface, component
from zope.app.authentication.principalfolder import InternalPrincipal
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.i18nmessageid import MessageFactory
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.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.meta.interfaces import IOptions
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.node import NodeView
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 IMemberRegistration, IPasswordChange
from loops.organize.party import getPersonForUser, Person
from loops.organize.util import getInternalPrincipal
from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
import loops.browser.util
from loops.util import _
@ -76,10 +79,11 @@ class PersonalInfo(ConceptView):
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.')
template = form_macros
formErrors = dict(
confirm_nomatch=FormError(_(u'Password and password confirmation do not match.')),
@ -88,10 +92,23 @@ class MemberRegistration(NodeView, CreateForm):
label = _(u'Member Registration')
label_submit = _(u'Register')
title = _('Member Registration')
permissions_key = u'registration.permissions'
roles_key = u'registration.roles'
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
def macro(self):
@ -99,7 +116,7 @@ class MemberRegistration(NodeView, CreateForm):
def checkPermissions(self):
personType = adapted(self.conceptManager['person'])
perms = IOptions(personType)('registration.permission')
perms = IOptions(personType)(self.permissions_key)
if perms:
return checkPermission(perms[0], self.context)
return checkPermission('loops.ManageSite', self.context)
@ -108,12 +125,38 @@ class MemberRegistration(NodeView, CreateForm):
def item(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
def schema(self):
schema = super(MemberRegistration, self).schema
schema.fields.remove('birthDate')
schema.fields.reorder(-2, 'loginName')
return schema
# TODO: add company, create institution
@Lazy
def object(self):
@ -121,7 +164,7 @@ class MemberRegistration(NodeView, CreateForm):
def update(self):
form = self.request.form
if not form.get('action'):
if not form.get('form.action'):
return True
instance = component.getAdapter(self.object, IInstance, name='editor')
instance.template = self.schema
@ -157,6 +200,165 @@ class MemberRegistration(NodeView, CreateForm):
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):
interface = IPasswordChange

View file

@ -1,5 +1,42 @@
<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:data use-macro="view/concept_macros/conceptdata">
</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
# it under the terms of the GNU General Public License as published by
@ -62,19 +62,29 @@ class MemberRegistrationManager(object):
def __init__(self, context):
self.context = context
@Lazy
def personType(self):
concepts = self.context.getConceptManager()
return adapted(concepts[self.person_typeName])
def getPrincipalFolderFromOption(self):
options = IOptions(self.personType)
pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0]
return getPrincipalFolder(self.context, pfName)
def register(self, userId, password, lastName, firstName=u'',
groups=[], useExisting=False, pfName=None, **kw):
concepts = self.context.getConceptManager()
personType = adapted(concepts[self.person_typeName])
options = IOptions(personType)
options = IOptions(self.personType)
if pfName is None:
pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0]
self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting)
if len(groups)==0:
self.createPrincipal(pfName, userId, password, lastName, firstName,
useExisting=useExisting)
if not groups:
groups = options(self.groups_key, ())
self.setGroupsForPrincipal(pfName, userId, groups=groups)
self.createPersonForPrincipal(pfName, userId, lastName, firstName,
return self.createPersonForPrincipal(pfName, userId, lastName, firstName,
useExisting, **kw)
def createPrincipal(self, pfName, userId, password, lastName,

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
# it under the terms of the GNU General Public License as published by
@ -18,8 +18,6 @@
"""
Basic implementations for stateful objects and adapters.
$Id$
"""
from zope.app.catalog.interfaces import ICatalog
@ -27,6 +25,7 @@ from zope.cachedescriptors.property import Lazy
from zope import component
from zope.component import adapts, adapter
from cybertools.composer.schema.field import Field
from cybertools.meta.interfaces import IOptions
from cybertools.stateful.base import Stateful as BaseStateful
from cybertools.stateful.base import StatefulAdapter, IndexInfo
@ -34,6 +33,7 @@ from cybertools.stateful.interfaces import IStatesDefinition, ITransitionEvent
from loops.common import adapted
from loops.interfaces import ILoopsObject, IConcept, IResource
from loops import util
from loops.util import _
class Stateful(BaseStateful):
@ -93,3 +93,10 @@ def handleTransition(obj, event):
if next != previous:
cat = component.getUtility(ICatalog)
cat.index_doc(int(util.getUidForObject(obj)), obj)
# predefined fields for transition forms
commentsField = Field('comments', _(u'label_transition_comments'), 'textarea',
description=_(u'desc_transition_comments'),
nostore=True)

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
# it under the terms of the GNU General Public License as published by
@ -23,12 +23,16 @@ Views and actions for states management.
from zope import component
from zope.app.pagetemplate import ViewPageTemplateFile
from zope.cachedescriptors.property import Lazy
from zope.event import notify
from zope.i18n import translate
from zope.lifecycleevent import ObjectModifiedEvent, Attributes
from cybertools.browser.action import Action, actions
from cybertools.composer.schema.schema import Schema
from cybertools.stateful.interfaces import IStateful, IStatesDefinition
from loops.browser.common import BaseView
from loops.browser.concept import ConceptView
from loops.browser.form import ObjectForm, EditObject
from loops.expert.query import And, Or, State, Type, getObjects
from loops.expert.browser.search import search_template
from loops.security.common import checkPermission
@ -43,6 +47,16 @@ statefulActions = ('classification_quality',
'publishable_task',)
def registerStatesPortlet(controller, view, statesDefs,
region='portlet_right', priority=98):
cm = controller.macros
stfs = [component.getAdapter(view.context, IStateful, name=std)
for std in statesDefs]
cm.register(region, 'states', title=_(u'Workflow'),
subMacro=template.macros['portlet_states'],
priority=priority, info=view, stfs=stfs)
class StateAction(Action):
url = None
@ -67,22 +81,92 @@ class StateAction(Action):
@Lazy
def icon(self):
icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color
return 'cybertools.icons/' + icon
return self.stateObject.stateIcon
#icon = self.stateObject.icon or 'led%s.png' % self.stateObject.color
#return 'cybertools.icons/' + icon
for std in statefulActions:
actions.register('state.' + std, 'object', StateAction,
definition = std,
definition=std,
cssClass='icon-action',
)
class ChangeStateBase(object):
@Lazy
def stateful(self):
return component.getAdapter(self.view.virtualTargetObject, IStateful,
name=self.definition)
@Lazy
def definition(self):
return self.request.form.get('stdef') or u''
@Lazy
def action(self):
return self.request.form.get('action') or u''
@Lazy
def transition(self):
return self.stateful.getStatesDefinition().transitions[self.action]
@Lazy
def stateObject(self):
return self.stateful.getStateObject()
@Lazy
def schema(self):
schema = self.transition.schema
if schema is None:
return Schema()
else:
schema.manager = self
schema.request = self.request
return schema
class ChangeStateForm(ChangeStateBase, ObjectForm):
form_action = 'change_state_action'
data = {}
@Lazy
def macro(self):
return template.macros['change_state']
@Lazy
def title(self):
return self.virtualTargetObject.title
class ChangeState(ChangeStateBase, EditObject):
def update(self):
formData = self.request.form
# store data in target object (unless field.nostore)
self.object = self.target
formState = self.instance.applyTemplate(data=formData)
# TODO: check formState
# track all fields
trackData = dict(transition=self.action)
for f in self.fields:
if f.readonly:
continue
name = f.name
fi = formState.fieldInstances[name]
rawValue = fi.getRawValue(formData, name, u'')
trackData[name] = fi.unmarshall(rawValue)
self.stateful.doTransition(self.action)
notify(ObjectModifiedEvent(self.view.virtualTargetObject, trackData))
return True
#class StateQuery(ConceptView):
class StateQuery(BaseView):
template = template
form_action = 'execute_search_action'
@Lazy

View file

@ -77,7 +77,7 @@
set_schema="cybertools.stateful.interfaces.IStateful" />
</zope:class>
<!-- views -->
<!-- views and form controllers -->
<browser:page
for="loops.interfaces.IConcept"
@ -91,6 +91,19 @@
class="loops.organize.stateful.browser.FilterAllStates"
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 -->
<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.traversing.api import getName
from cybertools.composer.schema.schema import Schema
from cybertools.stateful.definition import StatesDefinition
from cybertools.stateful.definition import State, Transition
from cybertools.stateful.interfaces import IStatesDefinition, IStateful
from loops.common import adapted
from loops.organize.stateful.base import commentsField
from loops.organize.stateful.base import StatefulLoopsObject
from loops.security.interfaces import ISecuritySetter
from loops.util import _
def setPermissionsForRoles(settings):
@ -42,6 +45,10 @@ def setPermissionsForRoles(settings):
return setSecurity
defaultSchema = Schema(commentsField,
name='change_state')
@implementer(IStatesDefinition)
def taskStates():
return StatesDefinition('task_states',
@ -55,10 +62,11 @@ def taskStates():
color='x'),
State('archived', 'archived', ('reopen',),
color='grey'),
Transition('release', 'release', 'active'),
Transition('finish', 'finish', 'finished'),
Transition('cancel', 'cancel', 'cancelled'),
Transition('reopen', 're-open', 'draft'),
Transition('release', 'release', 'active', schema=defaultSchema),
Transition('finish', 'finish', 'finished', schema=defaultSchema),
Transition('cancel', 'cancel', 'cancelled', schema=defaultSchema),
Transition('reopen', 're-open', 'draft', schema=defaultSchema),
Transition('archive', 'archive', 'archived', schema=defaultSchema),
initialState='draft')

View file

@ -68,4 +68,82 @@
</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>

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

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

View file

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

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

View file

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

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

View file

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