self registration with confirmation: basically working; missing: translations, sender email, improved error handling

This commit is contained in:
Helmut Merz 2013-05-05 16:55:11 +02:00
parent 90b4da1a9b
commit 7b9a85a586
7 changed files with 180 additions and 21 deletions

View file

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

View file

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

View file

@ -124,6 +124,9 @@ class Base(object):
return getName(c) return getName(c)
return 'textelement' return 'textelement'
def getMacroForResource(self, r):
return self.book_macros['default_text']
def getParentsForResource(self, r): def getParentsForResource(self, r):
for c in r.context.getConcepts([self.defaultPredicate]): for c in r.context.getConcepts([self.defaultPredicate]):
if c != self.context and c.conceptType != self.documentTypeType: if c != self.context and c.conceptType != self.documentTypeType:

View file

@ -42,9 +42,11 @@
<metal:text define-macro="textresources"> <metal:text define-macro="textresources">
<div tal:repeat="related item/textResources"> <div tal:repeat="related item/textResources">
<div class="span-4"> <div class="span-4">
<div tal:attributes="class python: <metal:text define-macro="default_text">
item.getCssClassForResource(related)" <div tal:attributes="class python:
tal:content="structure related/render" /> item.getCssClassForResource(related)"
tal:content="structure related/render" />
</metal:text>
</div> </div>
<div class="span-2 last" style="padding-top: 0.4em"> <div class="span-2 last" style="padding-top: 0.4em">
<div class="object-actions" style="padding-top: 0" <div class="object-actions" style="padding-top: 0"

View file

@ -21,6 +21,8 @@ Definition of view classes and other browser related stuff for
members (persons). members (persons).
""" """
from datetime import datetime
from email.MIMEText import MIMEText
from zope import interface, component from zope import interface, component
from zope.app.authentication.principalfolder import InternalPrincipal from zope.app.authentication.principalfolder import InternalPrincipal
from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget from zope.app.form.browser.textwidgets import PasswordWidget as BasePasswordWidget
@ -29,6 +31,8 @@ from zope.app.principalannotation import annotations
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.i18nmessageid import MessageFactory from zope.i18nmessageid import MessageFactory
from zope.security import checkPermission from zope.security import checkPermission
from zope.sendmail.interfaces import IMailDelivery
from zope.traversing.browser import absoluteURL
from cybertools.composer.interfaces import IInstance from cybertools.composer.interfaces import IInstance
from cybertools.composer.schema.browser.common import schema_macros from cybertools.composer.schema.browser.common import schema_macros
@ -36,7 +40,8 @@ from cybertools.composer.schema.browser.form import Form, CreateForm
from cybertools.composer.schema.schema import FormState, FormError from cybertools.composer.schema.schema import FormState, FormError
from cybertools.meta.interfaces import IOptions from cybertools.meta.interfaces import IOptions
from cybertools.typology.interfaces import IType from cybertools.typology.interfaces import IType
from loops.browser.common import concept_macros from cybertools.util.randomname import generateName
from loops.browser.common import concept_macros, form_macros
from loops.browser.concept import ConceptView, ConceptRelationView from loops.browser.concept import ConceptView, ConceptRelationView
from loops.browser.node import NodeView from loops.browser.node import NodeView
from loops.common import adapted from loops.common import adapted
@ -44,7 +49,7 @@ from loops.concept import Concept
from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager from loops.organize.interfaces import ANNOTATION_KEY, IMemberRegistrationManager
from loops.organize.interfaces import IMemberRegistration, IPasswordChange from loops.organize.interfaces import IMemberRegistration, IPasswordChange
from loops.organize.party import getPersonForUser, Person from loops.organize.party import getPersonForUser, Person
from loops.organize.util import getInternalPrincipal from loops.organize.util import getInternalPrincipal, getPrincipalForUserId
import loops.browser.util import loops.browser.util
from loops.util import _ from loops.util import _
@ -74,7 +79,7 @@ class PersonalInfo(ConceptView):
return self return self
class MemberRegistration(NodeView, CreateForm): class BaseMemberRegistration(NodeView):
interface = IMemberRegistration # TODO: add company, create institution interface = IMemberRegistration # TODO: add company, create institution
message = _(u'The user account has been created.') message = _(u'The user account has been created.')
@ -106,6 +111,12 @@ class MemberRegistration(NodeView, CreateForm):
def item(self): def item(self):
return self return self
def getPrincipalAnnotation(self, principal):
return annotations(principal).get(ANNOTATION_KEY, None)
class MemberRegistration(BaseMemberRegistration, CreateForm):
@Lazy @Lazy
def schema(self): def schema(self):
schema = super(MemberRegistration, self).schema schema = super(MemberRegistration, self).schema
@ -113,6 +124,7 @@ class MemberRegistration(NodeView, CreateForm):
schema.fields.reorder(-2, 'loginName') schema.fields.reorder(-2, 'loginName')
return schema return schema
# TODO: add company, create institution # TODO: add company, create institution
@Lazy @Lazy
def object(self): def object(self):
return Person(Concept()) return Person(Concept())
@ -155,30 +167,138 @@ class MemberRegistration(NodeView, CreateForm):
return False return False
class SecureMemberRegistration(MemberRegistration): class SecureMemberRegistration(BaseMemberRegistration, CreateForm):
permissions_key = u'secure_registration.permissions' permissions_key = u'secure_registration.permissions'
roles_key = u'secure_registration.roles' roles_key = u'secure_registration.roles'
@Lazy @Lazy
def schema(self): def schema(self):
schema = super(MemberRegistration, self).schema schema = super(SecureMemberRegistration, self).schema
schema.fields.remove('birthDate') schema.fields.remove('birthDate')
schema.fields.remove('password') schema.fields.remove('password')
schema.fields.remove('passwordConfirm') schema.fields.remove('passwordConfirm')
schema.fields.remove('phoneNumbers') schema.fields.remove('phoneNumbers')
schema.fields.reorder(-2, 'loginName') #schema.fields.reorder(-2, 'loginName')
return schema return schema
@Lazy
def object(self):
return Person(Concept())
class ConfirmMemberRegistration(NodeView): def update(self):
form = self.request.form
if not form.get('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')
result = regMan.register(login, pw,
form.get('lastName'), form.get('firstName'),
email=email,)
if isinstance(result, dict):
fi = formState.fieldInstances[result['fieldName']]
fi.setError(result['error'], 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'])
msg = self.message
self.request.response.redirect('%s?loops.message=%s' % (self.url, msg))
return False
# TODO: control form via interface? 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]
message = u'Um die Anmeldung abzuschliessen muessen Sie folgenden Link betaetigen:\n\n'
message = (message + url).encode('UTF-8')
sender = 'webmaster@zeitraum-bayern.de'
msg = MIMEText(message, 'plain', 'utf-8')
msg['Subject'] = 'Benutzer-Registrierung'
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'
template = form_macros
isInnerHtml = False
showAssignments = False
form_action = 'confirm_registration'
def closeAction(self, submit=True):
return u''
@Lazy @Lazy
def macro(self): def macro(self):
return organize_macros.macros['confirm'] 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('birthDate')
schema.fields.remove('phoneNumbers')
schema.fields.loginName.readonly = True
schema.fields.reorder(-2, 'loginName')
schema.fields.firstName.readonly = True
schema.fields.lastName.readonly = True
schema.fields.firstName.readonly = True
schema.fields.email.readonly = True
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
pw = form.get('password')
pwConfirm = form.get('passwordConfirm')
if pw != pwConfirm:
msg = self.errorMessages['confirm_nomatch']
self.request.form['message'] = msg
return True
del pa['id']
del pa['timestamp']
ip = getInternalPrincipal(userId)
ip.setPassword(pw)
url = '%s?loops.message=%s' % (self.url, self.message)
self.request.response.redirect(url)
return False
class PasswordChange(NodeView, Form): class PasswordChange(NodeView, Form):

View file

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

View file

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