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

Last change on this file since 6761 was 6761, checked in by Henrik Bettermann, 14 years ago

Add save method with logging.

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