source: main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py @ 9011

Last change on this file since 9011 was 9010, checked in by Henrik Bettermann, 12 years ago

Show history on clearance slip.

Render signatures.

Remove unused function.

  • Property svn:keywords set to Id
File size: 73.0 KB
Line 
1## $Id: browser.py 9010 2012-07-17 08:38:17Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import sys
21import grok
22from urllib import urlencode
23from datetime import datetime
24from copy import deepcopy
25from zope.event import notify
26from zope.i18n import translate
27from zope.catalog.interfaces import ICatalog
28from zope.component import queryUtility, getUtility, createObject
29from zope.schema.interfaces import ConstraintNotSatisfied
30from zope.formlib.textwidgets import BytesDisplayWidget
31from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
32from waeup.kofa.accesscodes import (
33    invalidate_accesscode, get_access_code)
34from waeup.kofa.accesscodes.workflow import USED
35from waeup.kofa.browser import (
36    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
37    ContactAdminForm, KofaForm, NullValidator)
38from waeup.kofa.browser.breadcrumbs import Breadcrumb
39from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
40from waeup.kofa.browser.layout import jsaction, action, UtilityView
41from waeup.kofa.browser.interfaces import ICaptchaManager
42from waeup.kofa.interfaces import (
43    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
44    IKofaUtils, IUniversity)
45from waeup.kofa.interfaces import MessageFactory as _
46from waeup.kofa.widgets.datewidget import (
47    FriendlyDateWidget, FriendlyDateDisplayWidget,
48    FriendlyDatetimeDisplayWidget)
49from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
50from waeup.kofa.students.interfaces import (
51    IStudentsContainer, IStudent,
52    IUGStudentClearance,IPGStudentClearance,
53    IStudentPersonal, IStudentBase, IStudentStudyCourse,
54    IStudentAccommodation, IStudentStudyLevel,
55    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
56    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentRequestPW
57    )
58from waeup.kofa.students.catalog import search
59from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
60    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
61from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
62from waeup.kofa.students.vocabularies import StudyLevelSource
63from waeup.kofa.browser.resources import toggleall
64from waeup.kofa.hostels.hostel import NOT_OCCUPIED
65from waeup.kofa.utils.helpers import get_current_principal, to_timezone
66from waeup.kofa.mandates.mandate import PasswordMandate
67
68grok.context(IKofaObject) # Make IKofaObject the default context
69
70# Save function used for save methods in pages
71def msave(view, **data):
72    changed_fields = view.applyData(view.context, **data)
73    # Turn list of lists into single list
74    if changed_fields:
75        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
76    # Inform catalog if certificate has changed
77    # (applyData does this only for the context)
78    if 'certificate' in changed_fields:
79        notify(grok.ObjectModifiedEvent(view.context.student))
80    fields_string = ' + '.join(changed_fields)
81    view.flash(_('Form has been saved.'))
82    if fields_string:
83        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
84    return
85
86def emit_lock_message(view):
87    """Flash a lock message.
88    """
89    view.flash(_('The requested form is locked (read-only).'))
90    view.redirect(view.url(view.context))
91    return
92
93def translated_values(view):
94    lang = view.request.cookies.get('kofa.language')
95    for value in view.context.values():
96        value_dict = dict([i for i in value.__dict__.items()])
97        value_dict['mandatory_bool'] = value.mandatory
98        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
99            target_language=lang)
100        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
101            target_language=lang)
102        value_dict['automatic'] = translate(str(value.automatic), 'zope',
103            target_language=lang)
104        yield value_dict
105
106class StudentsBreadcrumb(Breadcrumb):
107    """A breadcrumb for the students container.
108    """
109    grok.context(IStudentsContainer)
110    title = _('Students')
111
112    @property
113    def target(self):
114        user = get_current_principal()
115        if getattr(user, 'user_type', None) == 'student':
116            return None
117        return self.viewname
118
119class StudentBreadcrumb(Breadcrumb):
120    """A breadcrumb for the student container.
121    """
122    grok.context(IStudent)
123
124    def title(self):
125        return self.context.display_fullname
126
127class SudyCourseBreadcrumb(Breadcrumb):
128    """A breadcrumb for the student study course.
129    """
130    grok.context(IStudentStudyCourse)
131    title = _('Study Course')
132
133class PaymentsBreadcrumb(Breadcrumb):
134    """A breadcrumb for the student payments folder.
135    """
136    grok.context(IStudentPaymentsContainer)
137    title = _('Payments')
138
139class OnlinePaymentBreadcrumb(Breadcrumb):
140    """A breadcrumb for payments.
141    """
142    grok.context(IStudentOnlinePayment)
143
144    @property
145    def title(self):
146        return self.context.p_id
147
148class AccommodationBreadcrumb(Breadcrumb):
149    """A breadcrumb for the student accommodation folder.
150    """
151    grok.context(IStudentAccommodation)
152    title = _('Accommodation')
153
154class BedTicketBreadcrumb(Breadcrumb):
155    """A breadcrumb for bed tickets.
156    """
157    grok.context(IBedTicket)
158
159    @property
160    def title(self):
161        return _('Bed Ticket ${a}',
162            mapping = {'a':self.context.getSessionString()})
163
164class StudyLevelBreadcrumb(Breadcrumb):
165    """A breadcrumb for course lists.
166    """
167    grok.context(IStudentStudyLevel)
168
169    @property
170    def title(self):
171        return self.context.level_title
172
173class StudentsContainerPage(KofaPage):
174    """The standard view for student containers.
175    """
176    grok.context(IStudentsContainer)
177    grok.name('index')
178    grok.require('waeup.viewStudentsContainer')
179    grok.template('containerpage')
180    label = _('Student Section')
181    search_button = _('Search')
182    pnav = 4
183
184    def update(self, *args, **kw):
185        datatable.need()
186        form = self.request.form
187        self.hitlist = []
188        if 'searchterm' in form and form['searchterm']:
189            self.searchterm = form['searchterm']
190            self.searchtype = form['searchtype']
191        elif 'old_searchterm' in form:
192            self.searchterm = form['old_searchterm']
193            self.searchtype = form['old_searchtype']
194        else:
195            if 'search' in form:
196                self.flash(_('Empty search string'))
197            return
198        if self.searchtype == 'current_session':
199            try:
200                self.searchterm = int(self.searchterm)
201            except ValueError:
202                self.flash(_('Only year dates allowed (e.g. 2011).'))
203                return
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
210class StudentsContainerManagePage(KofaPage):
211    """The manage page for student containers.
212    """
213    grok.context(IStudentsContainer)
214    grok.name('manage')
215    grok.require('waeup.manageStudent')
216    grok.template('containermanagepage')
217    pnav = 4
218    label = _('Manage student section')
219    search_button = _('Search')
220    remove_button = _('Remove selected')
221
222    def update(self, *args, **kw):
223        datatable.need()
224        toggleall.need()
225        warning.need()
226        form = self.request.form
227        self.hitlist = []
228        if 'searchterm' in form and form['searchterm']:
229            self.searchterm = form['searchterm']
230            self.searchtype = form['searchtype']
231        elif 'old_searchterm' in form:
232            self.searchterm = form['old_searchterm']
233            self.searchtype = form['old_searchtype']
234        else:
235            if 'search' in form:
236                self.flash(_('Empty search string'))
237            return
238        if self.searchtype == 'current_session':
239            try:
240                self.searchterm = int(self.searchterm)
241            except ValueError:
242                self.flash('Only year dates allowed (e.g. 2011).')
243                return
244        if not 'entries' in form:
245            self.hitlist = search(query=self.searchterm,
246                searchtype=self.searchtype, view=self)
247            if not self.hitlist:
248                self.flash(_('No student found.'))
249            if 'remove' in form:
250                self.flash(_('No item selected.'))
251            return
252        entries = form['entries']
253        if isinstance(entries, basestring):
254            entries = [entries]
255        deleted = []
256        for entry in entries:
257            if 'remove' in form:
258                del self.context[entry]
259                deleted.append(entry)
260        self.hitlist = search(query=self.searchterm,
261            searchtype=self.searchtype, view=self)
262        if len(deleted):
263            self.flash(_('Successfully removed: ${a}',
264                mapping = {'a':', '.join(deleted)}))
265        return
266
267class StudentAddFormPage(KofaAddFormPage):
268    """Add-form to add a student.
269    """
270    grok.context(IStudentsContainer)
271    grok.require('waeup.manageStudent')
272    grok.name('addstudent')
273    form_fields = grok.AutoFields(IStudent).select(
274        'firstname', 'middlename', 'lastname', 'reg_number')
275    label = _('Add student')
276    pnav = 4
277
278    @action(_('Create student record'), style='primary')
279    def addStudent(self, **data):
280        student = createObject(u'waeup.Student')
281        self.applyData(student, **data)
282        self.context.addStudent(student)
283        self.flash(_('Student record created.'))
284        self.redirect(self.url(self.context[student.student_id], 'index'))
285        return
286
287class StudentBaseDisplayFormPage(KofaDisplayFormPage):
288    """ Page to display student base data
289    """
290    grok.context(IStudent)
291    grok.name('index')
292    grok.require('waeup.viewStudent')
293    grok.template('basepage')
294    form_fields = grok.AutoFields(IStudentBase).omit('password', 'suspended')
295    pnav = 4
296
297    @property
298    def label(self):
299        if self.context.suspended:
300            return _('${a}: Base Data (account suspended)',
301                mapping = {'a':self.context.display_fullname})
302        return  _('${a}: Base Data',
303            mapping = {'a':self.context.display_fullname})
304
305    @property
306    def hasPassword(self):
307        if self.context.password:
308            return _('set')
309        return _('unset')
310
311class ContactStudentForm(ContactAdminForm):
312    grok.context(IStudent)
313    grok.name('contactstudent')
314    grok.require('waeup.viewStudent')
315    pnav = 4
316    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
317
318    def update(self, subject=u''):
319        self.form_fields.get('subject').field.default = subject
320        self.subject = subject
321        return
322
323    def label(self):
324        return _(u'Send message to ${a}',
325            mapping = {'a':self.context.display_fullname})
326
327    @action('Send message now', style='primary')
328    def send(self, *args, **data):
329        try:
330            email = self.request.principal.email
331        except AttributeError:
332            email = self.config.email_admin
333        usertype = getattr(self.request.principal,
334                           'user_type', 'system').title()
335        kofa_utils = getUtility(IKofaUtils)
336        success = kofa_utils.sendContactForm(
337                self.request.principal.title,email,
338                self.context.display_fullname,self.context.email,
339                self.request.principal.id,usertype,
340                self.config.name,
341                data['body'],data['subject'])
342        if success:
343            self.flash(_('Your message has been sent.'))
344        else:
345            self.flash(_('An smtp server error occurred.'))
346        return
347
348class StudentBaseManageFormPage(KofaEditFormPage):
349    """ View to manage student base data
350    """
351    grok.context(IStudent)
352    grok.name('manage_base')
353    grok.require('waeup.manageStudent')
354    form_fields = grok.AutoFields(IStudentBase).omit('student_id', 'adm_code')
355    grok.template('basemanagepage')
356    label = _('Manage base data')
357    pnav = 4
358
359    def update(self):
360        datepicker.need() # Enable jQuery datepicker in date fields.
361        tabs.need()
362        self.tab1 = self.tab2 = ''
363        qs = self.request.get('QUERY_STRING', '')
364        if not qs:
365            qs = 'tab1'
366        setattr(self, qs, 'active')
367        super(StudentBaseManageFormPage, self).update()
368        self.wf_info = IWorkflowInfo(self.context)
369        return
370
371    def getTransitions(self):
372        """Return a list of dicts of allowed transition ids and titles.
373
374        Each list entry provides keys ``name`` and ``title`` for
375        internal name and (human readable) title of a single
376        transition.
377        """
378        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
379            if not t[0].startswith('pay')]
380        return [dict(name='', title=_('No transition'))] +[
381            dict(name=x, title=y) for x, y in allowed_transitions]
382
383    @action(_('Save'), style='primary')
384    def save(self, **data):
385        form = self.request.form
386        password = form.get('password', None)
387        password_ctl = form.get('control_password', None)
388        if password:
389            validator = getUtility(IPasswordValidator)
390            errors = validator.validate_password(password, password_ctl)
391            if errors:
392                self.flash( ' '.join(errors))
393                return
394        changed_fields = self.applyData(self.context, **data)
395        # Turn list of lists into single list
396        if changed_fields:
397            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
398        else:
399            changed_fields = []
400        if password:
401            # Now we know that the form has no errors and can set password ...
402            IUserAccount(self.context).setPassword(password)
403            changed_fields.append('password')
404        # ... and execute transition
405        if form.has_key('transition') and form['transition']:
406            transition_id = form['transition']
407            self.wf_info.fireTransition(transition_id)
408        fields_string = ' + '.join(changed_fields)
409        self.flash(_('Form has been saved.'))
410        if fields_string:
411            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
412        return
413
414class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
415    """ Page to display student clearance data
416    """
417    grok.context(IStudent)
418    grok.name('view_clearance')
419    grok.require('waeup.viewStudent')
420    pnav = 4
421
422    @property
423    def separators(self):
424        return getUtility(IStudentsUtils).SEPARATORS_DICT
425
426    @property
427    def form_fields(self):
428        if self.context.is_postgrad:
429            form_fields = grok.AutoFields(
430                IPGStudentClearance).omit('clearance_locked')
431        else:
432            form_fields = grok.AutoFields(
433                IUGStudentClearance).omit('clearance_locked')
434        return form_fields
435
436    @property
437    def label(self):
438        return _('${a}: Clearance Data',
439            mapping = {'a':self.context.display_fullname})
440
441class ExportPDFClearanceSlipPage(grok.View):
442    """Deliver a PDF slip of the context.
443    """
444    grok.context(IStudent)
445    grok.name('clearance.pdf')
446    grok.require('waeup.viewStudent')
447    prefix = 'form'
448
449    @property
450    def form_fields(self):
451        if self.context.is_postgrad:
452            form_fields = grok.AutoFields(
453                IPGStudentClearance).omit('clearance_locked')
454        else:
455            form_fields = grok.AutoFields(
456                IUGStudentClearance).omit('clearance_locked')
457        return form_fields
458
459    @property
460    def title(self):
461        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
462        return translate(_('Clearance Data'), 'waeup.kofa',
463            target_language=portal_language)
464
465    @property
466    def label(self):
467        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
468        return translate(_('Clearance Slip of '),
469            'waeup.kofa', target_language=portal_language) \
470            + ' %s' % self.context.display_fullname
471
472    def _signatures(self):
473        if self.context.state == CLEARED:
474            return (_('Student Signature'), _('Clearance Officer Signature'))
475        return
476
477    def render(self):
478        studentview = StudentBaseDisplayFormPage(self.context.student,
479            self.request)
480        students_utils = getUtility(IStudentsUtils)
481        return students_utils.renderPDF(
482            self, 'clearance.pdf',
483            self.context.student, studentview, signatures=self._signatures())
484
485class StudentClearanceManageFormPage(KofaEditFormPage):
486    """ Page to manage student clearance data
487    """
488    grok.context(IStudent)
489    grok.name('manage_clearance')
490    grok.require('waeup.manageStudent')
491    grok.template('clearanceeditpage')
492    label = _('Manage clearance data')
493    pnav = 4
494
495    @property
496    def separators(self):
497        return getUtility(IStudentsUtils).SEPARATORS_DICT
498
499    @property
500    def form_fields(self):
501        if self.context.is_postgrad:
502            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
503        else:
504            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
505        return form_fields
506
507    def update(self):
508        datepicker.need() # Enable jQuery datepicker in date fields.
509        tabs.need()
510        self.tab1 = self.tab2 = ''
511        qs = self.request.get('QUERY_STRING', '')
512        if not qs:
513            qs = 'tab1'
514        setattr(self, qs, 'active')
515        return super(StudentClearanceManageFormPage, self).update()
516
517    @action(_('Save'), style='primary')
518    def save(self, **data):
519        msave(self, **data)
520        return
521
522class StudentClearPage(UtilityView, grok.View):
523    """ Clear student by clearance officer
524    """
525    grok.context(IStudent)
526    grok.name('clear')
527    grok.require('waeup.clearStudent')
528
529    def update(self):
530        if self.context.state == REQUESTED:
531            IWorkflowInfo(self.context).fireTransition('clear')
532            self.flash(_('Student has been cleared.'))
533        else:
534            self.flash(_('Student is in wrong state.'))
535        self.redirect(self.url(self.context,'view_clearance'))
536        return
537
538    def render(self):
539        return
540
541class StudentRejectClearancePage(UtilityView, grok.View):
542    """ Reject clearance by clearance officers
543    """
544    grok.context(IStudent)
545    grok.name('reject_clearance')
546    grok.require('waeup.clearStudent')
547
548    def update(self):
549        if self.context.state == CLEARED:
550            IWorkflowInfo(self.context).fireTransition('reset4')
551            message = _('Clearance has been annulled.')
552            self.flash(message)
553        elif self.context.state == REQUESTED:
554            IWorkflowInfo(self.context).fireTransition('reset3')
555            message = _('Clearance request has been rejected.')
556            self.flash(message)
557        else:
558            self.flash(_('Student is in wrong state.'))
559            self.redirect(self.url(self.context,'view_clearance'))
560            return
561        args = {'subject':message}
562        self.redirect(self.url(self.context) +
563            '/contactstudent?%s' % urlencode(args))
564        return
565
566    def render(self):
567        return
568
569class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
570    """ Page to display student personal data
571    """
572    grok.context(IStudent)
573    grok.name('view_personal')
574    grok.require('waeup.viewStudent')
575    form_fields = grok.AutoFields(IStudentPersonal)
576    form_fields['perm_address'].custom_widget = BytesDisplayWidget
577    pnav = 4
578
579    @property
580    def label(self):
581        return _('${a}: Personal Data',
582            mapping = {'a':self.context.display_fullname})
583
584class StudentPersonalManageFormPage(KofaEditFormPage):
585    """ Page to manage personal data
586    """
587    grok.context(IStudent)
588    grok.name('manage_personal')
589    grok.require('waeup.manageStudent')
590    form_fields = grok.AutoFields(IStudentPersonal)
591    label = _('Manage personal data')
592    pnav = 4
593
594    @action(_('Save'), style='primary')
595    def save(self, **data):
596        msave(self, **data)
597        return
598
599class StudentPersonalEditFormPage(StudentPersonalManageFormPage):
600    """ Page to edit personal data
601    """
602    grok.name('edit_personal')
603    grok.require('waeup.handleStudent')
604    label = _('Edit personal data')
605    pnav = 4
606
607class StudyCourseDisplayFormPage(KofaDisplayFormPage):
608    """ Page to display the student study course data
609    """
610    grok.context(IStudentStudyCourse)
611    grok.name('index')
612    grok.require('waeup.viewStudent')
613    grok.template('studycoursepage')
614    pnav = 4
615
616    @property
617    def form_fields(self):
618        if self.context.is_postgrad:
619            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
620                'current_verdict', 'previous_verdict')
621        else:
622            form_fields = grok.AutoFields(IStudentStudyCourse)
623        return form_fields
624
625    @property
626    def label(self):
627        return _('${a}: Study Course',
628            mapping = {'a':self.context.__parent__.display_fullname})
629
630    @property
631    def current_mode(self):
632        if self.context.certificate is not None:
633            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
634            return studymodes_dict[self.context.certificate.study_mode]
635        return
636
637    @property
638    def department(self):
639        if self.context.certificate is not None:
640            return self.context.certificate.__parent__.__parent__
641        return
642
643    @property
644    def faculty(self):
645        if self.context.certificate is not None:
646            return self.context.certificate.__parent__.__parent__.__parent__
647        return
648
649class StudyCourseManageFormPage(KofaEditFormPage):
650    """ Page to edit the student study course data
651    """
652    grok.context(IStudentStudyCourse)
653    grok.name('manage')
654    grok.require('waeup.manageStudent')
655    grok.template('studycoursemanagepage')
656    label = _('Manage study course')
657    pnav = 4
658    taboneactions = [_('Save'),_('Cancel')]
659    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
660    tabthreeactions = [_('Add study level')]
661
662    @property
663    def form_fields(self):
664        if self.context.is_postgrad:
665            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
666                'current_verdict', 'previous_verdict')
667        else:
668            form_fields = grok.AutoFields(IStudentStudyCourse)
669        return form_fields
670
671    def update(self):
672        super(StudyCourseManageFormPage, self).update()
673        tabs.need()
674        self.tab1 = self.tab2 = ''
675        qs = self.request.get('QUERY_STRING', '')
676        if not qs:
677            qs = 'tab1'
678        setattr(self, qs, 'active')
679        warning.need()
680        datatable.need()
681        return
682
683    @action(_('Save'), style='primary')
684    def save(self, **data):
685        try:
686            msave(self, **data)
687        except ConstraintNotSatisfied:
688            # The selected level might not exist in certificate
689            self.flash(_('Current level not available for certificate.'))
690            return
691        notify(grok.ObjectModifiedEvent(self.context.__parent__))
692        return
693
694    @property
695    def level_dict(self):
696        studylevelsource = StudyLevelSource().factory
697        for code in studylevelsource.getValues(self.context):
698            title = studylevelsource.getTitle(self.context, code)
699            yield(dict(code=code, title=title))
700
701    @action(_('Add study level'))
702    def addStudyLevel(self, **data):
703        level_code = self.request.form.get('addlevel', None)
704        studylevel = createObject(u'waeup.StudentStudyLevel')
705        studylevel.level = int(level_code)
706        try:
707            self.context.addStudentStudyLevel(
708                self.context.certificate,studylevel)
709            self.flash(_('Study level has been added.'))
710        except KeyError:
711            self.flash(_('This level exists.'))
712        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
713        return
714
715    @jsaction(_('Remove selected levels'))
716    def delStudyLevels(self, **data):
717        form = self.request.form
718        if form.has_key('val_id'):
719            child_id = form['val_id']
720        else:
721            self.flash(_('No study level selected.'))
722            self.redirect(self.url(self.context, '@@manage')+'?tab2')
723            return
724        if not isinstance(child_id, list):
725            child_id = [child_id]
726        deleted = []
727        for id in child_id:
728            del self.context[id]
729            deleted.append(id)
730        if len(deleted):
731            self.flash(_('Successfully removed: ${a}',
732                mapping = {'a':', '.join(deleted)}))
733        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
734        return
735
736class StudyLevelDisplayFormPage(KofaDisplayFormPage):
737    """ Page to display student study levels
738    """
739    grok.context(IStudentStudyLevel)
740    grok.name('index')
741    grok.require('waeup.viewStudent')
742    form_fields = grok.AutoFields(IStudentStudyLevel)
743    grok.template('studylevelpage')
744    pnav = 4
745
746    def update(self):
747        super(StudyLevelDisplayFormPage, self).update()
748        datatable.need()
749        return
750
751    @property
752    def translated_values(self):
753        return translated_values(self)
754
755    @property
756    def label(self):
757        # Here we know that the cookie has been set
758        lang = self.request.cookies.get('kofa.language')
759        level_title = translate(self.context.level_title, 'waeup.kofa',
760            target_language=lang)
761        return _('${a}: Study Level ${b}', mapping = {
762            'a':self.context.student.display_fullname,
763            'b':level_title})
764
765    @property
766    def total_credits(self):
767        total_credits = 0
768        for key, val in self.context.items():
769            total_credits += val.credits
770        return total_credits
771
772class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
773    """Deliver a PDF slip of the context.
774    """
775    grok.context(IStudentStudyLevel)
776    grok.name('course_registration.pdf')
777    grok.require('waeup.viewStudent')
778    form_fields = grok.AutoFields(IStudentStudyLevel)
779    prefix = 'form'
780
781    @property
782    def title(self):
783        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
784        return translate(_('Level Data'), 'waeup.kofa',
785            target_language=portal_language)
786
787    @property
788    def content_title(self):
789        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
790        return translate(_('Course List'), 'waeup.kofa',
791            target_language=portal_language)
792
793    @property
794    def label(self):
795        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
796        lang = self.request.cookies.get('kofa.language', portal_language)
797        level_title = translate(self.context.level_title, 'waeup.kofa',
798            target_language=lang)
799        return translate(_('Course Registration Slip'),
800            'waeup.kofa', target_language=portal_language) \
801            + ' %s' % level_title
802
803    def render(self):
804        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
805        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
806        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
807        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
808        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
809        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
810        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
811        Mand = translate(_('Mand.'), 'waeup.kofa', target_language=portal_language)
812        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
813        studentview = StudentBaseDisplayFormPage(self.context.student,
814            self.request)
815        students_utils = getUtility(IStudentsUtils)
816        tabledata = sorted(self.context.values(),
817            key=lambda value: str(value.semester) + value.code)
818        return students_utils.renderPDF(
819            self, 'course_registration.pdf',
820            self.context.student, studentview,
821            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
822                         (Title,'title', 5),
823                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
824                         (Cred, 'credits', 1.5),
825                         (Mand, 'mandatory', 1.5),
826                         (Score, 'score', 1.5),
827                         #('Auto', 'automatic', 1.5)
828                         ],
829            tabledata=tabledata)
830
831class StudyLevelManageFormPage(KofaEditFormPage):
832    """ Page to edit the student study level data
833    """
834    grok.context(IStudentStudyLevel)
835    grok.name('manage')
836    grok.require('waeup.manageStudent')
837    grok.template('studylevelmanagepage')
838    form_fields = grok.AutoFields(IStudentStudyLevel)
839    pnav = 4
840    taboneactions = [_('Save'),_('Cancel')]
841    tabtwoactions = [_('Add course ticket'),
842        _('Remove selected tickets'),_('Cancel')]
843
844    def update(self):
845        super(StudyLevelManageFormPage, self).update()
846        tabs.need()
847        self.tab1 = self.tab2 = ''
848        qs = self.request.get('QUERY_STRING', '')
849        if not qs:
850            qs = 'tab1'
851        setattr(self, qs, 'active')
852        warning.need()
853        datatable.need()
854        return
855
856    @property
857    def translated_values(self):
858        return translated_values(self)
859
860    @property
861    def label(self):
862        # Here we know that the cookie has been set
863        lang = self.request.cookies.get('kofa.language')
864        level_title = translate(self.context.level_title, 'waeup.kofa',
865            target_language=lang)
866        return _('Manage study level ${a}',
867            mapping = {'a':level_title})
868
869    @action(_('Save'), style='primary')
870    def save(self, **data):
871        msave(self, **data)
872        return
873
874    @action(_('Add course ticket'))
875    def addCourseTicket(self, **data):
876        self.redirect(self.url(self.context, '@@add'))
877
878    @jsaction(_('Remove selected tickets'))
879    def delCourseTicket(self, **data):
880        form = self.request.form
881        if form.has_key('val_id'):
882            child_id = form['val_id']
883        else:
884            self.flash(_('No ticket selected.'))
885            self.redirect(self.url(self.context, '@@manage')+'?tab2')
886            return
887        if not isinstance(child_id, list):
888            child_id = [child_id]
889        deleted = []
890        for id in child_id:
891            del self.context[id]
892            deleted.append(id)
893        if len(deleted):
894            self.flash(_('Successfully removed: ${a}',
895                mapping = {'a':', '.join(deleted)}))
896        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
897        return
898
899class ValidateCoursesPage(UtilityView, grok.View):
900    """ Validate course list by course adviser
901    """
902    grok.context(IStudentStudyLevel)
903    grok.name('validate_courses')
904    grok.require('waeup.validateStudent')
905
906    def update(self):
907        if str(self.context.__parent__.current_level) != self.context.__name__:
908            self.flash(_('This level does not correspond current level.'))
909        elif self.context.student.state == REGISTERED:
910            IWorkflowInfo(self.context.student).fireTransition(
911                'validate_courses')
912            self.flash(_('Course list has been validated.'))
913        else:
914            self.flash(_('Student is in the wrong state.'))
915        self.redirect(self.url(self.context))
916        return
917
918    def render(self):
919        return
920
921class RejectCoursesPage(UtilityView, grok.View):
922    """ Reject course list by course adviser
923    """
924    grok.context(IStudentStudyLevel)
925    grok.name('reject_courses')
926    grok.require('waeup.validateStudent')
927
928    def update(self):
929        if str(self.context.__parent__.current_level) != self.context.__name__:
930            self.flash(_('This level does not correspond current level.'))
931            self.redirect(self.url(self.context))
932            return
933        elif self.context.student.state == VALIDATED:
934            IWorkflowInfo(self.context.student).fireTransition('reset8')
935            message = _('Course list request has been annulled.')
936            self.flash(message)
937        elif self.context.student.state == REGISTERED:
938            IWorkflowInfo(self.context.student).fireTransition('reset7')
939            message = _('Course list request has been rejected:')
940            self.flash(message)
941        else:
942            self.flash(_('Student is in the wrong state.'))
943            self.redirect(self.url(self.context))
944            return
945        args = {'subject':message}
946        self.redirect(self.url(self.context.student) +
947            '/contactstudent?%s' % urlencode(args))
948        return
949
950    def render(self):
951        return
952
953class CourseTicketAddFormPage(KofaAddFormPage):
954    """Add a course ticket.
955    """
956    grok.context(IStudentStudyLevel)
957    grok.name('add')
958    grok.require('waeup.manageStudent')
959    label = _('Add course ticket')
960    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
961        'score', 'automatic', 'carry_over')
962    pnav = 4
963
964    @action(_('Add course ticket'))
965    def addCourseTicket(self, **data):
966        ticket = createObject(u'waeup.CourseTicket')
967        course = data['course']
968        ticket.automatic = False
969        ticket.carry_over = False
970        try:
971            self.context.addCourseTicket(ticket, course)
972        except KeyError:
973            self.flash(_('The ticket exists.'))
974            return
975        self.flash(_('Successfully added ${a}.',
976            mapping = {'a':ticket.code}))
977        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
978        return
979
980    @action(_('Cancel'), validator=NullValidator)
981    def cancel(self, **data):
982        self.redirect(self.url(self.context))
983
984class CourseTicketDisplayFormPage(KofaDisplayFormPage):
985    """ Page to display course tickets
986    """
987    grok.context(ICourseTicket)
988    grok.name('index')
989    grok.require('waeup.viewStudent')
990    form_fields = grok.AutoFields(ICourseTicket)
991    grok.template('courseticketpage')
992    pnav = 4
993
994    @property
995    def label(self):
996        return _('${a}: Course Ticket ${b}', mapping = {
997            'a':self.context.student.display_fullname,
998            'b':self.context.code})
999
1000class CourseTicketManageFormPage(KofaEditFormPage):
1001    """ Page to manage course tickets
1002    """
1003    grok.context(ICourseTicket)
1004    grok.name('manage')
1005    grok.require('waeup.manageStudent')
1006    form_fields = grok.AutoFields(ICourseTicket)
1007    grok.template('courseticketmanagepage')
1008    pnav = 4
1009
1010    @property
1011    def label(self):
1012        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1013
1014    @action('Save', style='primary')
1015    def save(self, **data):
1016        msave(self, **data)
1017        return
1018
1019class PaymentsManageFormPage(KofaEditFormPage):
1020    """ Page to manage the student payments
1021
1022    This manage form page is for both students and students officers.
1023    """
1024    grok.context(IStudentPaymentsContainer)
1025    grok.name('index')
1026    grok.require('waeup.payStudent')
1027    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1028    grok.template('paymentsmanagepage')
1029    pnav = 4
1030
1031    def unremovable(self, ticket):
1032        usertype = getattr(self.request.principal, 'user_type', None)
1033        if not usertype:
1034            return False
1035        return (self.request.principal.user_type == 'student' and ticket.r_code)
1036
1037    @property
1038    def label(self):
1039        return _('${a}: Payments',
1040            mapping = {'a':self.context.__parent__.display_fullname})
1041
1042    def update(self):
1043        super(PaymentsManageFormPage, self).update()
1044        datatable.need()
1045        warning.need()
1046        return
1047
1048    @jsaction(_('Remove selected tickets'))
1049    def delPaymentTicket(self, **data):
1050        form = self.request.form
1051        if form.has_key('val_id'):
1052            child_id = form['val_id']
1053        else:
1054            self.flash(_('No payment selected.'))
1055            self.redirect(self.url(self.context))
1056            return
1057        if not isinstance(child_id, list):
1058            child_id = [child_id]
1059        deleted = []
1060        for id in child_id:
1061            # Students are not allowed to remove used payment tickets
1062            if not self.unremovable(self.context[id]):
1063                del self.context[id]
1064                deleted.append(id)
1065        if len(deleted):
1066            self.flash(_('Successfully removed: ${a}',
1067                mapping = {'a': ', '.join(deleted)}))
1068            self.context.writeLogMessage(
1069                self,'removed: %s' % ', '.join(deleted))
1070        self.redirect(self.url(self.context))
1071        return
1072
1073    @action(_('Add online payment ticket'))
1074    def addPaymentTicket(self, **data):
1075        self.redirect(self.url(self.context, '@@addop'))
1076
1077class OnlinePaymentAddFormPage(KofaAddFormPage):
1078    """ Page to add an online payment ticket
1079    """
1080    grok.context(IStudentPaymentsContainer)
1081    grok.name('addop')
1082    grok.require('waeup.payStudent')
1083    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1084        'p_category')
1085    label = _('Add online payment')
1086    pnav = 4
1087
1088    @action(_('Create ticket'), style='primary')
1089    def createTicket(self, **data):
1090        p_category = data['p_category']
1091        student = self.context.__parent__
1092        if p_category == 'bed_allocation' and student[
1093            'studycourse'].current_session != grok.getSite()[
1094            'hostels'].accommodation_session:
1095                self.flash(
1096                    _('Your current session does not match ' + \
1097                    'accommodation session.'))
1098                self.redirect(self.url(self.context))
1099                return
1100        students_utils = getUtility(IStudentsUtils)
1101        error, payment = students_utils.setPaymentDetails(p_category, student)
1102        if error is not None:
1103            self.flash(error)
1104            self.redirect(self.url(self.context))
1105            return
1106        self.context[payment.p_id] = payment
1107        self.flash(_('Payment ticket created.'))
1108        self.redirect(self.url(self.context))
1109        return
1110
1111class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1112    """ Page to view an online payment ticket
1113    """
1114    grok.context(IStudentOnlinePayment)
1115    grok.name('index')
1116    grok.require('waeup.viewStudent')
1117    form_fields = grok.AutoFields(IStudentOnlinePayment)
1118    form_fields[
1119        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1120    form_fields[
1121        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1122    pnav = 4
1123
1124    @property
1125    def label(self):
1126        return _('${a}: Online Payment Ticket ${b}', mapping = {
1127            'a':self.context.student.display_fullname,
1128            'b':self.context.p_id})
1129
1130class OnlinePaymentApprovePage(UtilityView, grok.View):
1131    """ Callback view
1132    """
1133    grok.context(IStudentOnlinePayment)
1134    grok.name('approve')
1135    grok.require('waeup.managePortal')
1136
1137    def update(self):
1138        success, msg, log = self.context.approveStudentPayment()
1139        if log is not None:
1140            self.context.writeLogMessage(self,log)
1141        self.flash(msg)
1142        return
1143
1144    def render(self):
1145        self.redirect(self.url(self.context, '@@index'))
1146        return
1147
1148class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1149    """ Approval view for students.
1150
1151    This view is used for browser tests only and
1152    must be neutralized in custom pages!
1153    """
1154
1155    grok.name('fake_approve')
1156    grok.require('waeup.payStudent')
1157
1158class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1159    """Deliver a PDF slip of the context.
1160    """
1161    grok.context(IStudentOnlinePayment)
1162    grok.name('payment_slip.pdf')
1163    grok.require('waeup.viewStudent')
1164    form_fields = grok.AutoFields(IStudentOnlinePayment)
1165    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1166    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1167    prefix = 'form'
1168    note = None
1169
1170    @property
1171    def title(self):
1172        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1173        return translate(_('Payment Data'), 'waeup.kofa',
1174            target_language=portal_language)
1175
1176    @property
1177    def label(self):
1178        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1179        return translate(_('Online Payment Slip'),
1180            'waeup.kofa', target_language=portal_language) \
1181            + ' %s' % self.context.p_id
1182
1183    def render(self):
1184        #if self.context.p_state != 'paid':
1185        #    self.flash('Ticket not yet paid.')
1186        #    self.redirect(self.url(self.context))
1187        #    return
1188        studentview = StudentBaseDisplayFormPage(self.context.student,
1189            self.request)
1190        students_utils = getUtility(IStudentsUtils)
1191        return students_utils.renderPDF(self, 'payment_slip.pdf',
1192            self.context.student, studentview, note=self.note)
1193
1194
1195class AccommodationManageFormPage(KofaEditFormPage):
1196    """ Page to manage bed tickets.
1197
1198    This manage form page is for both students and students officers.
1199    """
1200    grok.context(IStudentAccommodation)
1201    grok.name('index')
1202    grok.require('waeup.handleAccommodation')
1203    form_fields = grok.AutoFields(IStudentAccommodation)
1204    grok.template('accommodationmanagepage')
1205    pnav = 4
1206    officers_only_actions = [_('Remove selected')]
1207
1208    @property
1209    def label(self):
1210        return _('${a}: Accommodation',
1211            mapping = {'a':self.context.__parent__.display_fullname})
1212
1213    def update(self):
1214        super(AccommodationManageFormPage, self).update()
1215        datatable.need()
1216        warning.need()
1217        return
1218
1219    @jsaction(_('Remove selected'))
1220    def delBedTickets(self, **data):
1221        if getattr(self.request.principal, 'user_type', None) == 'student':
1222            self.flash(_('You are not allowed to remove bed tickets.'))
1223            self.redirect(self.url(self.context))
1224            return
1225        form = self.request.form
1226        if form.has_key('val_id'):
1227            child_id = form['val_id']
1228        else:
1229            self.flash(_('No bed ticket selected.'))
1230            self.redirect(self.url(self.context))
1231            return
1232        if not isinstance(child_id, list):
1233            child_id = [child_id]
1234        deleted = []
1235        for id in child_id:
1236            del self.context[id]
1237            deleted.append(id)
1238        if len(deleted):
1239            self.flash(_('Successfully removed: ${a}',
1240                mapping = {'a':', '.join(deleted)}))
1241            self.context.writeLogMessage(
1242                self,'removed: % s' % ', '.join(deleted))
1243        self.redirect(self.url(self.context))
1244        return
1245
1246    @property
1247    def selected_actions(self):
1248        if getattr(self.request.principal, 'user_type', None) == 'student':
1249            return [action for action in self.actions
1250                    if not action.label in self.officers_only_actions]
1251        return self.actions
1252
1253class BedTicketAddPage(KofaPage):
1254    """ Page to add an online payment ticket
1255    """
1256    grok.context(IStudentAccommodation)
1257    grok.name('add')
1258    grok.require('waeup.handleAccommodation')
1259    grok.template('enterpin')
1260    ac_prefix = 'HOS'
1261    label = _('Add bed ticket')
1262    pnav = 4
1263    buttonname = _('Create bed ticket')
1264    notice = ''
1265
1266    def update(self, SUBMIT=None):
1267        student = self.context.student
1268        students_utils = getUtility(IStudentsUtils)
1269        acc_details  = students_utils.getAccommodationDetails(student)
1270        if acc_details.get('expired', False):
1271            startdate = acc_details.get('startdate')
1272            enddate = acc_details.get('enddate')
1273            if startdate and enddate:
1274                tz = getUtility(IKofaUtils).tzinfo
1275                startdate = to_timezone(
1276                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1277                enddate = to_timezone(
1278                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1279                self.flash(_("Outside booking period: ${a} - ${b}",
1280                    mapping = {'a': startdate, 'b': enddate}))
1281            else:
1282                self.flash(_("Outside booking period."))
1283            self.redirect(self.url(self.context))
1284            return
1285        if not acc_details:
1286            self.flash(_("Your data are incomplete."))
1287            self.redirect(self.url(self.context))
1288            return
1289        if not student.state in acc_details['allowed_states']:
1290            self.flash(_("You are in the wrong registration state."))
1291            self.redirect(self.url(self.context))
1292            return
1293        if student['studycourse'].current_session != acc_details[
1294            'booking_session']:
1295            self.flash(
1296                _('Your current session does not match accommodation session.'))
1297            self.redirect(self.url(self.context))
1298            return
1299        if str(acc_details['booking_session']) in self.context.keys():
1300            self.flash(
1301                _('You already booked a bed space in current ' \
1302                    + 'accommodation session.'))
1303            self.redirect(self.url(self.context))
1304            return
1305        self.ac_series = self.request.form.get('ac_series', None)
1306        self.ac_number = self.request.form.get('ac_number', None)
1307        if SUBMIT is None:
1308            return
1309        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1310        code = get_access_code(pin)
1311        if not code:
1312            self.flash(_('Activation code is invalid.'))
1313            return
1314        # Search and book bed
1315        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1316        entries = cat.searchResults(
1317            owner=(student.student_id,student.student_id))
1318        if len(entries):
1319            # If bed space has bee manually allocated use this bed
1320            bed = [entry for entry in entries][0]
1321        else:
1322            # else search for other available beds
1323            entries = cat.searchResults(
1324                bed_type=(acc_details['bt'],acc_details['bt']))
1325            available_beds = [
1326                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1327            if available_beds:
1328                students_utils = getUtility(IStudentsUtils)
1329                bed = students_utils.selectBed(available_beds)
1330                bed.bookBed(student.student_id)
1331            else:
1332                self.flash(_('There is no free bed in your category ${a}.',
1333                    mapping = {'a':acc_details['bt']}))
1334                return
1335        # Mark pin as used (this also fires a pin related transition)
1336        if code.state == USED:
1337            self.flash(_('Activation code has already been used.'))
1338            return
1339        else:
1340            comment = _(u'invalidated')
1341            # Here we know that the ac is in state initialized so we do not
1342            # expect an exception, but the owner might be different
1343            if not invalidate_accesscode(
1344                pin,comment,self.context.student.student_id):
1345                self.flash(_('You are not the owner of this access code.'))
1346                return
1347        # Create bed ticket
1348        bedticket = createObject(u'waeup.BedTicket')
1349        bedticket.booking_code = pin
1350        bedticket.booking_session = acc_details['booking_session']
1351        bedticket.bed_type = acc_details['bt']
1352        bedticket.bed = bed
1353        hall_title = bed.__parent__.hostel_name
1354        coordinates = bed.getBedCoordinates()[1:]
1355        block, room_nr, bed_nr = coordinates
1356        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1357            'a':hall_title, 'b':block,
1358            'c':room_nr, 'd':bed_nr,
1359            'e':bed.bed_type})
1360        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1361        bedticket.bed_coordinates = translate(
1362            bc, 'waeup.kofa',target_language=portal_language)
1363        key = str(acc_details['booking_session'])
1364        self.context[key] = bedticket
1365        self.flash(_('Bed ticket created and bed booked: ${a}',
1366            mapping = {'a':bedticket.bed_coordinates}))
1367        self.redirect(self.url(self.context))
1368        return
1369
1370class BedTicketDisplayFormPage(KofaDisplayFormPage):
1371    """ Page to display bed tickets
1372    """
1373    grok.context(IBedTicket)
1374    grok.name('index')
1375    grok.require('waeup.handleAccommodation')
1376    form_fields = grok.AutoFields(IBedTicket)
1377    pnav = 4
1378
1379    @property
1380    def label(self):
1381        return _('Bed Ticket for Session ${a}',
1382            mapping = {'a':self.context.getSessionString()})
1383
1384class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1385    """Deliver a PDF slip of the context.
1386    """
1387    grok.context(IBedTicket)
1388    grok.name('bed_allocation.pdf')
1389    grok.require('waeup.handleAccommodation')
1390    form_fields = grok.AutoFields(IBedTicket)
1391    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1392    prefix = 'form'
1393
1394    @property
1395    def title(self):
1396        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1397        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1398            target_language=portal_language)
1399
1400    @property
1401    def label(self):
1402        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1403        return translate(_('Bed Allocation: '),
1404            'waeup.kofa', target_language=portal_language) \
1405            + ' %s' % self.context.bed_coordinates
1406
1407    def render(self):
1408        studentview = StudentBaseDisplayFormPage(self.context.student,
1409            self.request)
1410        students_utils = getUtility(IStudentsUtils)
1411        return students_utils.renderPDF(
1412            self, 'bed_allocation.pdf',
1413            self.context.student, studentview)
1414
1415class BedTicketRelocationPage(UtilityView, grok.View):
1416    """ Callback view
1417    """
1418    grok.context(IBedTicket)
1419    grok.name('relocate')
1420    grok.require('waeup.manageHostels')
1421
1422    # Relocate student if student parameters have changed or the bed_type
1423    # of the bed has changed
1424    def update(self):
1425        student = self.context.student
1426        students_utils = getUtility(IStudentsUtils)
1427        acc_details  = students_utils.getAccommodationDetails(student)
1428        if self.context.bed != None and \
1429              'reserved' in self.context.bed.bed_type:
1430            self.flash(_("Students in reserved beds can't be relocated."))
1431            self.redirect(self.url(self.context))
1432            return
1433        if acc_details['bt'] == self.context.bed_type and \
1434                self.context.bed != None and \
1435                self.context.bed.bed_type == self.context.bed_type:
1436            self.flash(_("Student can't be relocated."))
1437            self.redirect(self.url(self.context))
1438            return
1439        # Search a bed
1440        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1441        entries = cat.searchResults(
1442            owner=(student.student_id,student.student_id))
1443        if len(entries) and self.context.bed == None:
1444            # If booking has been cancelled but other bed space has been
1445            # manually allocated after cancellation use this bed
1446            new_bed = [entry for entry in entries][0]
1447        else:
1448            # Search for other available beds
1449            entries = cat.searchResults(
1450                bed_type=(acc_details['bt'],acc_details['bt']))
1451            available_beds = [
1452                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1453            if available_beds:
1454                students_utils = getUtility(IStudentsUtils)
1455                new_bed = students_utils.selectBed(available_beds)
1456                new_bed.bookBed(student.student_id)
1457            else:
1458                self.flash(_('There is no free bed in your category ${a}.',
1459                    mapping = {'a':acc_details['bt']}))
1460                self.redirect(self.url(self.context))
1461                return
1462        # Release old bed if exists
1463        if self.context.bed != None:
1464            self.context.bed.owner = NOT_OCCUPIED
1465            notify(grok.ObjectModifiedEvent(self.context.bed))
1466        # Alocate new bed
1467        self.context.bed_type = acc_details['bt']
1468        self.context.bed = new_bed
1469        hall_title = new_bed.__parent__.hostel_name
1470        coordinates = new_bed.getBedCoordinates()[1:]
1471        block, room_nr, bed_nr = coordinates
1472        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1473            'a':hall_title, 'b':block,
1474            'c':room_nr, 'd':bed_nr,
1475            'e':new_bed.bed_type})
1476        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1477        self.context.bed_coordinates = translate(
1478            bc, 'waeup.kofa',target_language=portal_language)
1479        self.flash(_('Student relocated: ${a}',
1480            mapping = {'a':self.context.bed_coordinates}))
1481        self.redirect(self.url(self.context))
1482        return
1483
1484    def render(self):
1485        return
1486
1487class StudentHistoryPage(KofaPage):
1488    """ Page to display student clearance data
1489    """
1490    grok.context(IStudent)
1491    grok.name('history')
1492    grok.require('waeup.viewStudent')
1493    grok.template('studenthistory')
1494    pnav = 4
1495
1496    @property
1497    def label(self):
1498        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1499
1500# Pages for students only
1501
1502class StudentBaseEditFormPage(KofaEditFormPage):
1503    """ View to edit student base data
1504    """
1505    grok.context(IStudent)
1506    grok.name('edit_base')
1507    grok.require('waeup.handleStudent')
1508    form_fields = grok.AutoFields(IStudentBase).select(
1509        'email', 'phone')
1510    label = _('Edit base data')
1511    pnav = 4
1512
1513    @action(_('Save'), style='primary')
1514    def save(self, **data):
1515        msave(self, **data)
1516        return
1517
1518class StudentChangePasswordPage(KofaEditFormPage):
1519    """ View to manage student base data
1520    """
1521    grok.context(IStudent)
1522    grok.name('change_password')
1523    grok.require('waeup.handleStudent')
1524    grok.template('change_password')
1525    label = _('Change password')
1526    pnav = 4
1527
1528    @action(_('Save'), style='primary')
1529    def save(self, **data):
1530        form = self.request.form
1531        password = form.get('change_password', None)
1532        password_ctl = form.get('change_password_repeat', None)
1533        if password:
1534            validator = getUtility(IPasswordValidator)
1535            errors = validator.validate_password(password, password_ctl)
1536            if not errors:
1537                IUserAccount(self.context).setPassword(password)
1538                self.context.writeLogMessage(self, 'saved: password')
1539                self.flash(_('Password changed.'))
1540            else:
1541                self.flash( ' '.join(errors))
1542        return
1543
1544class StudentFilesUploadPage(KofaPage):
1545    """ View to upload files by student
1546    """
1547    grok.context(IStudent)
1548    grok.name('change_portrait')
1549    grok.require('waeup.uploadStudentFile')
1550    grok.template('filesuploadpage')
1551    label = _('Upload portrait')
1552    pnav = 4
1553
1554    def update(self):
1555        if self.context.student.state != ADMITTED:
1556            emit_lock_message(self)
1557            return
1558        super(StudentFilesUploadPage, self).update()
1559        return
1560
1561class StartClearancePage(KofaPage):
1562    grok.context(IStudent)
1563    grok.name('start_clearance')
1564    grok.require('waeup.handleStudent')
1565    grok.template('enterpin')
1566    label = _('Start clearance')
1567    ac_prefix = 'CLR'
1568    notice = ''
1569    pnav = 4
1570    buttonname = _('Start clearance now')
1571
1572    @property
1573    def all_required_fields_filled(self):
1574        if self.context.email and self.context.phone:
1575            return True
1576        return False
1577
1578    @property
1579    def portrait_uploaded(self):
1580        store = getUtility(IExtFileStore)
1581        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1582            return True
1583        return False
1584
1585    def update(self, SUBMIT=None):
1586        if not self.context.state == ADMITTED:
1587            self.flash(_("Wrong state"))
1588            self.redirect(self.url(self.context))
1589            return
1590        if not self.portrait_uploaded:
1591            self.flash(_("No portrait uploaded."))
1592            self.redirect(self.url(self.context, 'change_portrait'))
1593            return
1594        if not self.all_required_fields_filled:
1595            self.flash(_("Not all required fields filled."))
1596            self.redirect(self.url(self.context, 'edit_base'))
1597            return
1598        self.ac_series = self.request.form.get('ac_series', None)
1599        self.ac_number = self.request.form.get('ac_number', None)
1600
1601        if SUBMIT is None:
1602            return
1603        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1604        code = get_access_code(pin)
1605        if not code:
1606            self.flash(_('Activation code is invalid.'))
1607            return
1608        if code.state == USED:
1609            self.flash(_('Activation code has already been used.'))
1610            return
1611        # Mark pin as used (this also fires a pin related transition)
1612        # and fire transition start_clearance
1613        comment = _(u"invalidated")
1614        # Here we know that the ac is in state initialized so we do not
1615        # expect an exception, but the owner might be different
1616        if not invalidate_accesscode(pin, comment, self.context.student_id):
1617            self.flash(_('You are not the owner of this access code.'))
1618            return
1619        self.context.clr_code = pin
1620        IWorkflowInfo(self.context).fireTransition('start_clearance')
1621        self.flash(_('Clearance process has been started.'))
1622        self.redirect(self.url(self.context,'cedit'))
1623        return
1624
1625class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1626    """ View to edit student clearance data by student
1627    """
1628    grok.context(IStudent)
1629    grok.name('cedit')
1630    grok.require('waeup.handleStudent')
1631    label = _('Edit clearance data')
1632
1633    @property
1634    def form_fields(self):
1635        if self.context.is_postgrad:
1636            form_fields = grok.AutoFields(IPGStudentClearance).omit(
1637                'clearance_locked', 'clr_code')
1638        else:
1639            form_fields = grok.AutoFields(IUGStudentClearance).omit(
1640                'clearance_locked', 'clr_code')
1641        return form_fields
1642
1643    def update(self):
1644        if self.context.clearance_locked:
1645            emit_lock_message(self)
1646            return
1647        return super(StudentClearanceEditFormPage, self).update()
1648
1649    @action(_('Save'), style='primary')
1650    def save(self, **data):
1651        self.applyData(self.context, **data)
1652        self.flash(_('Clearance form has been saved.'))
1653        return
1654
1655    def dataNotComplete(self):
1656        """To be implemented in the customization package.
1657        """
1658        return False
1659
1660    @action(_('Save and request clearance'), style='primary')
1661    def requestClearance(self, **data):
1662        self.applyData(self.context, **data)
1663        if self.dataNotComplete():
1664            self.flash(self.dataNotComplete())
1665            return
1666        self.flash(_('Clearance form has been saved.'))
1667        self.redirect(self.url(self.context,'request_clearance'))
1668        return
1669
1670class RequestClearancePage(KofaPage):
1671    grok.context(IStudent)
1672    grok.name('request_clearance')
1673    grok.require('waeup.handleStudent')
1674    grok.template('enterpin')
1675    label = _('Request clearance')
1676    notice = _('Enter the CLR access code used for starting clearance.')
1677    ac_prefix = 'CLR'
1678    pnav = 4
1679    buttonname = _('Request clearance now')
1680
1681    def update(self, SUBMIT=None):
1682        self.ac_series = self.request.form.get('ac_series', None)
1683        self.ac_number = self.request.form.get('ac_number', None)
1684        if SUBMIT is None:
1685            return
1686        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1687        if self.context.clr_code != pin:
1688            self.flash(_("This isn't your CLR access code."))
1689            return
1690        state = IWorkflowState(self.context).getState()
1691        # This shouldn't happen, but the application officer
1692        # might have forgotten to lock the form after changing the state
1693        if state != CLEARANCE:
1694            self.flash(_('This form cannot be submitted. Wrong state!'))
1695            return
1696        IWorkflowInfo(self.context).fireTransition('request_clearance')
1697        self.flash(_('Clearance has been requested.'))
1698        self.redirect(self.url(self.context))
1699        return
1700
1701class StartSessionPage(KofaPage):
1702    grok.context(IStudentStudyCourse)
1703    grok.name('start_session')
1704    grok.require('waeup.handleStudent')
1705    grok.template('enterpin')
1706    label = _('Start session')
1707    ac_prefix = 'SFE'
1708    notice = ''
1709    pnav = 4
1710    buttonname = _('Start now')
1711
1712    def update(self, SUBMIT=None):
1713        if not self.context.next_session_allowed:
1714            self.flash(_("You are not entitled to start session."))
1715            self.redirect(self.url(self.context))
1716            return
1717        self.ac_series = self.request.form.get('ac_series', None)
1718        self.ac_number = self.request.form.get('ac_number', None)
1719
1720        if SUBMIT is None:
1721            return
1722        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1723        code = get_access_code(pin)
1724        if not code:
1725            self.flash(_('Activation code is invalid.'))
1726            return
1727        # Mark pin as used (this also fires a pin related transition)
1728        if code.state == USED:
1729            self.flash(_('Activation code has already been used.'))
1730            return
1731        else:
1732            comment = _(u"invalidated")
1733            # Here we know that the ac is in state initialized so we do not
1734            # expect an error, but the owner might be different
1735            if not invalidate_accesscode(
1736                pin,comment,self.context.student.student_id):
1737                self.flash(_('You are not the owner of this access code.'))
1738                return
1739        if self.context.student.state == CLEARED:
1740            IWorkflowInfo(self.context.student).fireTransition(
1741                'pay_first_school_fee')
1742        elif self.context.student.state == RETURNING:
1743            IWorkflowInfo(self.context.student).fireTransition(
1744                'pay_school_fee')
1745        elif self.context.student.state == PAID:
1746            IWorkflowInfo(self.context.student).fireTransition(
1747                'pay_pg_fee')
1748        self.flash(_('Session started.'))
1749        self.redirect(self.url(self.context))
1750        return
1751
1752class AddStudyLevelFormPage(KofaEditFormPage):
1753    """ Page for students to add current study levels
1754    """
1755    grok.context(IStudentStudyCourse)
1756    grok.name('add')
1757    grok.require('waeup.handleStudent')
1758    grok.template('studyleveladdpage')
1759    form_fields = grok.AutoFields(IStudentStudyCourse)
1760    pnav = 4
1761
1762    @property
1763    def label(self):
1764        studylevelsource = StudyLevelSource().factory
1765        code = self.context.current_level
1766        title = studylevelsource.getTitle(self.context, code)
1767        return _('Add current level ${a}', mapping = {'a':title})
1768
1769    def update(self):
1770        if self.context.student.state != PAID:
1771            emit_lock_message(self)
1772            return
1773        super(AddStudyLevelFormPage, self).update()
1774        return
1775
1776    @action(_('Create course list now'), style='primary')
1777    def addStudyLevel(self, **data):
1778        studylevel = createObject(u'waeup.StudentStudyLevel')
1779        studylevel.level = self.context.current_level
1780        studylevel.level_session = self.context.current_session
1781        try:
1782            self.context.addStudentStudyLevel(
1783                self.context.certificate,studylevel)
1784        except KeyError:
1785            self.flash(_('This level exists.'))
1786        self.redirect(self.url(self.context))
1787        return
1788
1789class StudyLevelEditFormPage(KofaEditFormPage):
1790    """ Page to edit the student study level data by students
1791    """
1792    grok.context(IStudentStudyLevel)
1793    grok.name('edit')
1794    grok.require('waeup.handleStudent')
1795    grok.template('studyleveleditpage')
1796    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1797        'level_session', 'level_verdict')
1798    pnav = 4
1799    max_credits = 50
1800
1801    def update(self):
1802        if self.context.student.state != PAID:
1803            emit_lock_message(self)
1804            return
1805        super(StudyLevelEditFormPage, self).update()
1806        datatable.need()
1807        warning.need()
1808        return
1809
1810    @property
1811    def label(self):
1812        # Here we know that the cookie has been set
1813        lang = self.request.cookies.get('kofa.language')
1814        level_title = translate(self.context.level_title, 'waeup.kofa',
1815            target_language=lang)
1816        return _('Edit course list of ${a}',
1817            mapping = {'a':level_title})
1818
1819    @property
1820    def total_credits(self):
1821        total_credits = 0
1822        for key, val in self.context.items():
1823            total_credits += val.credits
1824        return total_credits
1825
1826    @property
1827    def translated_values(self):
1828        return translated_values(self)
1829
1830    @action(_('Add course ticket'))
1831    def addCourseTicket(self, **data):
1832        self.redirect(self.url(self.context, 'ctadd'))
1833
1834    @jsaction(_('Remove selected tickets'))
1835    def delCourseTicket(self, **data):
1836        form = self.request.form
1837        if form.has_key('val_id'):
1838            child_id = form['val_id']
1839        else:
1840            self.flash(_('No ticket selected.'))
1841            self.redirect(self.url(self.context, '@@edit'))
1842            return
1843        if not isinstance(child_id, list):
1844            child_id = [child_id]
1845        deleted = []
1846        for id in child_id:
1847            # Students are not allowed to remove core tickets
1848            if not self.context[id].mandatory:
1849                del self.context[id]
1850                deleted.append(id)
1851        if len(deleted):
1852            self.flash(_('Successfully removed: ${a}',
1853                mapping = {'a':', '.join(deleted)}))
1854        self.redirect(self.url(self.context, u'@@edit'))
1855        return
1856
1857    @action(_('Register course list'), style='primary')
1858    def registerCourses(self, **data):
1859        if self.total_credits > self.max_credits:
1860            self.flash(_('Maximum credits of ${a} exceeded.',
1861                mapping = {'a':self.max_credits}))
1862            return
1863        IWorkflowInfo(self.context.student).fireTransition(
1864            'register_courses')
1865        self.flash(_('Course list has been registered.'))
1866        self.redirect(self.url(self.context))
1867        return
1868
1869class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1870    """Add a course ticket by student.
1871    """
1872    grok.name('ctadd')
1873    grok.require('waeup.handleStudent')
1874    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
1875        'score', 'mandatory', 'automatic', 'carry_over')
1876
1877    def update(self):
1878        if self.context.student.state != PAID:
1879            emit_lock_message(self)
1880            return
1881        super(CourseTicketAddFormPage2, self).update()
1882        return
1883
1884    @action(_('Add course ticket'))
1885    def addCourseTicket(self, **data):
1886        # Safety belt
1887        if self.context.student.state != PAID:
1888            return
1889        ticket = createObject(u'waeup.CourseTicket')
1890        course = data['course']
1891        ticket.automatic = False
1892        ticket.carry_over = False
1893        try:
1894            self.context.addCourseTicket(ticket, course)
1895        except KeyError:
1896            self.flash(_('The ticket exists.'))
1897            return
1898        self.flash(_('Successfully added ${a}.',
1899            mapping = {'a':ticket.code}))
1900        self.redirect(self.url(self.context, u'@@edit'))
1901        return
1902
1903
1904class SetPasswordPage(KofaPage):
1905    grok.context(IKofaObject)
1906    grok.name('setpassword')
1907    grok.require('waeup.Anonymous')
1908    grok.template('setpassword')
1909    label = _('Set password for first-time login')
1910    ac_prefix = 'PWD'
1911    pnav = 0
1912    set_button = _('Set')
1913
1914    def update(self, SUBMIT=None):
1915        self.reg_number = self.request.form.get('reg_number', None)
1916        self.ac_series = self.request.form.get('ac_series', None)
1917        self.ac_number = self.request.form.get('ac_number', None)
1918
1919        if SUBMIT is None:
1920            return
1921        hitlist = search(query=self.reg_number,
1922            searchtype='reg_number', view=self)
1923        if not hitlist:
1924            self.flash(_('No student found.'))
1925            return
1926        if len(hitlist) != 1:   # Cannot happen but anyway
1927            self.flash(_('More than one student found.'))
1928            return
1929        student = hitlist[0].context
1930        self.student_id = student.student_id
1931        student_pw = student.password
1932        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1933        code = get_access_code(pin)
1934        if not code:
1935            self.flash(_('Access code is invalid.'))
1936            return
1937        if student_pw and pin == student.adm_code:
1938            self.flash(_(
1939                'Password has already been set. Your Student Id is ${a}',
1940                mapping = {'a':self.student_id}))
1941            return
1942        elif student_pw:
1943            self.flash(
1944                _('Password has already been set. You are using the ' +
1945                'wrong Access Code.'))
1946            return
1947        # Mark pin as used (this also fires a pin related transition)
1948        # and set student password
1949        if code.state == USED:
1950            self.flash(_('Access code has already been used.'))
1951            return
1952        else:
1953            comment = _(u"invalidated")
1954            # Here we know that the ac is in state initialized so we do not
1955            # expect an exception
1956            invalidate_accesscode(pin,comment)
1957            IUserAccount(student).setPassword(self.ac_number)
1958            student.adm_code = pin
1959        self.flash(_('Password has been set. Your Student Id is ${a}',
1960            mapping = {'a':self.student_id}))
1961        return
1962
1963class StudentRequestPasswordPage(KofaAddFormPage):
1964    """Captcha'd registration page for applicants.
1965    """
1966    grok.name('requestpw')
1967    grok.require('waeup.Anonymous')
1968    grok.template('requestpw')
1969    form_fields = grok.AutoFields(IStudentRequestPW).select(
1970        'firstname','number','email')
1971    label = _('Request password for first-time login')
1972
1973    def update(self):
1974        # Handle captcha
1975        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1976        self.captcha_result = self.captcha.verify(self.request)
1977        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1978        return
1979
1980    def _redirect(self, email, password, student_id):
1981        # Forward only email to landing page in base package.
1982        self.redirect(self.url(self.context, 'requestpw_complete',
1983            data = dict(email=email)))
1984        return
1985
1986    def _pw_used(self):
1987        # XXX: False if password has not been used. We need an extra
1988        #      attribute which remembers if student logged in.
1989        return True
1990
1991    @action(_('Send login credentials to email address'), style='primary')
1992    def get_credentials(self, **data):
1993        if not self.captcha_result.is_valid:
1994            # Captcha will display error messages automatically.
1995            # No need to flash something.
1996            return
1997        number = data.get('number','')
1998        firstname = data.get('firstname','')
1999        cat = getUtility(ICatalog, name='students_catalog')
2000        results = list(
2001            cat.searchResults(reg_number=(number, number)))
2002        if not results:
2003            results = list(
2004                cat.searchResults(matric_number=(number, number)))
2005        if results:
2006            student = results[0]
2007            if getattr(student,'firstname',None) is None:
2008                self.flash(_('An error occurred.'))
2009                return
2010            elif student.firstname.lower() != firstname.lower():
2011                # Don't tell the truth here. Anonymous must not
2012                # know that a record was found and only the firstname
2013                # verification failed.
2014                self.flash(_('No student record found.'))
2015                return
2016            elif student.password is not None and self._pw_used:
2017                self.flash(_('Your password has already been set and used. '
2018                             'Please proceed to the login page.'))
2019                return
2020            # Store email address but nothing else.
2021            student.email = data['email']
2022            notify(grok.ObjectModifiedEvent(student))
2023        else:
2024            # No record found, this is the truth.
2025            self.flash(_('No student record found.'))
2026            return
2027
2028        kofa_utils = getUtility(IKofaUtils)
2029        password = kofa_utils.genPassword()
2030        mandate = PasswordMandate()
2031        mandate.params['password'] = password
2032        mandate.params['user'] = student
2033        site = grok.getSite()
2034        site['mandates'].addMandate(mandate)
2035        # Send email with credentials
2036        args = {'mandate_id':mandate.mandate_id}
2037        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2038        url_info = u'Confirmation link: %s' % mandate_url
2039        msg = _('You have successfully requested a password for the')
2040        if kofa_utils.sendCredentials(IUserAccount(student),
2041            password, url_info, msg):
2042            email_sent = student.email
2043        else:
2044            email_sent = None
2045        self._redirect(email=email_sent, password=password,
2046            student_id=student.student_id)
2047        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2048        self.context.logger.info(
2049            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2050        return
2051
2052class StudentRequestPasswordEmailSent(KofaPage):
2053    """Landing page after successful password request.
2054
2055    """
2056    grok.name('requestpw_complete')
2057    grok.require('waeup.Public')
2058    grok.template('requestpwmailsent')
2059    label = _('Your password request was successful.')
2060
2061    def update(self, email=None, student_id=None, password=None):
2062        self.email = email
2063        self.password = password
2064        self.student_id = student_id
2065        return
Note: See TracBrowser for help on using the repository browser.