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>
</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

@ -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

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

View file

@ -42,9 +42,11 @@
<metal:text define-macro="textresources">
<div tal:repeat="related item/textResources">
<div class="span-4">
<div tal:attributes="class python:
item.getCssClassForResource(related)"
tal:content="structure related/render" />
<metal:text define-macro="default_text">
<div tal:attributes="class python:
item.getCssClassForResource(related)"
tal:content="structure related/render" />
</metal:text>
</div>
<div class="span-2 last" style="padding-top: 0.4em">
<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).
"""
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,7 +79,7 @@ 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.')
@ -106,6 +111,12 @@ class MemberRegistration(NodeView, CreateForm):
def item(self):
return self
def getPrincipalAnnotation(self, principal):
return annotations(principal).get(ANNOTATION_KEY, None)
class MemberRegistration(BaseMemberRegistration, CreateForm):
@Lazy
def schema(self):
schema = super(MemberRegistration, self).schema
@ -113,6 +124,7 @@ class MemberRegistration(NodeView, CreateForm):
schema.fields.reorder(-2, 'loginName')
return schema
# TODO: add company, create institution
@Lazy
def object(self):
return Person(Concept())
@ -155,30 +167,138 @@ 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'
@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 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
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('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):

View file

@ -1,7 +1,22 @@
<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">

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,11 +62,20 @@ 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]
@ -74,7 +83,7 @@ class MemberRegistrationManager(object):
if len(groups)==0:
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,