source: main/waeup.sirp/trunk/src/waeup/sirp/students/browser.py @ 6737

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

Add requestclearance action.

Let transition event handler also handle the locking and unlocking of the clearance form.

  • Property svn:keywords set to Id
File size: 23.7 KB
Line 
1## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
2## This program is free software; you can redistribute it and/or modify
3## it under the terms of the GNU General Public License as published by
4## the Free Software Foundation; either version 2 of the License, or
5## (at your option) any later version.
6##
7## This program is distributed in the hope that it will be useful,
8## but WITHOUT ANY WARRANTY; without even the implied warranty of
9## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10## GNU General Public License for more details.
11##
12## You should have received a copy of the GNU General Public License
13## along with this program; if not, write to the Free Software
14## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
15##
16"""UI components for students and related components.
17"""
18import sys
19import grok
20
21from datetime import datetime
22from zope.formlib.widget import CustomWidgetFactory
23from zope.formlib.form import setUpEditWidgets
24from zope.securitypolicy.interfaces import IPrincipalRoleManager
25from zope.traversing.browser import absoluteURL
26from zope.component import (
27    createObject,)
28
29from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
30from reportlab.pdfgen import canvas
31from reportlab.lib.units import cm
32from reportlab.lib.pagesizes import A4
33from reportlab.lib.styles import getSampleStyleSheet
34from reportlab.platypus import (Frame, Paragraph, Image,
35    Table, Spacer)
36from reportlab.platypus.tables import TableStyle
37
38from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
39from waeup.sirp.accesscodes.workflow import USED
40from waeup.sirp.browser import (
41    WAeUPPage, WAeUPEditFormPage, WAeUPAddFormPage, WAeUPDisplayFormPage)
42from waeup.sirp.browser.breadcrumbs import Breadcrumb
43from waeup.sirp.browser.layout import NullValidator
44from waeup.sirp.browser.pages import add_local_role, del_local_roles
45from waeup.sirp.browser.resources import datepicker, tabs, datatable
46from waeup.sirp.browser.viewlets import (
47    ManageActionButton, PrimaryNavTab,
48    AddActionButton, ActionButton, PlainActionButton,
49    )
50from waeup.sirp.image.browser.widget import (
51    ThumbnailWidget, EncodingImageFileWidget,
52    )
53from waeup.sirp.image.image import createWAeUPImageFile
54from waeup.sirp.interfaces import (
55    IWAeUPObject, ILocalRolesAssignable, IUserAccount,
56    )
57from waeup.sirp.permissions import get_users_with_local_roles
58from waeup.sirp.university.interfaces import ICertificate
59from waeup.sirp.widgets.datewidget import (
60    FriendlyDateWidget, FriendlyDateDisplayWidget)
61from waeup.sirp.widgets.restwidget import ReSTDisplayWidget
62from waeup.sirp.widgets.objectwidget import (
63    WAeUPObjectWidget, WAeUPObjectDisplayWidget)
64from waeup.sirp.students.interfaces import (
65    IStudentsContainer, IStudent, IStudentClearance,
66    IStudentPersonal, IStudentBase, IStudentStudyCourse,
67    IStudentPayments, IStudentAccommodation, IStudentNavigation,
68    IStudentBaseEdit, IStudentClearanceEdit,
69    )
70from waeup.sirp.students.student import Student
71from waeup.sirp.students.catalog import search
72from waeup.sirp.accesscodes import invalidate_accesscode, get_access_code
73from waeup.sirp.accesscodes.workflow import USED
74from waeup.sirp.students.workflow import CLEARANCE
75
76class StudentsTab(PrimaryNavTab):
77    """Students tab in primary navigation.
78    """
79
80    grok.context(IWAeUPObject)
81    grok.order(3)
82    grok.require('waeup.viewStudent')
83    grok.template('primarynavtab')
84
85    pnav = 4
86    tab_title = u'Students'
87
88    @property
89    def link_target(self):
90        return self.view.application_url('students')
91
92class StudentsBreadcrumb(Breadcrumb):
93    """A breadcrumb for the students container.
94    """
95    grok.context(IStudentsContainer)
96    title = u'Students'
97
98class SudyCourseBreadcrumb(Breadcrumb):
99    """A breadcrumb for the student study course.
100    """
101    grok.context(IStudentStudyCourse)
102    title = u'Study Course'
103
104class PaymentsBreadcrumb(Breadcrumb):
105    """A breadcrumb for the student payments folder.
106    """
107    grok.context(IStudentPayments)
108    title = u'Payments'
109
110class AccommodationBreadcrumb(Breadcrumb):
111    """A breadcrumb for the student accommodation folder.
112    """
113    grok.context(IStudentAccommodation)
114    title = u'Accommodation'
115
116class StudentsContainerPage(WAeUPPage):
117    """The standard view for student containers.
118    """
119    grok.context(IStudentsContainer)
120    grok.name('index')
121    grok.require('waeup.viewStudent')
122    grok.template('containerpage')
123    label = 'Student Section'
124    title = 'Students'
125    pnav = 4
126
127    def update(self, *args, **kw):
128        datatable.need()
129        form = self.request.form
130        self.hitlist = []
131        if 'searchterm' in form and form['searchterm']:
132            self.searchterm = form['searchterm']
133            self.searchtype = form['searchtype']
134        elif 'old_searchterm' in form:
135            self.searchterm = form['old_searchterm']
136            self.searchtype = form['old_searchtype']
137        else:
138            if 'search' in form:
139                self.flash('Empty search string.')
140            return
141        self.hitlist = search(query=self.searchterm,
142            searchtype=self.searchtype, view=self)
143        if not self.hitlist:
144            self.flash('No student found.')
145        return
146
147class SetPassword(WAeUPPage):
148    grok.context(IWAeUPObject)
149    grok.name('setpassword')
150    grok.require('waeup.Public')
151    title = ''
152    label = 'Set password for first-time login'
153    acprefix = 'PWD'
154    pnav = 0
155
156    def update(self, SUBMIT=None):
157        self.reg_number = self.request.form.get('form.reg_number', None)
158        # We must not use form.ac_series and form.ac_number in forms since these
159        # are interpreted as applicant credentials in the applicants package
160        self.acseries = self.request.form.get('form.acseries', None)
161        self.acnumber = self.request.form.get('form.acnumber', None)
162       
163        if SUBMIT is None:
164            return
165        hitlist = search(query=self.reg_number,
166            searchtype='reg_number', view=self)
167        if not hitlist:
168            self.flash('No student found.')
169            return
170        if len(hitlist) != 1:   # Cannot happen but anyway
171            self.flash('More than one student found.')
172            return
173        student = hitlist[0].context
174        self.student_id = student.student_id
175        student_pw = student.password
176        pin = '%s-%s-%s' % (self.acprefix,self.acseries,self.acnumber)
177        code = get_access_code(pin)
178        if not code:
179            self.flash('Access code is invalid.')
180            return
181        if student_pw and pin == student.adm_code:
182            self.flash('Password has already been set. Your Student Id is %s'
183                % self.student_id)
184            return
185        elif student_pw:
186            self.flash('Password has already been set.')
187            return
188        # Mark pin as used (this also fires a pin related transition)
189        # and set student password
190        if code.state == USED:
191            self.flash('Access code has already been used.')
192            return
193        else:
194            comment = u"AC invalidated for %s" % self.student_id
195            # Here we know that the ac is in state initialized so we do not
196            # expect an exception
197            #import pdb; pdb.set_trace()
198            invalidate_accesscode(pin,comment)
199            IUserAccount(student).setPassword(self.acnumber)
200        self.flash('Password has been set. Your Student Id is %s'
201            % self.student_id)
202        return
203
204class StudentsContainerManageActionButton(ManageActionButton):
205    grok.order(1)
206    grok.context(IStudentsContainer)
207    grok.view(StudentsContainerPage)
208    grok.require('waeup.manageStudents')
209    text = 'Manage student section'
210
211
212class StudentsContainerManagePage(WAeUPPage):
213    """The manage page for student containers.
214    """
215    grok.context(IStudentsContainer)
216    grok.name('manage')
217    grok.require('waeup.manageStudents')
218    grok.template('containermanagepage')
219    pnav = 4
220    title = 'Manage student section'
221
222    @property
223    def label(self):
224        return self.title
225
226    def update(self, *args, **kw):
227        datatable.need()
228        form = self.request.form
229        self.hitlist = []
230        if 'searchterm' in form and form['searchterm']:
231            self.searchterm = form['searchterm']
232            self.searchtype = form['searchtype']
233        elif 'old_searchterm' in form:
234            self.searchterm = form['old_searchterm']
235            self.searchtype = form['old_searchtype']
236        else:
237            if 'search' in form:
238                self.flash('Empty search string.')
239            return
240        if not 'entries' in form:
241            self.hitlist = search(query=self.searchterm,
242                searchtype=self.searchtype, view=self)
243            if not self.hitlist:
244                self.flash('No student found.')
245            return
246        entries = form['entries']
247        if isinstance(entries, basestring):
248            entries = [entries]
249        deleted = []
250        for entry in entries:
251            if 'remove' in form:
252                del self.context[entry]
253                deleted.append(entry)
254        self.hitlist = search(query=self.searchterm,
255            searchtype=self.searchtype, view=self)
256        if len(deleted):
257            self.flash('Successfully removed: %s' % ', '.join(deleted))
258        return
259
260class StudentsContainerAddActionButton(AddActionButton):
261    grok.order(1)
262    grok.context(IStudentsContainer)
263    grok.view(StudentsContainerManagePage)
264    grok.require('waeup.manageStudents')
265    text = 'Add student'
266    target = 'addstudent'
267
268class StudentAddFormPage(WAeUPAddFormPage):
269    """Add-form to add a student.
270    """
271    grok.context(IStudentsContainer)
272    grok.require('waeup.manageStudents')
273    grok.name('addstudent')
274    grok.template('studentaddpage')
275    form_fields = grok.AutoFields(IStudent)
276    title = 'Students'
277    label = 'Add student'
278    pnav = 4
279
280    @grok.action('Create student record')
281    def addStudent(self, **data):
282        student = createObject(u'waeup.Student')
283        self.applyData(student, **data)
284        self.context.addStudent(student)
285        self.flash('Student record created.')
286        self.redirect(self.url(self.context[student.student_id], 'index'))
287        return
288
289class StudentBaseDisplayFormPage(WAeUPDisplayFormPage):
290    """ Page to display student base data
291    """
292    grok.context(IStudent)
293    grok.name('index')
294    grok.require('waeup.viewStudent')
295    grok.template('basepage')
296    form_fields = grok.AutoFields(IStudentBase)  #.omit('password')
297    pnav = 4
298    title = 'Base Data'
299
300    @property
301    def label(self):
302        return '%s: Base Data' % self.context.name
303
304    @property
305    def hasPassword(self):
306        if self.context.password:
307            return 'set'
308        return 'unset'
309
310class StudentBaseManageActionButton(ManageActionButton):
311    grok.order(1)
312    grok.context(IStudent)
313    grok.view(StudentBaseDisplayFormPage)
314    grok.require('waeup.manageStudents')
315    text = 'Manage'
316    target = 'edit_base'
317
318class StudentBaseManageFormPage(WAeUPEditFormPage):
319    """ View to edit student base data
320    """
321    grok.context(IStudent)
322    grok.name('edit_base')
323    grok.require('waeup.manageStudents')
324    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
325    grok.template('basemanagepage')
326    label = 'Manage base data'
327    title = 'Base Data'
328    pnav = 4
329
330    def update(self):
331        datepicker.need() # Enable jQuery datepicker in date fields.
332        super(StudentBaseManageFormPage, self).update()
333        self.wf_info = IWorkflowInfo(self.context)
334        return
335
336    def getTransitions(self):
337        """Return a list of dicts of allowed transition ids and titles.
338
339        Each list entry provides keys ``name`` and ``title`` for
340        internal name and (human readable) title of a single
341        transition.
342        """
343        allowed_transitions = self.wf_info.getManualTransitions()
344        return [dict(name='', title='No transition')] +[
345            dict(name=x, title=y) for x, y in allowed_transitions]
346
347    @grok.action('Save')
348    def save(self, **data):
349        form = self.request.form
350        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
351        if form.has_key('password') and form['password']:
352            if form['password'] != form['control_password']:
353                self.flash('Passwords do not match.')
354                return
355            IUserAccount(self.context).setPassword(form['password'])
356            self.context.loggerInfo(ob_class, 'password changed')
357        changed_fields = self.applyData(self.context, **data)
358        changed_fields = changed_fields.values()
359        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
360        self.context._p_changed = True
361        if form.has_key('transition') and form['transition']:
362            transition_id = form['transition']
363            self.wf_info.fireTransition(transition_id)
364        self.flash('Form has been saved.')
365        if fields_string:
366            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
367        return
368
369class StudentClearanceDisplayFormPage(WAeUPDisplayFormPage):
370    """ Page to display student clearance data
371    """
372    grok.context(IStudent)
373    grok.name('view_clearance')
374    grok.require('waeup.viewStudent')
375    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
376    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
377    title = 'Clearance Data'
378    pnav = 4
379
380    @property
381    def label(self):
382        return '%s: Clearance Data' % self.context.name
383
384class StudentClearanceManageActionButton(ManageActionButton):
385    grok.order(1)
386    grok.context(IStudent)
387    grok.view(StudentClearanceDisplayFormPage)
388    grok.require('waeup.manageStudents')
389    text = 'Manage'
390    target = 'edit_clearance'
391
392class StudentClearanceManageFormPage(WAeUPEditFormPage):
393    """ Page to edit student clearance data
394    """
395    grok.context(IStudent)
396    grok.name('edit_clearance')
397    grok.require('waeup.manageStudents')
398    form_fields = grok.AutoFields(IStudentClearance)
399    label = 'Manage clearance data'
400    title = 'Clearance Data'
401    pnav = 4
402
403    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
404
405    def update(self):
406        datepicker.need() # Enable jQuery datepicker in date fields.
407        return super(StudentClearanceManageFormPage, self).update()
408
409    @grok.action('Save')
410    def save(self, **data):
411        changed_fields = self.applyData(self.context, **data)
412        changed_fields = changed_fields.values()
413        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
414        self.context._p_changed = True
415        form = self.request.form
416        self.flash('Form has been saved.')
417        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
418        if fields_string:
419            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
420        return
421
422class StudentPersonalDisplayFormPage(WAeUPDisplayFormPage):
423    """ Page to display student personal data
424    """
425    grok.context(IStudent)
426    grok.name('view_personal')
427    grok.require('waeup.viewStudent')
428    form_fields = grok.AutoFields(IStudentPersonal)
429    title = 'Personal Data'
430    pnav = 4
431
432    @property
433    def label(self):
434        return '%s: Personal Data' % self.context.name
435
436class StudentPersonalManageActionButton(ManageActionButton):
437    grok.order(1)
438    grok.context(IStudent)
439    grok.view(StudentPersonalDisplayFormPage)
440    grok.require('waeup.manageStudents')
441    text = 'Manage'
442    target = 'edit_personal'
443
444class StudentPersonalManageFormPage(WAeUPEditFormPage):
445    """ Page to edit student clearance data
446    """
447    grok.context(IStudent)
448    grok.name('edit_personal')
449    grok.require('waeup.viewStudent')
450    form_fields = grok.AutoFields(IStudentPersonal)
451    label = 'Manage personal data'
452    title = 'Personal Data'
453    pnav = 4
454
455class StudyCourseDisplayFormPage(WAeUPDisplayFormPage):
456    """ Page to display the student study course data
457    """
458    grok.context(IStudentStudyCourse)
459    grok.name('index')
460    grok.require('waeup.viewStudent')
461    form_fields = grok.AutoFields(IStudentStudyCourse)
462    #grok.template('studycoursepage')
463    title = 'Study Course'
464    pnav = 4
465
466    @property
467    def label(self):
468        return '%s: Study Course' % self.context.__parent__.name
469
470class StudyCourseManageActionButton(ManageActionButton):
471    grok.order(1)
472    grok.context(IStudentStudyCourse)
473    grok.view(StudyCourseDisplayFormPage)
474    grok.require('waeup.manageStudents')
475    text = 'Manage'
476    target = 'edit'
477
478class StudyCourseManageFormPage(WAeUPEditFormPage):
479    """ Page to edit the student study course data
480    """
481    grok.context(IStudentStudyCourse)
482    grok.name('edit')
483    grok.require('waeup.manageStudents')
484    form_fields = grok.AutoFields(IStudentStudyCourse)
485    title = 'Study Course'
486    label = 'Manage study course'
487    pnav = 4
488
489class PaymentsDisplayFormPage(WAeUPDisplayFormPage):
490    """ Page to display the student payments
491    """
492    grok.context(IStudentPayments)
493    grok.name('index')
494    grok.require('waeup.viewStudent')
495    form_fields = grok.AutoFields(IStudentPayments)
496    #grok.template('paymentspage')
497    title = 'Payments'
498    pnav = 4
499
500    @property
501    def label(self):
502        return '%s: Payments' % self.context.__parent__.name
503
504class AccommodationDisplayFormPage(WAeUPDisplayFormPage):
505    """ Page to display the student accommodation data
506    """
507    grok.context(IStudentAccommodation)
508    grok.name('index')
509    grok.require('waeup.viewStudent')
510    form_fields = grok.AutoFields(IStudentAccommodation)
511    #grok.template('accommodationpage')
512    title = 'Accommodation'
513    pnav = 4
514
515    @property
516    def label(self):
517        return '%s: Accommodation Data' % self.context.__parent__.name
518
519class StudentHistoryPage(WAeUPPage):
520    """ Page to display student clearance data
521    """
522    grok.context(IStudent)
523    grok.name('history')
524    grok.require('waeup.viewStudent')
525    grok.template('studenthistory')
526    title = 'History'
527    pnav = 4
528
529    @property
530    def label(self):
531        return '%s: History' % self.context.name
532
533# Pages for students only
534
535class StudentBaseEditActionButton(ManageActionButton):
536    grok.order(1)
537    grok.context(IStudent)
538    grok.view(StudentBaseDisplayFormPage)
539    grok.require('waeup.handleStudent')
540    text = 'Change password'
541    target = 'bedit'
542
543class StudentBaseEditFormPage(WAeUPEditFormPage):
544    """ View to edit student base data by student
545    """
546    grok.context(IStudent)
547    grok.name('bedit')
548    grok.require('waeup.handleStudent')
549    form_fields = grok.AutoFields(IStudentBaseEdit).omit(
550        'student_id', 'reg_number')
551    grok.template('baseeditpage')
552    label = 'Change password'
553    title = 'Base Data'
554    pnav = 4
555
556    def update(self):
557        datepicker.need() # Enable jQuery datepicker in date fields.
558        super(StudentBaseEditFormPage, self).update()
559        self.wf_info = IWorkflowInfo(self.context)
560        return
561
562    @grok.action('Save')
563    def save(self, **data):
564        form = self.request.form
565        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
566        if form.has_key('password') and form['password']:
567            if form['password'] != form['control_password']:
568                self.flash('Passwords do not match.')
569                return
570            IUserAccount(self.context).setPassword(form['password'])
571            self.context.loggerInfo(ob_class, 'password changed')
572        changed_fields = self.applyData(self.context, **data)
573        changed_fields = changed_fields.values()
574        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
575        self.context._p_changed = True
576        self.flash('Form has been saved.')
577        if fields_string:
578            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
579        return
580
581class StudentClearanceStartActionButton(ManageActionButton):
582    grok.order(1)
583    grok.context(IStudent)
584    grok.view(StudentClearanceDisplayFormPage)
585    grok.require('waeup.handleStudent')
586    icon = 'actionicon_start.png'
587    text = 'Start clearance'
588    target = 'start_clearance'
589
590    @property
591    def target_url(self):
592        if self.context.state != 'admitted':
593            return ''
594        return self.view.url(self.view.context, self.target)
595
596class StudentClearanceEditActionButton(ManageActionButton):
597    grok.order(1)
598    grok.context(IStudent)
599    grok.view(StudentClearanceDisplayFormPage)
600    grok.require('waeup.handleStudent')
601    text = 'Edit'
602    target = 'cedit'
603
604    @property
605    def target_url(self):
606        if self.context.clearance_locked:
607            return ''
608        return self.view.url(self.view.context, self.target)
609
610class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
611    """ View to edit student clearance data by student
612    """
613    grok.context(IStudent)
614    grok.name('cedit')
615    grok.require('waeup.handleStudent')
616    form_fields = grok.AutoFields(IStudentClearanceEdit).omit('clearance_locked')
617    #grok.template('clearanceeditpage')
618    label = 'Edit clearance data'
619    title = 'Clearance Data'
620    pnav = 4
621    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
622
623    def emitLockMessage(self):
624        self.flash('The requested form is locked (read-only).')
625        self.redirect(self.url(self.context))
626        return
627
628    def update(self):
629        if self.context.clearance_locked:
630            self.emitLockMessage()
631            return
632        datepicker.need()
633        return super(StudentClearanceEditFormPage, self).update()
634
635    @grok.action('Save')
636    def save(self, **data):
637        self.applyData(self.context, **data)
638        self.flash('Form has been saved.')
639        return
640
641    @grok.action('Save and request clearance')
642    def requestclearance(self, **data):
643        self.applyData(self.context, **data)
644        self.context._p_changed = True
645        #if self.dataNotComplete():
646        #    self.flash(self.dataNotComplete())
647        #    return
648        state = IWorkflowState(self.context).getState()
649        # This shouldn't happen, but the application officer
650        # might have forgotten to lock the form after changing the state
651        if state != CLEARANCE:
652            self.flash('This form cannot be submitted. Wrong state!')
653            return
654        IWorkflowInfo(self.context).fireTransition('request_clearance')
655        self.flash('Clearance has been requested.')
656        self.redirect(self.url(self.context))
657        return
658
659class StartClearance(WAeUPPage):
660    grok.context(IStudent)
661    grok.name('start_clearance')
662    grok.require('waeup.handleStudent')
663    grok.template('enterpin')
664    title = 'Start clearance'
665    label = 'Start clearance'
666    acprefix = 'CLR'
667    pnav = 4
668    buttonname = 'Start'
669   
670
671    def update(self, SUBMIT=None):
672        # We must not use form.ac_series and form.ac_number in forms since these
673        # are interpreted as applicant credentials in the applicants package
674        self.acseries = self.request.form.get('form.acseries', None)
675        self.acnumber = self.request.form.get('form.acnumber', None)
676
677        if SUBMIT is None:
678            return
679        pin = '%s-%s-%s' % (self.acprefix,self.acseries,self.acnumber)
680        code = get_access_code(pin)
681        if not code:
682            self.flash('Access code is invalid.')
683            return
684        # Mark pin as used (this also fires a pin related transition)
685        # and fire transition start_clearance
686        if code.state == USED:
687            self.flash('Access code has already been used.')
688            return
689        else:
690            comment = u"AC invalidated for %s" % self.context.student_id
691            # Here we know that the ac is in state initialized so we do not
692            # expect an exception
693            invalidate_accesscode(pin,comment)
694        IWorkflowInfo(self.context).fireTransition('start_clearance')
695        self.flash('Clearance process is started.')
696        self.redirect(self.url(self.context))
697        return
Note: See TracBrowser for help on using the repository browser.