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

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

Add save msave function which is called by save methods in manager pages.

  • 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))
716        return
Note: See TracBrowser for help on using the repository browser.