source: main/waeup.sirp/trunk/src/waeup/sirp/applicants/browser.py @ 6358

Last change on this file since 6358 was 6358, checked in by Henrik Bettermann, 13 years ago

Extend pdf slip example by fetching some widgets (experimental!).

File size: 24.2 KB
Line 
1##
2## browser.py
3## Login : <uli@pu.smp.net>
4## Started on  Sun Jun 27 11:03:10 2010 Uli Fouquet & Henrik Bettermann
5## $Id$
6##
7## Copyright (C) 2010 Uli Fouquet & Henrik Bettermann
8## This program is free software; you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation; either version 2 of the License, or
11## (at your option) any later version.
12##
13## This program is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16## GNU General Public License for more details.
17##
18## You should have received a copy of the GNU General Public License
19## along with this program; if not, write to the Free Software
20## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21##
22"""UI components for basic applicants and related components.
23"""
24import sys
25import grok
26
27from datetime import datetime
28from zope.formlib.widget import CustomWidgetFactory
29from zope.formlib.form import setUpEditWidgets
30from zope.securitypolicy.interfaces import IPrincipalRoleManager
31from zope.traversing.browser import absoluteURL
32
33from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
34
35from reportlab.pdfgen import canvas
36
37from waeup.sirp.browser import (
38    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
39from waeup.sirp.browser.breadcrumbs import Breadcrumb
40from waeup.sirp.browser.layout import NullValidator
41from waeup.sirp.browser.pages import add_local_role, del_local_roles
42from waeup.sirp.browser.resources import datepicker, tabs, datatable
43from waeup.sirp.browser.viewlets import (
44    ManageActionButton, PrimaryNavTab, LeftSidebarLink
45    )
46from waeup.sirp.image.browser.widget import (
47    ThumbnailWidget, EncodingImageFileWidget,
48    )
49from waeup.sirp.interfaces import IWAeUPObject, ILocalRolesAssignable
50from waeup.sirp.permissions import get_users_with_local_roles
51from waeup.sirp.university.interfaces import ICertificate
52from waeup.sirp.widgets.datewidget import (
53    FriendlyDateWidget, FriendlyDateDisplayWidget)
54from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
55from waeup.sirp.widgets.objectwidget import (
56    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
57from waeup.sirp.widgets.multilistwidget import (
58    MultiListWidget, MultiListDisplayWidget)
59from waeup.sirp.applicants import ResultEntry, Applicant, get_applicant_data
60from waeup.sirp.applicants.interfaces import (
61    IApplicant, IApplicantPrincipal,IApplicantEdit, IApplicantsRoot,
62    IApplicantsContainer, IApplicantsContainerAdd, application_types_vocab
63    )
64from waeup.sirp.applicants.workflow import INITIALIZED, STARTED
65
66results_widget = CustomWidgetFactory(
67    WAeUPObjectWidget, ResultEntry)
68
69results_display_widget = CustomWidgetFactory(
70    WAeUPObjectDisplayWidget, ResultEntry)
71
72list_results_widget = CustomWidgetFactory(
73    MultiListWidget, subwidget=results_widget)
74
75list_results_display_widget = CustomWidgetFactory(
76    MultiListDisplayWidget, subwidget=results_display_widget)
77
78#TRANSITION_OBJECTS = create_workflow()
79
80#TRANSITION_DICT = dict([
81#    (transition_object.transition_id,transition_object.title)
82#    for transition_object in TRANSITION_OBJECTS])
83
84class ApplicantsRootPage(WAeUPPage):
85    grok.context(IApplicantsRoot)
86    grok.name('index')
87    grok.require('waeup.Public')
88    title = 'Applicants'
89    label = 'Application Section'
90    pnav = 3
91
92    def update(self):
93        super(ApplicantsRootPage, self).update()
94        datatable.need()
95        return
96
97class ManageApplicantsRootActionButton(ManageActionButton):
98    grok.context(IApplicantsRoot)
99    grok.view(ApplicantsRootPage)
100    grok.require('waeup.manageApplications')
101    text = 'Manage application section'
102
103class ApplicantsRootManageFormPage(WAeUPEditFormPage):
104    grok.context(IApplicantsRoot)
105    grok.name('manage')
106    grok.template('applicantsrootmanagepage')
107    title = 'Applicants'
108    label = 'Manage application section'
109    pnav = 3
110    grok.require('waeup.manageApplications')
111    taboneactions = ['Add applicants container', 'Remove selected','Cancel']
112    tabtwoactions1 = ['Remove selected local roles']
113    tabtwoactions2 = ['Add local role']
114    subunits = 'Applicants Containers'
115
116    def update(self):
117        tabs.need()
118        datatable.need()
119        return super(ApplicantsRootManageFormPage, self).update()
120
121    def getLocalRoles(self):
122        roles = ILocalRolesAssignable(self.context)
123        return roles()
124
125    def getUsers(self):
126        """Get a list of all users.
127        """
128        for key, val in grok.getSite()['users'].items():
129            url = self.url(val)
130            yield(dict(url=url, name=key, val=val))
131
132    def getUsersWithLocalRoles(self):
133        return get_users_with_local_roles(self.context)
134
135    # ToDo: Show warning message before deletion
136    @grok.action('Remove selected')
137    def delApplicantsContainers(self, **data):
138        form = self.request.form
139        child_id = form['val_id']
140        if not isinstance(child_id, list):
141            child_id = [child_id]
142        deleted = []
143        for id in child_id:
144            try:
145                del self.context[id]
146                deleted.append(id)
147            except:
148                self.flash('Could not delete %s: %s: %s' % (
149                        id, sys.exc_info()[0], sys.exc_info()[1]))
150        if len(deleted):
151            self.flash('Successfully removed: %s' % ', '.join(deleted))
152        self.redirect(self.url(self.context, '@@manage')+'#tab-1')
153        return
154
155    @grok.action('Add applicants container', validator=NullValidator)
156    def addApplicantsContainer(self, **data):
157        self.redirect(self.url(self.context, '@@add'))
158        return
159
160    @grok.action('Cancel', validator=NullValidator)
161    def cancel(self, **data):
162        self.redirect(self.url(self.context))
163        return
164
165    @grok.action('Add local role', validator=NullValidator)
166    def addLocalRole(self, **data):
167        return add_local_role(self,2, **data)
168
169    @grok.action('Remove selected local roles')
170    def delLocalRoles(self, **data):
171        return del_local_roles(self,2,**data)
172
173class ApplicantsContainerAddFormPage(WAeUPAddFormPage):
174    grok.context(IApplicantsRoot)
175    grok.require('waeup.manageApplications')
176    grok.name('add')
177    grok.template('applicantscontaineraddpage')
178    title = 'Applicants'
179    label = 'Add applicants container'
180    pnav = 3
181
182    form_fields = grok.AutoFields(
183        IApplicantsContainerAdd).omit('code').omit('title')
184    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
185    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
186
187    def update(self):
188        datepicker.need() # Enable jQuery datepicker in date fields.
189        return super(ApplicantsContainerAddFormPage, self).update()
190
191    @grok.action('Add applicants container')
192    def addApplicantsContainer(self, **data):
193        year = data['year']
194        code = u'%s%s' % (data['prefix'], year)
195        prefix = application_types_vocab.getTerm(data['prefix'])
196        title = u'%s %s/%s' % (prefix.title, year, year + 1)
197        if code in self.context.keys():
198            self.flash(
199                'An applicants container for the same application '
200                'type and entrance year exists already in the database.')
201            return
202        # Add new applicants container...
203        provider = data['provider'][1]
204        container = provider.factory()
205        self.applyData(container, **data)
206        container.code = code
207        container.title = title
208        self.context[code] = container
209        self.flash('Added: "%s".' % code)
210        self.redirect(self.url(self.context, u'@@manage')+'#tab-1')
211        return
212
213    @grok.action('Cancel', validator=NullValidator)
214    def cancel(self, **data):
215        self.redirect(self.url(self.context, '@@manage') + '#tab-1')
216
217class ApplicantsRootBreadcrumb(Breadcrumb):
218    """A breadcrumb for applicantsroot.
219    """
220    grok.context(IApplicantsRoot)
221    title = u'Application Section'
222
223class ApplicantsContainerBreadcrumb(Breadcrumb):
224    """A breadcrumb for applicantscontainers.
225    """
226    grok.context(IApplicantsContainer)
227
228class ApplicantBreadcrumb(Breadcrumb):
229    """A breadcrumb for applicants.
230    """
231    grok.context(IApplicant)
232
233    @property
234    def title(self):
235        """Get a title for a context.
236        """
237        return self.context.access_code
238
239class ApplicantsTab(PrimaryNavTab):
240    """Applicants tab in primary navigation.
241    """
242
243    grok.context(IWAeUPObject)
244    grok.order(3)
245    grok.require('waeup.Public')
246    grok.template('primarynavtab')
247
248    pnav = 3
249    tab_title = u'Applicants'
250
251    @property
252    def link_target(self):
253        return self.view.application_url('applicants')
254
255class ApplicantsContainerPage(WAeUPDisplayFormPage):
256    """The standard view for regular applicant containers.
257    """
258    grok.context(IApplicantsContainer)
259    grok.name('index')
260    grok.require('waeup.Public')
261    grok.template('applicantscontainerpage')
262    pnav = 3
263
264    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
265    form_fields['startdate'].custom_widget = FriendlyDateDisplayWidget('le')
266    form_fields['enddate'].custom_widget = FriendlyDateDisplayWidget('le')
267    form_fields['description'].custom_widget = ReSTDisplayWidget
268
269    @property
270    def title(self):
271        return "Applicants Container: %s" % self.context.title
272
273    @property
274    def label(self):
275        return self.context.title
276
277class ApplicantsContainerManageActionButton(ManageActionButton):
278    grok.order(1)
279    grok.context(IApplicantsContainer)
280    grok.view(ApplicantsContainerPage)
281    grok.require('waeup.manageApplications')
282    text = 'Manage applicants container'
283
284class LoginApplicantActionButton(ManageActionButton):
285    grok.order(2)
286    grok.context(IApplicantsContainer)
287    grok.view(ApplicantsContainerPage)
288    grok.require('waeup.Anonymous')
289    icon = 'login.png'
290    text = 'Login for applicants'
291    target = 'login'
292
293class ApplicantsContainerManageFormPage(WAeUPEditFormPage):
294    grok.context(IApplicantsContainer)
295    grok.name('manage')
296    grok.template('applicantscontainermanagepage')
297    form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
298    taboneactions = ['Save','Cancel']
299    tabtwoactions = ['Add applicant', 'Remove selected','Cancel']
300    tabthreeactions1 = ['Remove selected local roles']
301    tabthreeactions2 = ['Add local role']
302    # Use friendlier date widget...
303    form_fields['startdate'].custom_widget = FriendlyDateWidget('le')
304    form_fields['enddate'].custom_widget = FriendlyDateWidget('le')
305    grok.require('waeup.manageApplications')
306
307    @property
308    def title(self):
309        return "Applicants Container: %s" % self.context.title
310
311    @property
312    def label(self):
313        return 'Manage applicants container'
314
315    pnav = 3
316
317    def update(self):
318        datepicker.need() # Enable jQuery datepicker in date fields.
319        tabs.need()
320        datatable.need()  # Enable jQurey datatables for contents listing
321        return super(ApplicantsContainerManageFormPage, self).update()
322
323    def getLocalRoles(self):
324        roles = ILocalRolesAssignable(self.context)
325        return roles()
326
327    def getUsers(self):
328        """Get a list of all users.
329        """
330        for key, val in grok.getSite()['users'].items():
331            url = self.url(val)
332            yield(dict(url=url, name=key, val=val))
333
334    def getUsersWithLocalRoles(self):
335        return get_users_with_local_roles(self.context)
336
337    @grok.action('Save')
338    def apply(self, **data):
339        self.applyData(self.context, **data)
340        self.flash('Data saved.')
341        return
342
343    # ToDo: Show warning message before deletion
344    @grok.action('Remove selected')
345    def delApplicant(self, **data):
346        form = self.request.form
347        if form.has_key('val_id'):
348            child_id = form['val_id']
349        else:
350            self.flash('No applicant selected!')
351            self.redirect(self.url(self.context, '@@manage')+'#tab-2')
352            return
353        if not isinstance(child_id, list):
354            child_id = [child_id]
355        deleted = []
356        for id in child_id:
357            try:
358                del self.context[id]
359                deleted.append(id)
360            except:
361                self.flash('Could not delete %s: %s: %s' % (
362                        id, sys.exc_info()[0], sys.exc_info()[1]))
363        if len(deleted):
364            self.flash('Successfully removed: %s' % ', '.join(deleted))
365        self.redirect(self.url(self.context, u'@@manage')+'#tab-2')
366        return
367
368    @grok.action('Add applicant', validator=NullValidator)
369    def addApplicant(self, **data):
370        self.redirect(self.url(self.context, 'addapplicant'))
371        return
372
373    @grok.action('Cancel', validator=NullValidator)
374    def cancel(self, **data):
375        self.redirect(self.url(self.context))
376        return
377
378    @grok.action('Add local role', validator=NullValidator)
379    def addLocalRole(self, **data):
380        return add_local_role(self,3, **data)
381
382    @grok.action('Remove selected local roles')
383    def delLocalRoles(self, **data):
384        return del_local_roles(self,3,**data)
385
386class LoginApplicant(WAeUPPage):
387    grok.context(IApplicantsContainer)
388    grok.name('login')
389    grok.require('waeup.Public')
390
391    @property
392    def title(self):
393        return u"Applicant Login: %s" % self.context.title
394
395    @property
396    def label(self):
397        return u'Login for applicants only'
398
399    pnav = 3
400
401    @property
402    def ac_prefix(self):
403        return self.context.ac_prefix
404
405    def update(self, SUBMIT=None):
406        self.ac_series = self.request.form.get('form.ac_series', None)
407        self.ac_number = self.request.form.get('form.ac_number', None)
408        if SUBMIT is None:
409            return
410        if self.request.principal.id == 'zope.anybody':
411            self.flash('Entered credentials are invalid.')
412            return
413        if not IApplicantPrincipal.providedBy(self.request.principal):
414            # Don't care if user is already authenticated as non-applicant
415            return
416        pin = self.request.principal.access_code
417        if pin not in self.context.keys():
418            # Create applicant record
419            applicant = Applicant()
420            applicant.access_code = pin
421            self.context[pin] = applicant
422        # Assign current principal the owner role on created applicant
423        # record
424        role_manager = IPrincipalRoleManager(self.context[pin])
425        role_manager.assignRoleToPrincipal(
426            'waeup.local.ApplicationOwner', self.request.principal.id)
427        # Assign current principal the PortalUser role
428        role_manager = IPrincipalRoleManager(grok.getSite()['faculties'])
429        role_manager.assignRoleToPrincipal(
430            'waeup.PortalUser', self.request.principal.id)
431        # XXX: disable for now. Pins will get a different workflow.
432        #state = IWorkflowState(self.context[pin]).getState()
433        #if state == INITIALIZED:
434        #    IWorkflowInfo(self.context[pin]).fireTransition('start')
435        self.redirect(self.url(self.context[pin], 'edit'))
436        return
437
438class ApplicantAddFormPage(WAeUPAddFormPage):
439    """Add-form to add certificate to a department.
440    """
441    grok.context(IApplicantsContainer)
442    grok.require('waeup.manageApplications')
443    grok.name('addapplicant')
444    grok.template('applicantaddpage')
445    title = 'Applicants'
446    label = 'Add applicant'
447    pnav = 3
448
449    @property
450    def title(self):
451        return "Applicants Container: %s" % self.context.title
452
453    @property
454    def ac_prefix(self):
455        return self.context.ac_prefix
456
457    @grok.action('Create application record')
458    def addApplicant(self, **data):
459        ac_series = self.request.form.get('form.ac_series', None)
460        ac_number = self.request.form.get('form.ac_number', None)
461        pin = '%s-%s-%s' % (self.ac_prefix,ac_series,ac_number)
462        if pin not in self.context.keys():
463            # Create applicant record
464            applicant = Applicant()
465            applicant.access_code = pin
466            self.context[pin] = applicant
467        self.redirect(self.url(self.context[pin], 'edit'))
468        return
469
470class AccessCodeLink(LeftSidebarLink):
471    grok.order(1)
472    grok.require('waeup.Public')
473
474    def render(self):
475        if not IApplicantPrincipal.providedBy(self.request.principal):
476            return ''
477        access_code = getattr(self.request.principal,'access_code',None)
478        if access_code:
479            applicant_object = get_applicant_data(access_code)
480            url = absoluteURL(applicant_object, self.request)
481            return u'<div class="portlet"><a href="%s/edit">%s</a></div>' % (
482                url,access_code)
483        return ''
484
485class DisplayApplicant(WAeUPDisplayFormPage):
486    grok.context(IApplicant)
487    grok.name('index')
488    grok.require('waeup.handleApplication')
489    form_fields = grok.AutoFields(IApplicant).omit(
490        'locked').omit('course_admitted')
491    #form_fields['fst_sit_results'].custom_widget = list_results_display_widget
492    form_fields['passport'].custom_widget = ThumbnailWidget
493    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
494    label = 'Applicant'
495    grok.template('form_display')
496    pnav = 3
497
498    @property
499    def title(self):
500        return '%s' % self.context.access_code
501
502    @property
503    def label(self):
504        container_title = self.context.__parent__.title
505        return '%s Application Record' % container_title
506
507    def getCourseAdmitted(self):
508        """Return link, title and code in html format to the certificate
509           admitted.
510        """
511        course_admitted = self.context.course_admitted
512        if ICertificate.providedBy(course_admitted):
513            url = self.url(course_admitted)
514            title = course_admitted.title
515            code = course_admitted.code
516            return '<a href="%s">%s (%s)</a>' %(url,title,code)
517        return 'not yet admitted'
518
519class PDFActionButton(ManageActionButton):
520    grok.context(IApplicant)
521    grok.view(DisplayApplicant)
522    grok.require('waeup.handleApplication')
523    icon = 'actionicon_pdf.png'
524    text = 'Download pdf slip'
525    target = 'application_slip.pdf'
526
527class ExportPDFPage(grok.View):
528    """Deliver a PDF slip of the context.
529    """
530    grok.context(IApplicant)
531    grok.name('application_slip.pdf')
532    grok.require('waeup.handleApplication')
533    form_fields = grok.AutoFields(IApplicant).omit(
534        'locked').omit('course_admitted')
535    form_fields['passport'].custom_widget = ThumbnailWidget
536    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
537
538    prefix = 'form'
539
540    def getCourseAdmitted(self):
541        """Return title and code in html format to the certificate
542           admitted.
543        """
544        course_admitted = self.context.course_admitted
545        if ICertificate.providedBy(course_admitted):
546            title = course_admitted.title
547            code = course_admitted.code
548            return '%s (%s)' %(title,code)
549        return 'not yet admitted'
550
551    def setUpWidgets(self, ignore_request=False):
552        self.adapters = {}
553        self.widgets = setUpEditWidgets(
554            self.form_fields, self.prefix, self.context, self.request,
555            adapters=self.adapters, for_display=True,
556            ignore_request=ignore_request
557            )
558
559    # Render a demo pdf page
560    def render(self):
561        from reportlab.pdfgen import canvas
562        from reportlab.lib.units import cm
563        from reportlab.lib.pagesizes import A4, landscape
564        from reportlab.lib.styles import getSampleStyleSheet
565        from reportlab.platypus import Frame, Paragraph
566
567        pdf = canvas.Canvas('application_slip.pdf',pagesize=A4)
568        width, height = A4
569        style = getSampleStyleSheet()
570        story = []
571        frame = Frame(1*cm,1*cm,width-(2*cm),height-(2*cm))
572        self.setUpWidgets()
573        #import pdb; pdb.set_trace()
574        for widget in self.widgets:
575            if widget.name != 'form.passport':
576                ptext = widget()
577                story.append(Paragraph(ptext, style["Normal"]))
578        frame.addFromList(story,pdf)
579        self.response.setHeader(
580            'Content-Type', 'application/pdf')
581        return pdf.getpdfdata()
582
583class ApplicantsManageActionButton(ManageActionButton):
584    grok.context(IApplicant)
585    grok.view(DisplayApplicant)
586    grok.require('waeup.manageApplications')
587    text = 'Edit application record'
588    target = 'edit_full'
589
590class EditApplicantFull(WAeUPEditFormPage):
591    """A full edit view for applicant data.
592    """
593    grok.context(IApplicant)
594    grok.name('edit_full')
595    grok.require('waeup.manageApplications')
596    form_fields = grok.AutoFields(IApplicant)   #.omit('locked')
597    form_fields['passport'].custom_widget = EncodingImageFileWidget
598    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
599    grok.template('form_edit')
600    manage_applications = True
601    pnav = 3
602
603    def update(self):
604        datepicker.need() # Enable jQuery datepicker in date fields.
605        super(EditApplicantFull, self).update()
606        self.wf_info = IWorkflowInfo(self.context)
607        return
608
609    @property
610    def title(self):
611        return self.context.access_code
612
613    @property
614    def label(self):
615        container_title = self.context.__parent__.title
616        return '%s Application Form' % container_title
617
618    def getTransitions(self):
619        """Return a list of dicts of allowed transition ids and titles.
620
621        Each list entry provides keys ``name`` and ``title`` for
622        internal name and (human readable) title of a single
623        transition.
624        """
625        allowed_transitions = self.wf_info.getManualTransitions()
626        return [dict(name='', title='No transition')] +[
627            dict(name=x, title=y) for x, y in allowed_transitions]
628
629    @grok.action('Save')
630    def save(self, **data):
631        self.applyData(self.context, **data)
632        self.context._p_changed = True
633        form = self.request.form
634        if form.has_key('transition') and form['transition']:
635            transition_id = form['transition']
636            self.wf_info.fireTransition(transition_id)
637        self.flash('Form has been saved.')
638        self.context.getApplicantsRootLogger().info('Saved')
639        return
640
641class EditApplicantStudent(EditApplicantFull):
642    """An applicant-centered edit view for applicant data.
643    """
644    grok.context(IApplicantEdit)
645    grok.name('edit')
646    grok.require('waeup.handleApplication')
647    form_fields = grok.AutoFields(IApplicantEdit).omit('locked')
648    form_fields['passport'].custom_widget = EncodingImageFileWidget
649    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
650    grok.template('form_edit')
651    manage_applications = False
652
653
654    def emitLockMessage(self):
655        self.flash('The requested form is locked (read-only).')
656        self.redirect(self.url(self.context))
657        return
658
659    def update(self):
660        if self.context.locked:
661            self.redirect(self.url(self.context))
662            return
663        datepicker.need() # Enable jQuery datepicker in date fields.
664        super(EditApplicantStudent, self).update()
665        return
666
667    def dataNotComplete(self):
668        if not self.request.form.get('confirm_passport', False):
669            return 'Passport confirmation box not ticked.'
670        if len(self.errors) > 0:
671            return 'Form has errors.'
672        return False
673
674    @grok.action('Save')
675    def save(self, **data):
676        if self.context.locked:
677            self.emitLockMessage()
678            return
679        self.applyData(self.context, **data)
680        self.context._p_changed = True
681        self.flash('Form has been saved.')
682        return
683
684    @grok.action('Final Submit')
685    def finalsubmit(self, **data):
686        if self.context.locked:
687            self.emitLockMessage()
688            return
689        self.applyData(self.context, **data)
690        self.context._p_changed = True
691        if self.dataNotComplete():
692            self.flash(self.dataNotComplete())
693            return
694        state = IWorkflowState(self.context).getState()
695        # This shouldn't happen, but the application officer
696        # might have forgotten to lock the form after changing the state
697        if state != STARTED:
698            self.flash('This form cannot be submitted. Wrong state!')
699            return
700        IWorkflowInfo(self.context).fireTransition('submit')
701        self.context.locked = True
702        self.flash('Form has been submitted.')
703        self.redirect(self.url(self.context))
704        return
705
Note: See TracBrowser for help on using the repository browser.