Merge branch 'master' of ssh://git.cy55.de/home/git/loops
This commit is contained in:
commit
4178829685
59 changed files with 1377 additions and 512 deletions
|
@ -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):
|
||||
|
|
4
browser/compound/__init__.py
Normal file
4
browser/compound/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
package loops.browser.compound
|
||||
"""
|
||||
|
14
browser/compound/configure.zcml
Normal file
14
browser/compound/configure.zcml
Normal 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>
|
54
browser/compound/standard.py
Normal file
54
browser/compound/standard.py
Normal 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
|
||||
|
13
browser/compound/view_macros.pt
Normal file
13
browser/compound/view_macros.pt
Normal 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>
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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']
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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']:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 -->
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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.'),
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"> </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)">
|
||||
<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}" />
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br />
|
||||
<input type="submit" name="submit" value="Evaluate Questionnaire"
|
||||
i18n:attributes="value" />
|
||||
</form>
|
||||
</metal:block>
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
14
query.py
14
query.py
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2008 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,8 +18,6 @@
|
|||
|
||||
"""
|
||||
Query management stuff.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from BTrees.IOBTree import IOBTree
|
||||
|
@ -33,6 +31,7 @@ from zope.cachedescriptors.property import Lazy
|
|||
from cybertools.typology.interfaces import IType
|
||||
from loops.common import AdapterBase
|
||||
from loops.interfaces import IConcept, IConceptSchema, ILoopsAdapter
|
||||
from loops.interfaces import IOptions
|
||||
from loops.security.common import canListObject
|
||||
from loops.type import TypeInterfaceSourceList
|
||||
from loops.versioning.util import getVersion
|
||||
|
@ -182,7 +181,7 @@ class ConceptQuery(BaseQuery):
|
|||
|
||||
# QueryConcept: concept objects that allow querying the database.
|
||||
|
||||
class IQueryConcept(IConceptSchema, ILoopsAdapter):
|
||||
class IQueryConcept(IConceptSchema, ILoopsAdapter, IOptions):
|
||||
""" The schema for the query type.
|
||||
"""
|
||||
|
||||
|
@ -194,13 +193,6 @@ class IQueryConcept(IConceptSchema, ILoopsAdapter):
|
|||
default=u'',
|
||||
required=False)
|
||||
|
||||
options = schema.List(
|
||||
title=_(u'Options'),
|
||||
description=_(u'Additional settings.'),
|
||||
value_type=schema.TextLine(),
|
||||
default=[],
|
||||
required=False)
|
||||
|
||||
|
||||
class QueryConcept(AdapterBase):
|
||||
|
||||
|
|
|
@ -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
23
type.py
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (c) 2006 Helmut Merz helmutm@cy55.de
|
||||
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
|
@ -18,8 +18,6 @@
|
|||
|
||||
"""
|
||||
Type management stuff.
|
||||
|
||||
$Id$
|
||||
"""
|
||||
|
||||
from zope import component, schema
|
||||
|
@ -28,12 +26,13 @@ from zope.interface import implements
|
|||
from zope.cachedescriptors.property import Lazy
|
||||
from zope.dottedname.resolve import resolve
|
||||
from zope.security.proxy import removeSecurityProxy
|
||||
from zope.traversing.api import getName
|
||||
from zope.traversing.api import getName, getPath
|
||||
|
||||
from cybertools.typology.type import BaseType, TypeManager
|
||||
from cybertools.typology.interfaces import ITypeManager
|
||||
from loops.interfaces import ILoopsObject, IConcept, IResource
|
||||
from loops.interfaces import ITypeConcept
|
||||
from loops.interfaces import IOptions
|
||||
from loops.interfaces import IResourceAdapter, IFile, IExternalFile, IImage
|
||||
from loops.interfaces import ITextDocument, INote
|
||||
from loops.concept import Concept
|
||||
|
@ -49,10 +48,15 @@ class LoopsType(BaseType):
|
|||
#document=Document)
|
||||
containerMapping = dict(concept='concepts', resource='resources')
|
||||
|
||||
isForeignReference = False
|
||||
|
||||
@Lazy
|
||||
def title(self):
|
||||
tp = self.typeProvider
|
||||
return tp is None and u'Unknown Type' or tp.title
|
||||
title = tp is None and u'Unknown Type' or tp.title
|
||||
if self.isForeignReference:
|
||||
title += (' (Site: %s)' % getName(self.root))
|
||||
return title
|
||||
|
||||
@Lazy
|
||||
def token(self):
|
||||
|
@ -63,7 +67,11 @@ class LoopsType(BaseType):
|
|||
def tokenForSearch(self):
|
||||
tp = self.typeProvider
|
||||
typeName = tp is None and 'unknown' or str(getName(tp))
|
||||
return ':'.join(('loops', self.qualifiers[0], typeName,))
|
||||
if self.isForeignReference:
|
||||
root = getPath(self.root)
|
||||
else:
|
||||
root = 'loops'
|
||||
return ':'.join((root, self.qualifiers[0], typeName,))
|
||||
|
||||
@Lazy
|
||||
def typeInterface(self):
|
||||
|
@ -272,7 +280,8 @@ class TypeInterfaceSourceList(object):
|
|||
|
||||
implements(schema.interfaces.IIterableSource)
|
||||
|
||||
typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote)
|
||||
typeInterfaces = (ITypeConcept, IFile, IExternalFile, ITextDocument, INote,
|
||||
IOptions)
|
||||
|
||||
def __init__(self, context):
|
||||
self.context = context
|
||||
|
|
5
util.py
5
util.py
|
@ -141,4 +141,7 @@ def saveRequest(request):
|
|||
local_data.request = request
|
||||
|
||||
def getRequest():
|
||||
return local_data.request
|
||||
try:
|
||||
return local_data.request
|
||||
except AttributeError:
|
||||
return None
|
||||
|
|
Loading…
Add table
Reference in a new issue