self registration with confirmation: basically working; missing: translations, sender email, improved error handling
This commit is contained in:
parent
90b4da1a9b
commit
7b9a85a586
7 changed files with 180 additions and 21 deletions
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue