diff --git a/composer/schema/browser/common.py b/composer/schema/browser/common.py index ef19a39..795f57f 100644 --- a/composer/schema/browser/common.py +++ b/composer/schema/browser/common.py @@ -124,7 +124,10 @@ class BaseView(object): submit=getCheckoutView, ) - #@Lazy # must be method for Zope 2.9 compatibility :-( ??? + @Lazy + def nextUrl(self): + return self.getNextUrl() + def getNextUrl(self): #viewName = 'thankyou.html' viewName = '' diff --git a/composer/schema/browser/schema.py b/composer/schema/browser/schema.py index 7813782..5495cbf 100644 --- a/composer/schema/browser/schema.py +++ b/composer/schema/browser/schema.py @@ -73,7 +73,7 @@ class SchemaView(BaseView): return True if self.isManageMode: # Don't store anything when editing - self.request.response.redirect(self.nextUrl()) + self.request.response.redirect(self.getNextUrl()) return False manager = self.context.getManager() if clientName: diff --git a/composer/schema/field.py b/composer/schema/field.py index 84e6b4b..17bf4cd 100644 --- a/composer/schema/field.py +++ b/composer/schema/field.py @@ -93,9 +93,11 @@ class Field(Component): def getFieldTypeInfo(self): return fieldTypes.getTerm(self.fieldType) - def getFieldInstance(self): + def getFieldInstance(self, clientInstance=None): instanceName = self.getFieldTypeInfo().instanceName - return component.getAdapter(self, IFieldInstance, name=instanceName) + fi = component.getAdapter(self, IFieldInstance, name=instanceName) + fi.clientInstance = clientInstance + return fi class FieldInstance(object): @@ -111,7 +113,7 @@ class FieldInstance(object): self.change = None def marshall(self, value): - return value + return value or u'' #return toStr(value) def display(self, value): @@ -140,7 +142,7 @@ class NumberFieldInstance(FieldInstance): def display(self, value): if value is None: - return '-' + return '' return str(value) def unmarshall(self, value): @@ -166,3 +168,9 @@ class FileUploadFieldInstance(FieldInstance): def unmarshall(self, value): return value + + +class CalculatedFieldInstance(FieldInstance): + + def marshall(self, value): + return str(value) diff --git a/composer/schema/instance.py b/composer/schema/instance.py index 71b3405..ec4303b 100644 --- a/composer/schema/instance.py +++ b/composer/schema/instance.py @@ -50,7 +50,7 @@ class Instance(BaseInstance): if not f.storeData: # a dummy field, e.g. a spacer continue - fi = f.getFieldInstance() + fi = f.getFieldInstance(self) name = f.name value = getattr(self.context, name, f.defaultValue) #value = getattr(self.context, name, u'') @@ -147,9 +147,10 @@ class ClientInstance(object): if not f.storeData: # a dummy field, e.g. a spacer continue - fi = f.getFieldInstance() + fi = f.getFieldInstance(self) name = f.name - value = values.get(name, u'') + #value = values.get(name, u'') + value = values.get(name, f.defaultValue) value = mode == 'view' and fi.display(value) or fi.marshall(value) result[name] = value # update result with standard fields: diff --git a/composer/schema/interfaces.py b/composer/schema/interfaces.py index 555a49b..533bf2c 100644 --- a/composer/schema/interfaces.py +++ b/composer/schema/interfaces.py @@ -91,17 +91,21 @@ fieldTypes = SimpleVocabulary(( instanceName='fileupload'), #FieldType('checkbox', 'checkbox', u'Checkbox'), FieldType('dropdown', 'dropdown', u'Drop-down selection'), + FieldType('calculated', 'display', u'Calculated Value', + instanceName='calculated'), FieldType('spacer', 'spacer', u'Spacer', fieldRenderer='field_spacer', storeData=False), )) -# TODO: move this to organize.service... +# TODO: move this to organize.service... (???) standardFieldNames = SimpleVocabulary(( SimpleTerm('', '', 'Not selected'), SimpleTerm('lastName', 'lastName', 'Last name'), SimpleTerm('firstName', 'firstName', 'First name'), SimpleTerm('organization', 'organization', 'Organization'), SimpleTerm('email', 'email', 'E-Mail address'), + SimpleTerm('number', 'number', 'Number of participants'), + # TODO: on organize.service: extend this list, e.g. with 'totalCost' )) class IField(IComponent): @@ -135,7 +139,8 @@ class IField(IComponent): vocabulary=standardFieldNames,) defaultValue = schema.TextLine( title=_(u'Default'), - description=_(u'Value with which to pre-set the field contents'), + description=_(u'Value with which to pre-set the field contents. ' + 'Use this also for populating a calculated field.'), required=False,) required = schema.Bool( title=_(u'Required'), diff --git a/organize/browser/service.py b/organize/browser/service.py index 3bbb23c..bf2060b 100644 --- a/organize/browser/service.py +++ b/organize/browser/service.py @@ -72,7 +72,11 @@ class BaseView(SchemaBaseView): class ServiceManagerView(BaseView): + isManageMode = False + def getCustomView(self): + if self.isManageMode: + return None viewName = self.context.getViewName() if viewName: return component.getMultiAdapter((self.context, self.request), @@ -103,6 +107,10 @@ class ServiceManagerView(BaseView): tpl = self.getRegistrationTemplate() return self.getUrlForObject(tpl) + def redirectToRegistration(self): + self.request.response.redirect(self.registrationUrl()) + return 'redirect' # let template skip rendering + def overview(self, includeCategories=None): result = [] classific = [] @@ -270,6 +278,8 @@ class ServiceView(BaseView): class RegistrationTemplateView(BaseView): + state = None + @Lazy def services(self): return self.getServices() @@ -278,6 +288,11 @@ class RegistrationTemplateView(BaseView): return self.context.getServices() #return sorted(self.context.getServices().values(), key=self.sortKey) + def overview(self): + categories = self.context.categories or None + mv = ServiceManagerView(self.context.getManager(), self.request) + return mv.overview(categories) + def sortKey(self, svc): return (svc.category, svc.getClassification(), svc.start) @@ -314,6 +329,7 @@ class RegistrationTemplateView(BaseView): return instance.applyTemplate() def update(self): + newClient = False form = self.request.form clientName = self.getClientName() if not form.get('action'): @@ -325,13 +341,11 @@ class RegistrationTemplateView(BaseView): return True else: client = IClientFactory(manager)() - clientName = manager.addClient(client) - self.setClientName(clientName) - regs = IClientRegistrations(client) + newClient = True # make persistent later + regs = self.state = IClientRegistrations(client) regs.template = self.context services = manager.getServices() # a mapping! allServices = services.values() - oldServices = [r.service for r in regs.getRegistrations()] # collect check boxes: newServices = [services[token] for token in form.get('service_tokens', [])] @@ -345,10 +359,18 @@ class RegistrationTemplateView(BaseView): if value > 0: newServices.append(svc) numbers.append(value) + regs.validate(clientName, newServices, numbers) + if regs.severity > 0: + return True + if newClient: + clientName = manager.addClient(client) + self.setClientName(clientName) regs.register(newServices, numbers=numbers) + oldServices = [r.service for r in regs.getRegistrations()] toDelete = [s for s in oldServices if s in allServices and s not in newServices] regs.unregister(toDelete) - #return True self.request.response.redirect(self.getNextUrl()) return False + + diff --git a/organize/interfaces.py b/organize/interfaces.py index ca71a2d..3ad1914 100644 --- a/organize/interfaces.py +++ b/organize/interfaces.py @@ -128,6 +128,8 @@ class ITask(Interface): serviceManagerViews = SimpleVocabulary(( SimpleTerm('', '', u'Default view'), SimpleTerm('events_overview.html', 'events_overview.html', u'Events overview'), + SimpleTerm('redirect_registration.html', 'redirect_registration.html', + u'Redirect to registration') )) class IServiceManager(Interface): @@ -145,7 +147,7 @@ class IServiceManager(Interface): viewName = schema.Choice( title=_(u'View name'), description=_(u'Select the name of a specialized view to be used ' - 'for presenting this object.'), + 'for presenting this object for visitors.'), vocabulary=serviceManagerViews, default='', required=False,) diff --git a/organize/service.py b/organize/service.py index 0bf8aaf..5fd8114 100644 --- a/organize/service.py +++ b/organize/service.py @@ -160,9 +160,10 @@ class Service(object): @property def availableCapacity(self): - if self.capacity >= 0 and len(self.registrations) >= self.capacity: + number = self.getNumberRegistered() + if self.capacity >= 0 and number >= self.capacity: return 0 - return self.capacity - len(self.registrations) + return self.capacity - number def register(self, client, number=1): clientName = client.__name__ @@ -170,20 +171,22 @@ class Service(object): reg = self.registrations[clientName] if number != reg.number: reg.number = number - #self.registrations[clientName] = reg # persistence hack return reg reg = self.registrationFactory(client, self, number) self.registrations[clientName] = reg return reg - #if self.availableCapacity: - # TODO: handle case when no capacity available - - # probably on 'submit' transition; UI feedback? def unregister(self, client): clientName = client.__name__ if clientName in self.registrations: del self.registrations[clientName] + def getNumberRegistered(self): + result = 0 + for r in self.registrations.values(): + result += r.number + return result + # default methods def getAllowRegWithNumberFromManager(self): return getattr(self.getManager(), 'allowRegWithNumber', None) @@ -240,6 +243,9 @@ class ClientRegistrations(object): registrationsAttributeName = '__service_registrations__' + errors = None + severity = 0 + def __init__(self, context): self.context = context @@ -266,6 +272,46 @@ class ClientRegistrations(object): regs = (r for r in regs if r.service in svcs) return regs + def validate(self, clientName, services, numbers=None): + self.errors = {} + if numbers is None: + numbers = len(services) * [1] + for svc, n in zip(services, numbers): + oldReg = svc.registrations.get(clientName, None) + oldN = oldReg and oldReg.number or 0 + if svc.capacity and svc.capacity > 0 and svc.availableCapacity < n - oldN: + error = registrationErrors['capacity_exceeded'] + entry = self.errors.setdefault(svc.token, []) + entry.append(error) + self.severity = max(self.severity, error.severity) + + +class RegistrationError(object): + + def __init__(self, title, description=None, severity=5, **kw): + self.title = title + self.description = description or title + self.severity = severity + for k, v in kw.items(): + setattr(self, k, v) + + def __str__(self): + return self.title + + def __repr__(self): + return "RegistrationError('%s')" % self.title + + +registrationErrors = dict( + capacity_exceeded=RegistrationError( + u'The capacity for this service has been exceeded.'), + time_conflict=RegistrationError( + u'You have registered already for another service at the same time.'), + number_exceeded=RegistrationError( + u'The total number of participants you are registering is less than the ' + 'number of persons you want to register for this service.'), +) + # registration states