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

This commit is contained in:
hplattner 2013-06-24 11:16:14 +02:00
commit 531252cc88
31 changed files with 577 additions and 87 deletions

View file

@ -123,6 +123,15 @@ actions.register('external_edit', 'object', TargetAction,
cssClass='icon-action',
)
actions.register('create_object', 'portlet', DialogAction,
title=_('Create Resource...'),
description=_('Create a new resource object.'),
viewName='create_object.html',
dialogName='edit',
prerequisites=['registerDojoEditor'],
permission='zope.ManageContent',
)
actions.register('edit_object', 'portlet', DialogAction,
title=_(u'Edit Resource...'),
description=_(u'Modify resource object.'),

View file

@ -489,7 +489,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:

View file

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

View file

@ -59,6 +59,15 @@ textarea {
margin-top: 0.5em;
}
/* elements taken from blueprint (Lobo) skin */
.span-1, .span-2, .span-3, .span-4, .span-5, .span-6 {float: left;}
.span-1 {width: 105px;}
.span-2 {width: 230px;}
.span-3 {width: 355px;}
.span-4 {width: 480px;}
.span-5 {width: 605px;}
.span-6 {width: 730px;}
table.listing {
margin: 1px;
margin-top: 6px;

View file

@ -386,6 +386,8 @@ class NodeView(BaseView):
ht = super(NodeView, self).headTitle
if ht not in parts:
parts.append(ht)
if self.globalOptions('reverseHeadTitle'):
parts.reverse()
return ' - ' .join(parts)
@Lazy

View file

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

View file

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

View file

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

View file

@ -82,6 +82,15 @@ table {
margin-top: 0.5em;
}
/* elements taken from blueprint (Lobo) skin */
.span-1, .span-2, .span-3, .span-4, .span-5, .span-6 {float: left;}
.span-1 {width: 105px;}
.span-2 {width: 230px;}
.span-3 {width: 355px;}
.span-4 {width: 480px;}
.span-5 {width: 605px;}
.span-6 {width: 730px;}
tr.even td {
background-color: transparent;
}

View file

@ -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
@ -118,11 +119,36 @@ class Base(object):
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]):

View file

@ -8,7 +8,7 @@
<zope:adapter
name="book_overview"
for="loops.interfaces.IConcept
loops.browser.skin.Lobo"
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.BookView"
permission="zope.View" />
@ -16,7 +16,7 @@
<zope:adapter
name="section_view"
for="loops.interfaces.IConcept
loops.browser.skin.Lobo"
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.SectionView"
permission="zope.View" />
@ -24,7 +24,7 @@
<zope:adapter
name="book_topic_view"
for="loops.interfaces.IConcept
loops.browser.skin.Lobo"
zope.publisher.interfaces.browser.IBrowserRequest"
provides="zope.interface.Interface"
factory="loops.compound.book.browser.TopicView"
permission="zope.View" />

View file

@ -1,4 +1,5 @@
type(u'documenttype', u'Dokumentenart', options=u'qualifier:assign',
typeInterface=u'loops.interfaces.IOptions',
viewName=u'')
# book types
@ -26,6 +27,7 @@ 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')

View file

@ -40,11 +40,21 @@
<metal:info use-macro="view/concept_macros/concepttitle" />
<metal:info use-macro="item/book_macros/children" />
<metal:text define-macro="textresources">
<div tal:repeat="related item/textResources">
<div style="clear: both"
tal:repeat="related item/textResources">
<div class="span-4">
<div tal:attributes="class python:
item.getCssClassForResource(related)"
tal:content="structure related/render" />
<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 class="span-2 last" style="padding-top: 0.4em">
<div class="object-actions" style="padding-top: 0"

View file

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

View file

@ -690,9 +690,21 @@ class IIndexAttributes(Interface):
"""
# reusable interface elements
class IOptions(Interface):
options = schema.List(
title=_(u'Options'),
description=_(u'Additional settings.'),
value_type=schema.TextLine(),
default=[],
required=False)
# types stuff
class ITypeConcept(IConceptSchema, ILoopsAdapter):
class ITypeConcept(IConceptSchema, ILoopsAdapter, IOptions):
""" Concepts of type 'type' should be adaptable to this interface.
"""
@ -725,13 +737,6 @@ class ITypeConcept(IConceptSchema, ILoopsAdapter):
default=u'',
required=False)
options = schema.List(
title=_(u'Options'),
description=_(u'Additional settings.'),
value_type=schema.TextLine(),
default=[],
required=False)
# storage = schema.Choice()

View file

@ -112,7 +112,7 @@ class SurveyView(ConceptView):
mapping=dict(minAnswers=qugroup.minAnswers)),
target_language=lang)
if info:
text = u'%s<br />(%s)' % (text, info)
text = u'<i>%s</i><br />(%s)' % (text, info)
return text
def getValues(self, question):

View file

@ -38,6 +38,13 @@ 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.'),

View file

@ -11,6 +11,9 @@
</tal:description>
<div tal:condition="feedback">
<h3 i18n:translate="">Feedback</h3>
<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>
@ -47,12 +50,12 @@
<table class="listing">
<tal:qugroup repeat="qugroup item/adapted/questionGroups">
<tr><td colspan="6">&nbsp;</td></tr>
<tr>
<tr class="vpad">
<td tal:define="infoText python:item.getInfoText(qugroup)">
<b tal:content="qugroup/title" />
<div tal:condition="infoText">
<div class="infotext"
tal:condition="infoText">
<span tal:content="structure infoText" />
<br />&nbsp;
</div>
</td>
<td style="text-align: center"
@ -63,7 +66,8 @@
style="text-align: right"
i18n:translate="">Does not apply</td>
</tr>
<tr tal:repeat="question qugroup/questions">
<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)">

Binary file not shown.

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: 0.13.0\n"
"POT-Creation-Date: 2007-05-22 12:00 CET\n"
"PO-Revision-Date: 2013-04-06 12:00 CET\n"
"PO-Revision-Date: 2013-06-20 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..."
@ -762,12 +765,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."
@ -1010,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"
@ -1030,6 +1060,12 @@ msgstr "Aufgabe"
msgid "publishable_task"
msgstr "Aufgabe/Zugriff"
msgid "label_transition_comments"
msgstr "Bemerkung"
msgid "desc_transition_comments"
msgstr "Notizen zum Statusübergang."
# state names
msgid "accepted"

View file

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

View file

@ -21,6 +21,8 @@ Definition of view classes and other browser related stuff for
members (persons).
"""
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
@ -29,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
@ -36,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
@ -44,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 _
@ -74,10 +79,11 @@ class PersonalInfo(ConceptView):
return self
class MemberRegistration(NodeView, CreateForm):
class BaseMemberRegistration(NodeView):
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.')),
@ -86,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):
@ -106,6 +125,31 @@ 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
@ -113,13 +157,14 @@ class MemberRegistration(NodeView, CreateForm):
schema.fields.reorder(-2, 'loginName')
return schema
# TODO: add company, create institution
@Lazy
def object(self):
return Person(Concept())
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
@ -155,30 +200,164 @@ class MemberRegistration(NodeView, CreateForm):
return False
class SecureMemberRegistration(MemberRegistration):
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(MemberRegistration, self).schema
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')
#schema.fields.reorder(-2, 'loginName')
return schema
@Lazy
def macro(self):
return organize_macros.macros['register']
class ConfirmMemberRegistration(NodeView):
@Lazy
def object(self):
return Person(Concept())
# TODO: control form via interface?
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):

View file

@ -1,7 +1,40 @@
<html i18n:domain="loops">
<metal:registration define-macro="confirm">
</metal:registration>
<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">

View file

@ -1,5 +1,5 @@
#
# Copyright (c) 2012 Helmut Merz helmutm@cy55.de
# Copyright (c) 2013 Helmut Merz helmutm@cy55.de
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -62,19 +62,29 @@ class MemberRegistrationManager(object):
def __init__(self, context):
self.context = context
@Lazy
def personType(self):
concepts = self.context.getConceptManager()
return adapted(concepts[self.person_typeName])
def getPrincipalFolderFromOption(self):
options = IOptions(self.personType)
pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0]
return getPrincipalFolder(self.context, pfName)
def register(self, userId, password, lastName, firstName=u'',
groups=[], useExisting=False, pfName=None, **kw):
concepts = self.context.getConceptManager()
personType = adapted(concepts[self.person_typeName])
options = IOptions(personType)
options = IOptions(self.personType)
if pfName is None:
pfName = options(self.principalfolder_key,
(self.default_principalfolder,))[0]
self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting)
self.createPrincipal(pfName, userId, password, lastName, firstName,
useExisting=useExisting)
if not groups:
groups = options(self.groups_key, ())
self.setGroupsForPrincipal(pfName, userId, groups=groups)
self.createPersonForPrincipal(pfName, userId, lastName, firstName,
return self.createPersonForPrincipal(pfName, userId, lastName, firstName,
useExisting, **kw)
def createPrincipal(self, pfName, userId, password, lastName,

View file

@ -26,9 +26,13 @@ from zope.cachedescriptors.property import Lazy
from zope.i18n import translate
from cybertools.browser.action import Action, actions
from cybertools.composer.schema.field import Field
from cybertools.composer.schema.interfaces import ISchemaFactory
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
@ -48,7 +52,7 @@ def registerStatesPortlet(controller, view, statesDefs,
cm = controller.macros
stfs = [component.getAdapter(view.context, IStateful, name=std)
for std in statesDefs]
cm.register(region, 'states', title=_(u'States'),
cm.register(region, 'states', title=_(u'Workflow'),
subMacro=template.macros['portlet_states'],
priority=priority, info=view, stfs=stfs)
@ -79,13 +83,14 @@ 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
def registerStatefulAction(std, msgFactory=_):
actions.register('state.' + std, 'object', StateAction,
definition = std,
definition=std,
cssClass='icon-action',
msgFactory=msgFactory,
)
@ -94,11 +99,67 @@ for std in statefulActions:
registerStatefulAction(std)
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()
class ChangeStateForm(ObjectForm, ChangeStateBase):
form_action = 'change_state_action'
data = {}
@Lazy
def macro(self):
return template.macros['change_state']
@Lazy
def title(self):
return self.virtualTargetObject.title
@Lazy
def schema(self):
# TODO: create schema directly, use field information specified
# in transition
commentsField = Field('comments', _(u'label_transition_comments'),
'textarea',
description=_(u'desc_transition_comments'))
fields = [commentsField]
return Schema(name='change_state', request=self.request,
manager=self, *fields)
class ChangeState(EditObject, ChangeStateBase):
def update(self):
print '***', self.request.form
self.stateful.doTransition(self.action)
return True
#class StateQuery(ConceptView):
class StateQuery(BaseView):
template = template
form_action = 'execute_search_action'
@Lazy

View file

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

View file

@ -59,6 +59,7 @@ def taskStates():
Transition('finish', 'finish', 'finished'),
Transition('cancel', 'cancel', 'cancelled'),
Transition('reopen', 're-open', 'draft'),
Transition('archive', 'archive', 'archived'),
initialState='draft')

View file

@ -68,26 +68,31 @@
</metal:query>
<!-- portlets -->
<metal:actions define-macro="portlet_states">
<div tal:repeat="stf macro/stfs">
<div tal:condition="python:len(macro.stfs) > 1">
<span i18n:translate="">Workflow</span>
<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" />
<div tal:define="stateObject stf/getStateObject">
<b i18n:translate="">State</b>:<br />
<span>
<img style="margin-bottom: -1px"
tal:attributes="src string:$resourceBase/${stateObject/stateIcon}" />
<span i18n:translate=""
tal:content="stateObject/title" />
</span>
</div>
<div>
<div><b i18n:translate="">Available Transitions</b>:</div>
<div><b i18n:translate="">Available Transitions</b>:
<ul>
<li tal:repeat="action stf/getAvailableTransitionsForUser">
<a i18n:translate=""
tal:attributes="href string:change_state.html"
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>
@ -96,4 +101,49 @@
</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:content="view/stateful/state" /> -
<span i18n:translate="">Transition</span>:
<span i18n:translate=""
tal:content="view/transition/title" />
</div>
<input type="hidden" name="form.action" value="change_state">
<input type="hidden" name="stdef"
tal:attributes="value request/form/stdef|nothing">
<input type="hidden" name="action"
tal:attributes="value request/form/action|nothing">
</div>
<div dojoType="dijit.layout.ContentPane" region="center">
<table cellpadding="3" class="form">
<tbody><tr><td colspan="5" style="padding-right: 15px">
<div id="form.fields">
<metal:fields use-macro="view/fieldRenderers/fields" />
</div>
</td></tr></tbody>
</table>
</div>
<div dojoType="dijit.layout.ContentPane" region="bottom">
<metal:buttons define-slot="buttons">
<input value="Save" type="submit"
onClick="submit();; return false"
i18n:attributes="value">
<input type="button" value="Cancel" onClick="dialog.hide();"
i18n:attributes="value">
</metal:buttons>
</div>
</div>
</form>
</metal:dialog>
</html>

View file

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

View file

@ -231,6 +231,8 @@ class ConceptSecuritySetter(LoopsObjectSecuritySetter):
setter.propagateSecurity(revert, updated)
def propagateSecurity(self, revert=False, updated=None):
if self.globalOptions('noPropagateSecurity'):
return
if updated is None:
updated = set()
obj = self.baseObject

View file

@ -32,6 +32,7 @@ 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.common import adapted
@ -276,7 +277,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