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

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

Fire transition start_clearance and unlock the clearance form when pressing the Start button.

Extend student workflow.

  • Property svn:keywords set to Id
File size: 22.8 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
74
75class StudentsTab(PrimaryNavTab):
76    """Students tab in primary navigation.
77    """
78
79    grok.context(IWAeUPObject)
80    grok.order(3)
81    grok.require('waeup.viewStudent')
82    grok.template('primarynavtab')
83
84    pnav = 4
85    tab_title = u'Students'
86
87    @property
88    def link_target(self):
89        return self.view.application_url('students')
90
91class StudentsBreadcrumb(Breadcrumb):
92    """A breadcrumb for the students container.
93    """
94    grok.context(IStudentsContainer)
95    title = u'Students'
96
97class SudyCourseBreadcrumb(Breadcrumb):
98    """A breadcrumb for the student study course.
99    """
100    grok.context(IStudentStudyCourse)
101    title = u'Study Course'
102
103class PaymentsBreadcrumb(Breadcrumb):
104    """A breadcrumb for the student payments folder.
105    """
106    grok.context(IStudentPayments)
107    title = u'Payments'
108
109class AccommodationBreadcrumb(Breadcrumb):
110    """A breadcrumb for the student accommodation folder.
111    """
112    grok.context(IStudentAccommodation)
113    title = u'Accommodation'
114
115class StudentsContainerPage(WAeUPPage):
116    """The standard view for student containers.
117    """
118    grok.context(IStudentsContainer)
119    grok.name('index')
120    grok.require('waeup.viewStudent')
121    grok.template('containerpage')
122    label = 'Student Section'
123    title = 'Students'
124    pnav = 4
125
126    def update(self, *args, **kw):
127        datatable.need()
128        form = self.request.form
129        self.hitlist = []
130        if 'searchterm' in form and form['searchterm']:
131            self.searchterm = form['searchterm']
132            self.searchtype = form['searchtype']
133        elif 'old_searchterm' in form:
134            self.searchterm = form['old_searchterm']
135            self.searchtype = form['old_searchtype']
136        else:
137            if 'search' in form:
138                self.flash('Empty search string.')
139            return
140        self.hitlist = search(query=self.searchterm,
141            searchtype=self.searchtype, view=self)
142        if not self.hitlist:
143            self.flash('No student found.')
144        return
145
146class SetPassword(WAeUPPage):
147    grok.context(IWAeUPObject)
148    grok.name('setpassword')
149    grok.require('waeup.Public')
150    title = ''
151    label = 'Set password for first-time login'
152    acprefix = 'PWD'
153    pnav = 0
154
155    def update(self, SUBMIT=None):
156        self.reg_number = self.request.form.get('form.reg_number', None)
157        # We must not use form.ac_series and form.ac_number in forms since these
158        # are interpreted as applicant credentials in the applicants package
159        self.acseries = self.request.form.get('form.acseries', None)
160        self.acnumber = self.request.form.get('form.acnumber', None)
161       
162        if SUBMIT is None:
163            return
164        hitlist = search(query=self.reg_number,
165            searchtype='reg_number', view=self)
166        if not hitlist:
167            self.flash('No student found.')
168            return
169        if len(hitlist) != 1:   # Cannot happen but anyway
170            self.flash('More than one student found.')
171            return
172        student = hitlist[0].context
173        self.student_id = student.student_id
174        student_pw = student.password
175        pin = '%s-%s-%s' % (self.acprefix,self.acseries,self.acnumber)
176        code = get_access_code(pin)
177        if not code:
178            self.flash('Access code is invalid.')
179            return
180        if student_pw and pin == student.adm_code:
181            self.flash('Password has already been set. Your Student Id is %s'
182                % self.student_id)
183            return
184        elif student_pw:
185            self.flash('Password has already been set.')
186            return
187        # Mark pin as used (this also fires a pin related transition)
188        # and set student password
189        if code.state == USED:
190            self.flash('Access code has already been used.')
191            return
192        else:
193            comment = u"AC invalidated for %s" % self.student_id
194            # Here we know that the ac is in state initialized so we do not
195            # expect an exception
196            #import pdb; pdb.set_trace()
197            invalidate_accesscode(pin,comment)
198            IUserAccount(student).setPassword(self.acnumber)
199        self.flash('Password has been set. Your Student Id is %s'
200            % self.student_id)
201        return
202
203class StudentsContainerManageActionButton(ManageActionButton):
204    grok.order(1)
205    grok.context(IStudentsContainer)
206    grok.view(StudentsContainerPage)
207    grok.require('waeup.manageStudents')
208    text = 'Manage student section'
209
210
211class StudentsContainerManagePage(WAeUPPage):
212    """The manage page for student containers.
213    """
214    grok.context(IStudentsContainer)
215    grok.name('manage')
216    grok.require('waeup.manageStudents')
217    grok.template('containermanagepage')
218    pnav = 4
219    title = 'Manage student section'
220
221    @property
222    def label(self):
223        return self.title
224
225    def update(self, *args, **kw):
226        datatable.need()
227        form = self.request.form
228        self.hitlist = []
229        if 'searchterm' in form and form['searchterm']:
230            self.searchterm = form['searchterm']
231            self.searchtype = form['searchtype']
232        elif 'old_searchterm' in form:
233            self.searchterm = form['old_searchterm']
234            self.searchtype = form['old_searchtype']
235        else:
236            if 'search' in form:
237                self.flash('Empty search string.')
238            return
239        if not 'entries' in form:
240            self.hitlist = search(query=self.searchterm,
241                searchtype=self.searchtype, view=self)
242            if not self.hitlist:
243                self.flash('No student found.')
244            return
245        entries = form['entries']
246        if isinstance(entries, basestring):
247            entries = [entries]
248        deleted = []
249        for entry in entries:
250            if 'remove' in form:
251                del self.context[entry]
252                deleted.append(entry)
253        self.hitlist = search(query=self.searchterm,
254            searchtype=self.searchtype, view=self)
255        if len(deleted):
256            self.flash('Successfully removed: %s' % ', '.join(deleted))
257        return
258
259class StudentsContainerAddActionButton(AddActionButton):
260    grok.order(1)
261    grok.context(IStudentsContainer)
262    grok.view(StudentsContainerManagePage)
263    grok.require('waeup.manageStudents')
264    text = 'Add student'
265    target = 'addstudent'
266
267class StudentAddFormPage(WAeUPAddFormPage):
268    """Add-form to add a student.
269    """
270    grok.context(IStudentsContainer)
271    grok.require('waeup.manageStudents')
272    grok.name('addstudent')
273    grok.template('studentaddpage')
274    form_fields = grok.AutoFields(IStudent)
275    title = 'Students'
276    label = 'Add student'
277    pnav = 4
278
279    @grok.action('Create student record')
280    def addStudent(self, **data):
281        student = createObject(u'waeup.Student')
282        self.applyData(student, **data)
283        self.context.addStudent(student)
284        self.flash('Student record created.')
285        self.redirect(self.url(self.context[student.student_id], 'index'))
286        return
287
288class StudentBaseDisplayFormPage(WAeUPDisplayFormPage):
289    """ Page to display student base data
290    """
291    grok.context(IStudent)
292    grok.name('index')
293    grok.require('waeup.viewStudent')
294    grok.template('basepage')
295    form_fields = grok.AutoFields(IStudentBase)  #.omit('password')
296    pnav = 4
297    title = 'Base Data'
298
299    @property
300    def label(self):
301        return '%s: Base Data' % self.context.name
302
303    @property
304    def hasPassword(self):
305        if self.context.password:
306            return 'set'
307        return 'unset'
308
309class StudentBaseManageActionButton(ManageActionButton):
310    grok.order(1)
311    grok.context(IStudent)
312    grok.view(StudentBaseDisplayFormPage)
313    grok.require('waeup.manageStudents')
314    text = 'Manage'
315    target = 'edit_base'
316
317class StudentBaseManageFormPage(WAeUPEditFormPage):
318    """ View to edit student base data
319    """
320    grok.context(IStudent)
321    grok.name('edit_base')
322    grok.require('waeup.manageStudents')
323    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
324    grok.template('basemanagepage')
325    label = 'Manage base data'
326    title = 'Base Data'
327    pnav = 4
328
329    def update(self):
330        datepicker.need() # Enable jQuery datepicker in date fields.
331        super(StudentBaseManageFormPage, self).update()
332        self.wf_info = IWorkflowInfo(self.context)
333        return
334
335    def getTransitions(self):
336        """Return a list of dicts of allowed transition ids and titles.
337
338        Each list entry provides keys ``name`` and ``title`` for
339        internal name and (human readable) title of a single
340        transition.
341        """
342        allowed_transitions = self.wf_info.getManualTransitions()
343        return [dict(name='', title='No transition')] +[
344            dict(name=x, title=y) for x, y in allowed_transitions]
345
346    @grok.action('Save')
347    def save(self, **data):
348        form = self.request.form
349        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
350        if form.has_key('password') and form['password']:
351            if form['password'] != form['control_password']:
352                self.flash('Passwords do not match.')
353                return
354            IUserAccount(self.context).setPassword(form['password'])
355            self.context.loggerInfo(ob_class, 'password changed')
356        changed_fields = self.applyData(self.context, **data)
357        changed_fields = changed_fields.values()
358        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
359        self.context._p_changed = True
360        if form.has_key('transition') and form['transition']:
361            transition_id = form['transition']
362            self.wf_info.fireTransition(transition_id)
363        self.flash('Form has been saved.')
364        if fields_string:
365            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
366        return
367
368class StudentClearanceDisplayFormPage(WAeUPDisplayFormPage):
369    """ Page to display student clearance data
370    """
371    grok.context(IStudent)
372    grok.name('view_clearance')
373    grok.require('waeup.viewStudent')
374    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
375    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
376    title = 'Clearance Data'
377    pnav = 4
378
379    @property
380    def label(self):
381        return '%s: Clearance Data' % self.context.name
382
383class StudentClearanceManageActionButton(ManageActionButton):
384    grok.order(1)
385    grok.context(IStudent)
386    grok.view(StudentClearanceDisplayFormPage)
387    grok.require('waeup.manageStudents')
388    text = 'Manage'
389    target = 'edit_clearance'
390
391class StudentClearanceManageFormPage(WAeUPEditFormPage):
392    """ Page to edit student clearance data
393    """
394    grok.context(IStudent)
395    grok.name('edit_clearance')
396    grok.require('waeup.manageStudents')
397    form_fields = grok.AutoFields(IStudentClearance)
398    label = 'Manage clearance data'
399    title = 'Clearance Data'
400    pnav = 4
401
402    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
403
404    def update(self):
405        datepicker.need() # Enable jQuery datepicker in date fields.
406        return super(StudentClearanceManageFormPage, self).update()
407
408    @grok.action('Save')
409    def save(self, **data):
410        changed_fields = self.applyData(self.context, **data)
411        changed_fields = changed_fields.values()
412        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
413        self.context._p_changed = True
414        form = self.request.form
415        self.flash('Form has been saved.')
416        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
417        if fields_string:
418            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
419        return
420
421class StudentPersonalDisplayFormPage(WAeUPDisplayFormPage):
422    """ Page to display student personal data
423    """
424    grok.context(IStudent)
425    grok.name('view_personal')
426    grok.require('waeup.viewStudent')
427    form_fields = grok.AutoFields(IStudentPersonal)
428    title = 'Personal Data'
429    pnav = 4
430
431    @property
432    def label(self):
433        return '%s: Personal Data' % self.context.name
434
435class StudentPersonalManageActionButton(ManageActionButton):
436    grok.order(1)
437    grok.context(IStudent)
438    grok.view(StudentPersonalDisplayFormPage)
439    grok.require('waeup.manageStudents')
440    text = 'Manage'
441    target = 'edit_personal'
442
443class StudentPersonalManageFormPage(WAeUPEditFormPage):
444    """ Page to edit student clearance data
445    """
446    grok.context(IStudent)
447    grok.name('edit_personal')
448    grok.require('waeup.viewStudent')
449    form_fields = grok.AutoFields(IStudentPersonal)
450    label = 'Manage personal data'
451    title = 'Personal Data'
452    pnav = 4
453
454class StudyCourseDisplayFormPage(WAeUPDisplayFormPage):
455    """ Page to display the student study course data
456    """
457    grok.context(IStudentStudyCourse)
458    grok.name('index')
459    grok.require('waeup.viewStudent')
460    form_fields = grok.AutoFields(IStudentStudyCourse)
461    #grok.template('studycoursepage')
462    title = 'Study Course'
463    pnav = 4
464
465    @property
466    def label(self):
467        return '%s: Study Course' % self.context.__parent__.name
468
469class StudyCourseManageActionButton(ManageActionButton):
470    grok.order(1)
471    grok.context(IStudentStudyCourse)
472    grok.view(StudyCourseDisplayFormPage)
473    grok.require('waeup.manageStudents')
474    text = 'Manage'
475    target = 'edit'
476
477class StudyCourseManageFormPage(WAeUPEditFormPage):
478    """ Page to edit the student study course data
479    """
480    grok.context(IStudentStudyCourse)
481    grok.name('edit')
482    grok.require('waeup.manageStudents')
483    form_fields = grok.AutoFields(IStudentStudyCourse)
484    title = 'Study Course'
485    label = 'Manage study course'
486    pnav = 4
487
488class PaymentsDisplayFormPage(WAeUPDisplayFormPage):
489    """ Page to display the student payments
490    """
491    grok.context(IStudentPayments)
492    grok.name('index')
493    grok.require('waeup.viewStudent')
494    form_fields = grok.AutoFields(IStudentPayments)
495    #grok.template('paymentspage')
496    title = 'Payments'
497    pnav = 4
498
499    @property
500    def label(self):
501        return '%s: Payments' % self.context.__parent__.name
502
503class AccommodationDisplayFormPage(WAeUPDisplayFormPage):
504    """ Page to display the student accommodation data
505    """
506    grok.context(IStudentAccommodation)
507    grok.name('index')
508    grok.require('waeup.viewStudent')
509    form_fields = grok.AutoFields(IStudentAccommodation)
510    #grok.template('accommodationpage')
511    title = 'Accommodation'
512    pnav = 4
513
514    @property
515    def label(self):
516        return '%s: Accommodation Data' % self.context.__parent__.name
517
518class StudentHistoryPage(WAeUPPage):
519    """ Page to display student clearance data
520    """
521    grok.context(IStudent)
522    grok.name('history')
523    grok.require('waeup.viewStudent')
524    grok.template('studenthistory')
525    title = 'History'
526    pnav = 4
527
528    @property
529    def label(self):
530        return '%s: History' % self.context.name
531
532# Pages for students only
533
534class StudentBaseEditActionButton(ManageActionButton):
535    grok.order(1)
536    grok.context(IStudent)
537    grok.view(StudentBaseDisplayFormPage)
538    grok.require('waeup.handleStudent')
539    text = 'Change password'
540    target = 'bedit'
541
542class StudentBaseEditFormPage(WAeUPEditFormPage):
543    """ View to edit student base data by student
544    """
545    grok.context(IStudent)
546    grok.name('bedit')
547    grok.require('waeup.handleStudent')
548    form_fields = grok.AutoFields(IStudentBaseEdit).omit(
549        'student_id', 'reg_number')
550    grok.template('baseeditpage')
551    label = 'Change password'
552    title = 'Base Data'
553    pnav = 4
554
555    def update(self):
556        datepicker.need() # Enable jQuery datepicker in date fields.
557        super(StudentBaseEditFormPage, self).update()
558        self.wf_info = IWorkflowInfo(self.context)
559        return
560
561    @grok.action('Save')
562    def save(self, **data):
563        form = self.request.form
564        ob_class = self.__implemented__.__name__.replace('waeup.sirp.','')
565        if form.has_key('password') and form['password']:
566            if form['password'] != form['control_password']:
567                self.flash('Passwords do not match.')
568                return
569            IUserAccount(self.context).setPassword(form['password'])
570            self.context.loggerInfo(ob_class, 'password changed')
571        changed_fields = self.applyData(self.context, **data)
572        changed_fields = changed_fields.values()
573        fields_string = '+'.join(' + '.join(str(i) for i in b) for b in changed_fields)
574        self.context._p_changed = True
575        self.flash('Form has been saved.')
576        if fields_string:
577            self.context.loggerInfo(ob_class, 'saved: % s' % fields_string)
578        return
579
580class StudentClearanceStartActionButton(ManageActionButton):
581    grok.order(1)
582    grok.context(IStudent)
583    grok.view(StudentClearanceDisplayFormPage)
584    grok.require('waeup.handleStudent')
585    icon = 'actionicon_start.png'
586    text = 'Start clearance'
587    target = 'start_clearance'
588
589    @property
590    def target_url(self):
591        if self.context.state != 'admitted':
592            return ''
593        return self.view.url(self.view.context, self.target)
594
595class StudentClearanceEditActionButton(ManageActionButton):
596    grok.order(1)
597    grok.context(IStudent)
598    grok.view(StudentClearanceDisplayFormPage)
599    grok.require('waeup.handleStudent')
600    text = 'Edit and submit'
601    target = 'cedit'
602
603    @property
604    def target_url(self):
605        if self.context.clearance_locked:
606            return ''
607        return self.view.url(self.view.context, self.target)
608
609class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
610    """ View to edit student clearance data by student
611    """
612    grok.context(IStudent)
613    grok.name('cedit')
614    grok.require('waeup.handleStudent')
615    form_fields = grok.AutoFields(IStudentClearanceEdit).omit('clearance_locked')
616    #grok.template('clearanceeditpage')
617    label = 'Edit clerance data'
618    title = 'Clearance Data'
619    pnav = 4
620    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
621
622    def emitLockMessage(self):
623        self.flash('The requested form is locked (read-only).')
624        self.redirect(self.url(self.context))
625        return
626
627    def update(self):
628        if self.context.clearance_locked:
629            self.emitLockMessage()
630            return
631        datepicker.need()
632        return super(StudentClearanceEditFormPage, self).update()
633
634class StartClearance(WAeUPPage):
635    grok.context(IStudent)
636    grok.name('start_clearance')
637    grok.require('waeup.handleStudent')
638    grok.template('enterpin')
639    title = 'Start clearance'
640    label = 'Start clearance'
641    acprefix = 'CLR'
642    pnav = 4
643    buttonname = 'Start'
644   
645
646    def update(self, SUBMIT=None):
647        # We must not use form.ac_series and form.ac_number in forms since these
648        # are interpreted as applicant credentials in the applicants package
649        self.acseries = self.request.form.get('form.acseries', None)
650        self.acnumber = self.request.form.get('form.acnumber', None)
651
652        if SUBMIT is None:
653            return
654        pin = '%s-%s-%s' % (self.acprefix,self.acseries,self.acnumber)
655        code = get_access_code(pin)
656        if not code:
657            self.flash('Access code is invalid.')
658            return
659        # Mark pin as used (this also fires a pin related transition)
660        # and fire transition start_clearance
661        if code.state == USED:
662            self.flash('Access code has already been used.')
663            return
664        else:
665            comment = u"AC invalidated for %s" % self.context.student_id
666            # Here we know that the ac is in state initialized so we do not
667            # expect an exception
668            invalidate_accesscode(pin,comment)
669        IWorkflowInfo(self.context).fireTransition('start_clearance')
670        self.context.application_date = datetime.now()
671        self.context.clearance_locked = False
672        self.flash('Clearance process is started.')
673        self.redirect(self.url(self.context))
674        return
Note: See TracBrowser for help on using the repository browser.