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

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

Add more tests.

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