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

Last change on this file since 6353 was 6353, checked in by uli, 13 years ago

Split workflow components (general use stuff goes to w.s.workflow), add some convenience stuff, ...)

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