diff --git a/browser/common.py b/browser/common.py index 69b54b6..02d9902 100644 --- a/browser/common.py +++ b/browser/common.py @@ -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: diff --git a/browser/form_macros.pt b/browser/form_macros.pt index f56b708..110280e 100644 --- a/browser/form_macros.pt +++ b/browser/form_macros.pt @@ -51,6 +51,8 @@ + +
@@ -59,7 +61,7 @@ - @@ -119,6 +121,8 @@ tal:attributes="value typeToken" /> + +
@@ -127,7 +131,7 @@ - diff --git a/browser/node.py b/browser/node.py index 8302c3d..4e23537 100644 --- a/browser/node.py +++ b/browser/node.py @@ -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 diff --git a/browser/node_macros.pt b/browser/node_macros.pt index b67f1fb..640f9a1 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -314,8 +314,13 @@ - + diff --git a/browser/skin/lobo/body.pt b/browser/skin/lobo/body.pt index f1f08c6..2a3ea56 100644 --- a/browser/skin/lobo/body.pt +++ b/browser/skin/lobo/body.pt @@ -30,7 +30,13 @@
-
+ +
+
-
+
+

+ + +

(%s)' % (text, info) + text = u'%s
(%s)' % (text, info) return text def getValues(self, question): diff --git a/knowledge/survey/interfaces.py b/knowledge/survey/interfaces.py index 7ce5849..e2adffc 100644 --- a/knowledge/survey/interfaces.py +++ b/knowledge/survey/interfaces.py @@ -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.'), diff --git a/knowledge/survey/view_macros.pt b/knowledge/survey/view_macros.pt index 51c2c6a..9e7593c 100644 --- a/knowledge/survey/view_macros.pt +++ b/knowledge/survey/view_macros.pt @@ -11,6 +11,9 @@

Feedback

+
@@ -47,12 +50,12 @@
Category
- + - + + + + + + + + + + + + + + + + + + diff --git a/organize/member.py b/organize/member.py index bd48853..ab2aded 100644 --- a/organize/member.py +++ b/organize/member.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2012 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -62,19 +62,29 @@ class MemberRegistrationManager(object): def __init__(self, context): self.context = context + @Lazy + def personType(self): + concepts = self.context.getConceptManager() + return adapted(concepts[self.person_typeName]) + + def getPrincipalFolderFromOption(self): + options = IOptions(self.personType) + pfName = options(self.principalfolder_key, + (self.default_principalfolder,))[0] + return getPrincipalFolder(self.context, pfName) + def register(self, userId, password, lastName, firstName=u'', groups=[], useExisting=False, pfName=None, **kw): - concepts = self.context.getConceptManager() - personType = adapted(concepts[self.person_typeName]) - options = IOptions(personType) + options = IOptions(self.personType) if pfName is None: pfName = options(self.principalfolder_key, (self.default_principalfolder,))[0] - self.createPrincipal(pfName, userId, password, lastName, firstName, useExisting=useExisting) + self.createPrincipal(pfName, userId, password, lastName, firstName, + useExisting=useExisting) if not groups: groups = options(self.groups_key, ()) self.setGroupsForPrincipal(pfName, userId, groups=groups) - self.createPersonForPrincipal(pfName, userId, lastName, firstName, + return self.createPersonForPrincipal(pfName, userId, lastName, firstName, useExisting, **kw) def createPrincipal(self, pfName, userId, password, lastName, diff --git a/organize/stateful/browser.py b/organize/stateful/browser.py index 5b77b7d..713d552 100644 --- a/organize/stateful/browser.py +++ b/organize/stateful/browser.py @@ -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 diff --git a/organize/stateful/configure.zcml b/organize/stateful/configure.zcml index 7833208..30ad487 100644 --- a/organize/stateful/configure.zcml +++ b/organize/stateful/configure.zcml @@ -77,7 +77,7 @@ set_schema="cybertools.stateful.interfaces.IStateful" /> - + + + + + diff --git a/organize/stateful/task.py b/organize/stateful/task.py index 0721bc4..5e60ee5 100644 --- a/organize/stateful/task.py +++ b/organize/stateful/task.py @@ -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') diff --git a/organize/stateful/view_macros.pt b/organize/stateful/view_macros.pt index d1be431..ae29073 100644 --- a/organize/stateful/view_macros.pt +++ b/organize/stateful/view_macros.pt @@ -68,12 +68,10 @@ - -
- Workflow + States Definition
@@ -81,13 +79,19 @@ State: +
-
 
-
+
-
 
Does not apply
diff --git a/locales/de/LC_MESSAGES/loops.mo b/locales/de/LC_MESSAGES/loops.mo index 45b1e4c..4308de5 100644 Binary files a/locales/de/LC_MESSAGES/loops.mo and b/locales/de/LC_MESSAGES/loops.mo differ diff --git a/locales/de/LC_MESSAGES/loops.po b/locales/de/LC_MESSAGES/loops.po index ca2a770..77ed12b 100644 --- a/locales/de/LC_MESSAGES/loops.po +++ b/locales/de/LC_MESSAGES/loops.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: 0.13.0\n" "POT-Creation-Date: 2007-05-22 12:00 CET\n" -"PO-Revision-Date: 2013-04-06 12:00 CET\n" +"PO-Revision-Date: 2013-06-20 12:00 CET\n" "Last-Translator: Helmut Merz \n" "Language-Team: loops developers \n" "MIME-Version: 1.0\n" @@ -86,6 +86,9 @@ msgstr "Thema bearbeiten..." msgid "Modify topic." msgstr "Thema ändern" +msgid "Please correct the indicated errors." +msgstr "Bitte berichtigen Sie die angezeigten Fehler." + # blog msgid "Edit Blog Post..." @@ -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" diff --git a/organize/README.txt b/organize/README.txt index f1742df..103973c 100644 --- a/organize/README.txt +++ b/organize/README.txt @@ -185,7 +185,7 @@ sure that a principal object can be served by a corresponding factory): ... 'lastName': u'Sawyer', ... 'firstName': u'Tom', ... 'email': u'tommy@sawyer.com', - ... 'action': 'update',} + ... 'form.action': 'update',} and register it. diff --git a/organize/browser/member.py b/organize/browser/member.py index 7b31e2f..5916929 100644 --- a/organize/browser/member.py +++ b/organize/browser/member.py @@ -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): diff --git a/organize/browser/view_macros.pt b/organize/browser/view_macros.pt index af8ea2b..93ef80e 100644 --- a/organize/browser/view_macros.pt +++ b/organize/browser/view_macros.pt @@ -1,7 +1,40 @@ - - + + + + +
+ +
+ +
+ + + + +
Login Name +
+
+ +
+
+ +
+
+
+
+ + + + +
+
+ + + + diff --git a/query.py b/query.py index d7a0a93..486eb7d 100644 --- a/query.py +++ b/query.py @@ -1,5 +1,5 @@ # -# Copyright (c) 2008 Helmut Merz helmutm@cy55.de +# Copyright (c) 2013 Helmut Merz helmutm@cy55.de # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -18,8 +18,6 @@ """ Query management stuff. - -$Id$ """ from BTrees.IOBTree import IOBTree @@ -33,6 +31,7 @@ from zope.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): diff --git a/type.py b/type.py index f9404e2..43e7c12 100644 --- a/type.py +++ b/type.py @@ -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