cybertools/organize/servicemanager.txt
helmutm 5c6f26e2ec start work on complex services/events (e.g. conferences)
git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@4150 fd906abe-77d9-0310-91a1-e0d9ade77398
2011-01-16 14:46:43 +00:00

767 lines
25 KiB
Text

===============
Service Manager
================
($Id$)
This package does not provide functionality on its own but shows only
how to integrate other packages into an application package.
>>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
>>> site = placefulSetUp(True)
>>> from cybertools.organize.tests import setUp
>>> setUp(site)
>>> from zope import component
>>> from cybertools.composer import schema
>>> from cybertools.composer.interfaces import IInstance
>>> from cybertools.composer.schema.interfaces import IClientFactory
>>> from cybertools.organize import service
Setting up a Service Manager
============================
A service manager is a collection of individual services; in our
example the service manager represents a workshop with two
parts (events, lectures, ...).
>>> workshop = site['workshop'] = service.ServiceManager()
>>> workshop.__parent__ = site
>>> workshop.__name__ = 'workshop'
>>> event1 = service.ScheduledService('event1', category='event', manager=workshop,
... title=u'Event 1', capacity=5)
>>> event2 = service.ScheduledService('event2', category='event', manager=workshop,
... title=u'Event 2')
>>> workshop.services.append(event1)
>>> workshop.services.append(event2)
In order to be able to registrate participants for the workshop we
have to provide data structures for the participants (the service
clients. This is done via to form descriptions (schemas), one for the
personal data (first name, last name), and one for the address.
>>> workshop.clientSchemas.append(schema.Schema(
... schema.Field('lastName', u'Last Name', required=True,
... standardFieldName='lastName'),
... schema.Field('firstName', u'First Name'),
... schema.Field('email', u'Email Address', required=True,
... fieldType='email', standardFieldName='email'),
... schema.Field('age', u'Age', fieldType='number'),
... schema.Field('addr', u'Personal Address', required=True,
... fieldtype='radiobuttons',
... vocabulary=u'Mrs\nMr'),
... schema.Field('acadTitles', u'Academic Titles',
... fieldtype='checkboxes',
... vocabulary=u'Prof.\nDr.'),
... name='person',
... manager=workshop,
... ))
>>> workshop.clientSchemas.append(schema.Schema(
... schema.Field('street', u'Street'),
... schema.Field('city', u'City', required=True),
... schema.Field('country', u'Country', required=True,
... fieldType='dropdown', vocabulary=u'USA\nGermany'),
... name='address',
... manager=workshop,
... ))
Registration of Clients
=======================
So we are now ready to register participants.
>>> client1 = IClientFactory(workshop)()
>>> client1Name = workshop.addClient(client1)
>>> data = dict(addr=u'Mr', lastName=u'Skywalker', email='luke@skywalker.universe')
>>> inst = component.getAdapter(client1, IInstance, name='editor')
>>> inst.template = workshop.clientSchemas['person']
>>> state = inst.applyTemplate(data=data)
>>> list(client1.__schema_attributes__)
['schema.client.__standard__', 'schema.client.person']
>>> client1.__schema_attributes__['schema.client.person']['lastName']
u'Skywalker'
Instead of directly peeking into the attributes we can also use a
suitable instance adapter.
>>> inst = IInstance(client1)
>>> inst.template = workshop.clientSchemas['person']
>>> inst.applyTemplate()
{'acadTitles': u'', 'standard.lastName': u'Skywalker', 'addr': u'Mr',
'firstName': u'', 'lastName': u'Skywalker', 'age': '', '__name__': '...',
'email': u'luke@skywalker.universe',
'standard.email': u'luke@skywalker.universe'}
Note that the ``standardFieldName`` setting for the ``lastName`` field
results in a 'standard.lastName' entry; this technique may be used to
retrieve certain standard informations from a client without having to
use a template.
Using schema views for displaying and editing data
--------------------------------------------------
We need some additional setup for working with schema views - so we have to
supply some session handling stuff in order to work with client names.
>>> from zope.interface import implements
>>> from zope.app.session.interfaces import IClientIdManager, ISessionDataContainer
>>> from zope.app.session import session
>>> component.provideAdapter(session.ClientId)
>>> component.provideAdapter(session.Session)
>>> component.provideUtility(session.RAMSessionDataContainer(), ISessionDataContainer)
>>> class ClientIdManager(object):
... implements(IClientIdManager)
... def getClientId(self, request): return 'dummy'
>>> component.provideUtility(ClientIdManager())
>>> from zope.publisher.browser import TestRequest
>>> from cybertools.composer.schema.browser.schema import SchemaView
>>> request = TestRequest()
>>> schema = workshop.clientSchemas['person']
>>> view = SchemaView(schema, request)
Let's have a closer look at some of the view's attributes.
>>> [f.name for f in view.fields]
['lastName', 'firstName', 'email', 'age', 'addr', 'acadTitles']
>>> view.data
{}
Providing an id will enable the view to return the data of
the corresponding client.
>>> input = dict(id=client1Name)
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.data
{'acadTitles': u'', 'standard.lastName': u'Skywalker', 'addr': u'Mr',
'firstName': u'', 'lastName': u'Skywalker', 'age': '', '__name__': '...',
'email': u'luke@skywalker.universe',
'standard.email': u'luke@skywalker.universe'}
When we provide data and an 'update' action a new client object will
be created - if we clear the client name in the session first via
``view.setClientName('')``.
>>> input = dict(lastName='Solo', firstName='Han', email='han.solo@space.net',
... addr=u'Mr', action='update')
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.setClientName('')
>>> view.update()
False
>>> client2Name = view.clientName
>>> client2Name != client1Name
True
>>> input = dict(id=client2Name)
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.data
{'acadTitles': u'', 'standard.lastName': u'Solo', 'addr': u'Mr',
'firstName': u'Han', 'lastName': u'Solo',
'age': '', '__name__': '...', 'email': u'han.solo@space.net',
'standard.email': u'han.solo@space.net'}
If we provide an id parameter we may also change an existing client.
>>> input = dict(lastName=u'Skywalker', firstName=u'Luke', id=client1Name,
... email='luke@skywalker.universe', addr=u'Mr', action='update')
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.update()
False
>>> input = dict(id=client1Name)
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.data
{'acadTitles': u'', 'standard.lastName': u'Skywalker', 'addr': u'Mr',
'firstName': u'Luke', 'lastName': u'Skywalker', 'age': '', '__name__': '...',
'email': u'luke@skywalker.universe',
'standard.email': u'luke@skywalker.universe'}
If we do not provide a value for a required attribute we get a validation
error and the form will be displayed again.
>>> input = dict(firstName=u'Anakin', action='update')
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.update()
True
The same happens if we provide a number field with a string that cannot
be converted to an integer.
>>> input = dict(firstName=u'Anakin', lastName=u'Skywalker', age='foo',
... action='update')
>>> request = TestRequest(form=input)
>>> view = SchemaView(schema, request)
>>> view.update()
True
More on special field types
---------------------------
>>> schema2 = workshop.clientSchemas['address']
>>> countryField = schema2.fields.country
>>> countryField.getVocabularyItems()
[{'token': u'USA', 'title': u'USA'}, {'token': u'Germany', 'title': u'Germany'}]
Registering for Services Using a Registration Template
======================================================
>>> from cybertools.organize.service import RegistrationTemplate
>>> workshop.clientSchemas.append(RegistrationTemplate(
... name='regform',
... manager=workshop))
>>> regForm = workshop.clientSchemas['regform']
>>> list(regForm.services)
[<...ScheduledService object...>, <...ScheduledService object...>]
The registration action itself is performed using an IClientRegistrations
adapter for a client object.
>>> from cybertools.organize.interfaces import IClientRegistrations
>>> from cybertools.organize.service import ClientRegistrations
>>> component.provideAdapter(ClientRegistrations)
>>> regs = IClientRegistrations(client1)
>>> regs.template = regForm
>>> regs.register([regForm.services[0]])
>>> regs = list(regs.getRegistrations())
>>> regs
[<...Registration object...>]
>>> regs[0].client is client1
True
Using a registration template view for displaying and editing registration data
-------------------------------------------------------------------------------
(Note: after creating a view we usually clear the client name in the session
using ``view.setClientName('')`` in order to create a new client object.)
>>> from cybertools.organize.browser.service import RegistrationTemplateView
>>> request = TestRequest()
>>> regForm = workshop.clientSchemas['regform']
>>> view = RegistrationTemplateView(regForm, request)
>>> view.setClientName('')
>>> len(view.services)
2
>>> view.getRegistrations()
[]
Providing an id will enable the view to return the data of
the corresponding client.
>>> input = dict(id=client1Name)
>>> request = TestRequest(form=input)
>>> view = RegistrationTemplateView(regForm, request)
>>> regs = list(view.getRegistrations())
>>> regs
[<...Registration object...>]
>>> regs[0].client is client1
True
When we provide registration data and an 'update' action a new client object will
be created.
>>> input = dict(service_tokens=['event2'], action='update')
>>> request = TestRequest(form=input)
>>> view = RegistrationTemplateView(regForm, request)
>>> view.setClientName('')
>>> view.update()
False
>>> client3Name = view.clientName
>>> input = dict(id=client3Name)
>>> request = TestRequest(form=input)
>>> view = RegistrationTemplateView(regForm, request)
>>> regs = list(view.getRegistrations())
>>> regs
[<...Registration object...>]
>>> regs[0].client.__name__ == client3Name
True
>>> regs[0].service.token
'event2'
If we provide an id parameter we may also change an existing client.
>>> input = dict(service_tokens=['event1'], id=client1Name, action='update')
>>> request = TestRequest(form=input)
>>> view = RegistrationTemplateView(regForm, request)
>>> view.update()
False
>>> input = dict(id=client1Name)
>>> request = TestRequest(form=input)
>>> view = RegistrationTemplateView(regForm, request)
>>> regs = list(view.getRegistrations())
>>> len(regs)
1
>>> regs[0].service.token
'event1'
Let's finally look at the services and what they know about the clients
registered for them.
>>> clientNames = [client1Name, client2Name, client3Name]
>>> for svc in workshop.getServices():
... for cn, reg in sorted(svc.registrations.items()):
... print 'client-%i: ' % (clientNames.index(cn)+1), reg.service.name
client-1: event1
client-3: event2
More on Schema-based Forms and Template Views
=============================================
Navigation
----------
If a service provides more than one template (schema or registration
template) the form may show buttons to navigate to the next or previous
template together with saving the data entered.
>>> schema = workshop.clientSchemas['person']
>>> view = SchemaView(schema, TestRequest())
>>> view.getPreviousTemplate()
''
>>> view.getNextTemplate()
'http://127.0.0.1/workshop/address'
>>> schema = workshop.clientSchemas['address']
>>> view = SchemaView(schema, TestRequest())
>>> view.getPreviousTemplate()
'http://127.0.0.1/workshop/person'
>>> view.getNextTemplate()
'http://127.0.0.1/workshop/regform'
>>> schema = workshop.clientSchemas['regform']
>>> view = SchemaView(schema, TestRequest())
>>> view.previousTemplate
'http://127.0.0.1/workshop/address'
>>> view.getNextTemplate()
''
>>> view.getCheckoutView()
'http://127.0.0.1/workshop/checkout.html'
Message Definition and Rule Handling
====================================
Setting up a message manager with messages
------------------------------------------
>>> messageText = '''Dear $person.firstName $person.lastName,
... You have been registered for the following $services.
... $@@list_registrations_text
... $footer
... '''
>>> from cybertools.composer.message.interfaces import IMessageManager
>>> from cybertools.organize.service import MessageManagerAdapter
>>> component.provideAdapter(MessageManagerAdapter)
>>> messageManager = IMessageManager(workshop)
>>> messageManager.addMessage('feedback_text', messageText,
... subjectLine='Workshop Registration')
>>> messageManager.addMessage('footer', 'Best regards, $sender')
>>> messageManager.addMessage('sender', 'Jack')
>>> messageManager.addMessage('services', text='events')
Controlling actions with rules
------------------------------
Let's first set up a rule with two actions.
>>> from cybertools.composer.rule.base import Rule, Action, Event
>>> from cybertools.organize.service import eventTypes
>>> checkoutEvent = eventTypes['service.checkout']
>>> checkoutRule = Rule('checkout')
>>> checkoutRule.events.append(checkoutEvent)
>>> checkoutRule.actions.append(Action('sendmail',
... parameters=dict(sender='manager@workshops.com',
... messageName='feedback_text')))
We also have to provide rule instance and action handler adapters that
will do the real work.
>>> from cybertools.composer.rule.instance import RuleInstance
>>> from cybertools.composer.rule.interfaces import IRuleInstance
>>> component.provideAdapter(RuleInstance, provides=IRuleInstance)
>>> from cybertools.composer.rule.mail import MailActionHandler
>>> from cybertools.composer.rule.message import MessageActionHandler
>>> component.provideAdapter(MessageActionHandler, name='message')
>>> component.provideAdapter(MailActionHandler, name='sendmail')
We can now get a rule manager for our workshop and add the rule to it.
>>> from cybertools.composer.rule.interfaces import IRuleManager
>>> from cybertools.organize.service import RuleManagerAdapter
>>> component.provideAdapter(RuleManagerAdapter)
>>> ruleManager = IRuleManager(workshop)
>>> ruleManager.addRule(checkoutRule)
For testing purposes we also have to register a TestMailer that
just prints the message and other parameters.
>>> from cybertools.composer.rule.tests import TestMailer
>>> from zope.sendmail.interfaces import IMailDelivery
>>> component.provideUtility(TestMailer(), provides=IMailDelivery, name='Mail')
We are now ready to trigger a registration checkout.
>>> result = ruleManager.handleEvent(Event(checkoutEvent, client1))
sender: manager@workshops.com
recipients: [u'luke@skywalker.universe']
subject: Workshop Registration
message:
Dear Luke Skywalker,
You have been registered for the following events.
@@list_registrations_text
Best regards, Jack
<BLANKLINE>
In addition to sending mails we can also control the redirection to a
feedback or thankyou page with a rule.
>>> messageHtml = '''<p>Dear $person.firstName $person.lastName,</p>
... <p>You have been registered for the following $services.</p>
... <div>$@@list_registrations_html</div>
... <div>$footer</div>
... '''
>>> messageManager.addMessage('feedback_html', messageHtml)
>>> checkoutRule.actions.append(Action('redirect',
... parameters=dict(viewName='message_view.html',
... messageName='feedback_html')))
>>> from cybertools.composer.rule.web import RedirectActionHandler
>>> component.provideAdapter(RedirectActionHandler, name='redirect')
>>> request = TestRequest()
>>> request._app_names = ['workshop', 'checkout.html']
>>> result = ruleManager.handleEvent(Event(checkoutEvent, client1, request))
sender: ...
This redirects to a special message view that will deliver the
rendered message.
>>> from cybertools.composer.rule.web import MessageView
>>> input = dict(message='feedback_html')
>>> view = MessageView(workshop, TestRequest(form=input))
>>> view.getMessage()
u'<p>Dear Luke Skywalker...'
Service Manager and Service Views
=================================
>>> from cybertools.organize.browser.service import ServiceManagerView
>>> wsView = ServiceManagerView(workshop, TestRequest())
Service manager view
--------------------
The service manager view provides an ``overview()`` method that
allows a hierarchical presentation.
>>> overview = wsView.overview()
>>> for line in overview:
... print line['title'], line['isHeadline'], line['level']
Event True 0
Event 1 False 1
Event 2 False 1
Service view
------------
>>> from cybertools.organize.browser.service import ServiceView
>>> srvView = ServiceView(event1, TestRequest(form=dict(with_temporary='yes')))
The service view allows us to retrieve the registrations of a service, e.g.
to list them on a page template.
>>> regs = srvView.listRegistrations()
>>> len(list(regs))
1
>>> reg = regs.keys()[0]
There are also convenience methods for retrieving and formatting client
and registration data.
>>> regInfo = srvView.getRegistrationInfo(reg)
>>> clientInfo = srvView.getDataForClient(reg)
>>> print srvView.formatClientInfo(clientInfo)
Skywalker
>>> print regInfo['number'], regInfo['stateTitle']
1 temporary
The service view also provides fields that sum up the numbers of all
(non-temporary) registrations. (The number of waiting participants is empty
because the ``waitingList`` flag has not been set.)
>>> srvView.registeredTotalSubmitted
0
>>> srvView.registeredTotalsSubmitted
{'numberWaiting': '', 'number': 0}
Checkout
--------
After finishing registrating for services the user may check out (submit)
her registrations.
>>> from cybertools.organize.browser.service import CheckoutView
>>> checkout = CheckoutView(workshop, TestRequest())
>>> data = checkout.getClientData()
>>> list(sorted(data))
['__name__', 'errors', 'info_messages', 'service_registrations',
'standard.email', 'standard.lastName']
>>> checkout.getRegistrationsInfo()
[...]
>>> checkout.listRegistrationsText()
u'Event 1\nDatum: -\nUhrzeit: -\n\n'
When the user clicks the "Confirm Registration" button the corresponding
actions will be carried out.
>>> input = dict(action='update')
>>> request = TestRequest(form=input)
>>> request._app_names = ['workshop', 'checkout.html']
>>> checkout = CheckoutView(workshop, request)
>>> checkout.update()
sender: unknown@sender.com
recipients: [u'luke@skywalker.universe']
subject: Workshop Registration
message: ...
False
The checkout procedure has set the registrations' state to 'submitted'.
>>> srvView = ServiceView(event1, TestRequest())
>>> srvView.getRegistrationInfo(reg)['state']
'submitted'
>>> srvView.registeredTotalSubmitted
1
What happens if we submit a registration again?
(We supply the clientName because this may have been cleared on checkout
for security reasons.)
>>> input = dict(action='update', id=client1Name)
>>> request = TestRequest(form=input)
>>> request._app_names = ['workshop', 'checkout.html']
>>> checkout = CheckoutView(workshop, request)
>>> checkout.update()
sender: unknown@sender.com
recipients: [u'luke@skywalker.universe']
subject: Workshop Registration
message: ...
False
>>> srvView.getRegistrationInfo(reg)['state']
'submitted'
Waiting List
============
The use of the waiting list is controlled by the ``waitingList`` flag.
>>> event1.waitingList
False
>>> len(event1.registrations)
1
>>> event1.availableCapacity
4
We now limit the capacity of the event to 1 so that there is no place available
any more.
>>> event1.capacity = 1
>>> event1.availableCapacity
0
>>> regView = RegistrationTemplateView(regForm, TestRequest())
>>> regView.setClientName('')
>>> regView.allowRegistration(event1)
False
If we now set the ``waitingList`` flag to True further registration is
possible.
>>> event1.waitingList = True
>>> regView.allowRegistration(event1)
True
>>> input = dict(lastName='Walker', firstName='John', email='john@walker.tv',
... addr=u'Mr', action='update')
>>> request = TestRequest(form=input)
>>> schema = workshop.clientSchemas['person']
>>> view = SchemaView(schema, request)
>>> view.setClientName('')
>>> view.update()
False
>>> client4Name = view.clientName
>>> input = {'action': 'update', 'service.event1': 3, 'id': client4Name}
>>> regView = RegistrationTemplateView(regForm, TestRequest(form=input))
>>> regView.update()
False
>>> for reg in sorted(event1.registrations.values(), key=lambda x: x.number):
... print reg.number, reg.numberWaiting
0 3
1 0
Let's fix the last registration by running the checkout process.
>>> input = dict(action='update', id=client4Name)
>>> request = TestRequest(form=input)
>>> request._app_names = ['workshop', 'checkout.html']
>>> checkout = CheckoutView(workshop, request)
>>> checkout.update()
sender: ...
Taking over free places
-----------------------
What happens if one of the participants cancels her registration?
>>> input = {'action': 'update', 'service.event1': 0, 'id': client1Name}
>>> regView = RegistrationTemplateView(regForm, TestRequest(form=input))
>>> regView.update()
False
>>> for reg in sorted(event1.registrations.values(), key=lambda x: x.number):
... print reg.number, reg.numberWaiting
0 3
As the participants on the waiting list get priority over newly registering
participants the available capacity will still be 0 as long as there are
people on the waiting list.
>>> event1.availableCapacity
0
>>> input = {'action': 'update', 'service.event1': 2}
>>> regView = RegistrationTemplateView(regForm, TestRequest(form=input))
>>> regView.setClientName('')
>>> regView.update()
False
>>> for reg in sorted(event1.registrations.values(), key=lambda x: x.numberWaiting):
... print reg.number, reg.numberWaiting
0 2
0 3
Now the participant on the waiting list registers again - and gets the
free place.
>>> input = {'action': 'update', 'service.event1': 3, 'id': client4Name}
>>> regView = RegistrationTemplateView(regForm, TestRequest(form=input))
>>> regView.update()
False
>>> for reg in sorted(event1.registrations.values(), key=lambda x: x.number):
... print reg.number, reg.numberWaiting
0 2
1 2
Let's finally fix this registration by running the checkout process.
>>> input = dict(action='update', id=client4Name)
>>> request = TestRequest(form=input)
>>> request._app_names = ['workshop', 'checkout.html']
>>> checkout = CheckoutView(workshop, request)
>>> checkout.update()
sender: ...
Excel Export
============
>>> from cybertools.organize.browser.report import RegistrationsExportCsv
>>> input = dict(get_data_method='getData')
>>> csv = RegistrationsExportCsv(workshop, TestRequest(form=input))
>>> print csv.render()
"Service","Client ID","Organization","First Name","Last Name","E-Mail","Number","State"
"Event 1","...","","Walker","","john@walker.tv",1,"submitted"
>>> input = dict(get_data_method='getAllDataInColumns')
>>> csv = RegistrationsExportCsv(workshop, TestRequest(form=input))
>>> result = csv.render().splitlines()
>>> print result[0]
"Client ID","Time Stamp","Last Name","First Name","Email Address","Age","Personal Address","Academic Titles","Street","City","Country","Event 1","WL Event 1","Event 2","WL Event 2"
>>> len(result)
2
>>> print [f.strip('"') for f in result[1].split(',')[1:]]
['...', 'Walker', 'John', 'john@walker.tv', '', 'Mr', '', '', '', '', '1', '2', '0', '0']
Removal of services and clients
===============================
>>> from cybertools.organize.service import serviceRemoved, clientRemoved
>>> client4 = workshop.clients[client4Name]
>>> regs = IClientRegistrations(client4)
>>> regs.template = regForm
>>> len(regs.getRegistrations())
1
>>> serviceRemoved(event1, None)
>>> len(regs.getRegistrations())
0
>>> regs.register([event2])
>>> len(event2.registrations)
2
>>> clientRemoved(client4, None)
>>> len(event2.registrations)
1
Compound Services
=================
Let's set up a new service manager with a fairly complicated multi-day
conference that is organized in parallel tracks.
>>> conference = site['conference'] = service.ServiceManager()
>>> conference.__parent__ = site
>>> conference.__name__ = 'conference'
>>> main = service.Service('conf_main', manager=conference,
... title=u'Conference', capacity=5)
>>> conference.services.append(main)
Fin de partie
=============
>>> placefulTearDown()