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

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

Translate transitions and history.

  • Property svn:keywords set to Id
File size: 64.9 KB
RevLine 
[7191]1## $Id: browser.py 7687 2012-02-23 10:18:10Z henrik $
2##
[6621]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"""
[7006]20import sys
[6621]21import grok
[7275]22from urllib import urlencode
[6869]23from time import time
[7256]24from datetime import datetime
[7015]25from zope.event import notify
[6996]26from zope.catalog.interfaces import ICatalog
[7386]27from zope.component import queryUtility, getUtility, createObject
28from zope.formlib.textwidgets import BytesDisplayWidget
[6621]29from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
[6936]30from waeup.sirp.accesscodes import (
[6937]31    invalidate_accesscode, get_access_code, create_accesscode)
[6621]32from waeup.sirp.accesscodes.workflow import USED
33from waeup.sirp.browser import (
[7321]34    SIRPPage, SIRPEditFormPage, SIRPAddFormPage, SIRPDisplayFormPage,
[7369]35    ContactAdminForm, SIRPForm)
[7591]36from waeup.sirp.browser.interfaces import ICaptchaManager
[6621]37from waeup.sirp.browser.breadcrumbs import Breadcrumb
[7329]38from waeup.sirp.browser.resources import datepicker, datatable, tabs, warning
[7459]39from waeup.sirp.browser.layout import jsaction, action, UtilityView
[7147]40from waeup.sirp.interfaces import (
[7358]41    ISIRPObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
[7369]42    ISIRPUtils, IUniversity)
[7687]43from waeup.sirp.interfaces import MessageFactory as _
[6621]44from waeup.sirp.widgets.datewidget import (
[6869]45    FriendlyDateWidget, FriendlyDateDisplayWidget,
46    FriendlyDatetimeDisplayWidget)
[6621]47from waeup.sirp.students.interfaces import (
[7144]48    IStudentsContainer, IStudent, IStudentClearance,
[6859]49    IStudentPersonal, IStudentBase, IStudentStudyCourse,
[7538]50    IStudentAccommodation, IStudentStudyLevel,
[6877]51    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
[7369]52    IStudentOnlinePayment, IBedTicket, IStudentsUtils, IStudentChangePassword
[6621]53    )
[6626]54from waeup.sirp.students.catalog import search
[7539]55from waeup.sirp.students.workflow import (ADMITTED, PAID,
[7158]56    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED)
[6795]57from waeup.sirp.students.studylevel import StudentStudyLevel, CourseTicket
[6775]58from waeup.sirp.students.vocabularies import StudyLevelSource
[6820]59from waeup.sirp.browser.resources import toggleall
[6997]60from waeup.sirp.hostels.hostel import NOT_OCCUPIED
[7459]61from waeup.sirp.utils.helpers import get_current_principal
[6621]62
[6943]63def write_log_message(view, message):
64    ob_class = view.__implemented__.__name__.replace('waeup.sirp.','')
65    view.context.getStudent().loggerInfo(ob_class, message)
66    return
67
[7133]68# Save function used for save methods in pages
[6762]69def msave(view, **data):
70    changed_fields = view.applyData(view.context, **data)
[6771]71    # Turn list of lists into single list
72    if changed_fields:
73        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7205]74    # Inform catalog if certificate has changed
75    # (applyData does this only for the context)
76    if 'certificate' in changed_fields:
77        notify(grok.ObjectModifiedEvent(view.context.getStudent()))
[6771]78    fields_string = ' + '.join(changed_fields)
[6762]79    view.flash('Form has been saved.')
80    if fields_string:
[7108]81        write_log_message(view, 'saved: %s' % fields_string)
[6762]82    return
83
[7145]84def emit_lock_message(view):
[7642]85    """Flash a lock message.
86    """
[7133]87    view.flash('The requested form is locked (read-only).')
88    view.redirect(view.url(view.context))
89    return
90
[6629]91class StudentsBreadcrumb(Breadcrumb):
92    """A breadcrumb for the students container.
93    """
94    grok.context(IStudentsContainer)
[7318]95    title = 'Students'
[6629]96
[7459]97    @property
98    def target(self):
99        user = get_current_principal()
100        if getattr(user, 'user_type', None) == 'student':
101            return None
102        return self.viewname
103
[6818]104class StudentBreadcrumb(Breadcrumb):
105    """A breadcrumb for the student container.
106    """
107    grok.context(IStudent)
108
109    def title(self):
[7364]110        return self.context.display_fullname
[6818]111
[6635]112class SudyCourseBreadcrumb(Breadcrumb):
113    """A breadcrumb for the student study course.
114    """
115    grok.context(IStudentStudyCourse)
[7318]116    title = 'Study Course'
[6635]117
118class PaymentsBreadcrumb(Breadcrumb):
119    """A breadcrumb for the student payments folder.
120    """
[6859]121    grok.context(IStudentPaymentsContainer)
[7318]122    title = 'Payments'
[6635]123
[6870]124class OnlinePaymentBreadcrumb(Breadcrumb):
[7251]125    """A breadcrumb for payments.
[6870]126    """
[6877]127    grok.context(IStudentOnlinePayment)
[6870]128
129    @property
130    def title(self):
131        return self.context.p_id
132
[6635]133class AccommodationBreadcrumb(Breadcrumb):
134    """A breadcrumb for the student accommodation folder.
135    """
136    grok.context(IStudentAccommodation)
[7318]137    title = 'Accommodation'
[6635]138
[6994]139class BedTicketBreadcrumb(Breadcrumb):
140    """A breadcrumb for bed tickets.
141    """
142    grok.context(IBedTicket)
[7009]143
[6994]144    @property
145    def title(self):
146        return 'Bed Ticket %s' % self.context.getSessionString()
147
[6776]148class StudyLevelBreadcrumb(Breadcrumb):
149    """A breadcrumb for course lists.
150    """
151    grok.context(IStudentStudyLevel)
152
153    @property
154    def title(self):
155        return self.context.level_title
156
[7321]157class StudentsContainerPage(SIRPPage):
[6626]158    """The standard view for student containers.
[6621]159    """
160    grok.context(IStudentsContainer)
161    grok.name('index')
[7240]162    grok.require('waeup.viewStudentsContainer')
[6695]163    grok.template('containerpage')
[6654]164    label = 'Student Section'
[6642]165    pnav = 4
[6621]166
[6626]167    def update(self, *args, **kw):
168        datatable.need()
169        form = self.request.form
170        self.hitlist = []
171        if 'searchterm' in form and form['searchterm']:
172            self.searchterm = form['searchterm']
173            self.searchtype = form['searchtype']
174        elif 'old_searchterm' in form:
175            self.searchterm = form['old_searchterm']
176            self.searchtype = form['old_searchtype']
177        else:
178            if 'search' in form:
179                self.flash('Empty search string.')
180            return
[7068]181        if self.searchtype == 'current_session':
182            self.searchterm = int(self.searchterm)
[6626]183        self.hitlist = search(query=self.searchterm,
184            searchtype=self.searchtype, view=self)
185        if not self.hitlist:
186            self.flash('No student found.')
187        return
188
[7321]189class StudentsContainerManagePage(SIRPPage):
[6626]190    """The manage page for student containers.
[6622]191    """
192    grok.context(IStudentsContainer)
193    grok.name('manage')
[7136]194    grok.require('waeup.manageStudent')
[6695]195    grok.template('containermanagepage')
[6642]196    pnav = 4
[7466]197    label = 'Manage student section'
[6622]198
[6626]199    def update(self, *args, **kw):
200        datatable.need()
[6820]201        toggleall.need()
[7329]202        warning.need()
[6626]203        form = self.request.form
204        self.hitlist = []
205        if 'searchterm' in form and form['searchterm']:
206            self.searchterm = form['searchterm']
207            self.searchtype = form['searchtype']
208        elif 'old_searchterm' in form:
209            self.searchterm = form['old_searchterm']
210            self.searchtype = form['old_searchtype']
211        else:
212            if 'search' in form:
213                self.flash('Empty search string.')
214            return
215        if not 'entries' in form:
216            self.hitlist = search(query=self.searchterm,
217                searchtype=self.searchtype, view=self)
218            if not self.hitlist:
219                self.flash('No student found.')
[7459]220            if 'remove' in form:
221                self.flash('No item selected.')
[6626]222            return
223        entries = form['entries']
224        if isinstance(entries, basestring):
225            entries = [entries]
226        deleted = []
227        for entry in entries:
228            if 'remove' in form:
229                del self.context[entry]
230                deleted.append(entry)
231        self.hitlist = search(query=self.searchterm,
232            searchtype=self.searchtype, view=self)
233        if len(deleted):
234            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6622]235        return
236
[7321]237class StudentAddFormPage(SIRPAddFormPage):
[6622]238    """Add-form to add a student.
239    """
240    grok.context(IStudentsContainer)
[7136]241    grok.require('waeup.manageStudent')
[6622]242    grok.name('addstudent')
[7357]243    form_fields = grok.AutoFields(IStudent).select(
[7520]244        'firstname', 'middlename', 'lastname', 'reg_number')
[6622]245    label = 'Add student'
[6642]246    pnav = 4
[6622]247
[7459]248    @action('Create student record', style='primary')
[6622]249    def addStudent(self, **data):
250        student = createObject(u'waeup.Student')
251        self.applyData(student, **data)
[6652]252        self.context.addStudent(student)
[6626]253        self.flash('Student record created.')
[6651]254        self.redirect(self.url(self.context[student.student_id], 'index'))
[6622]255        return
256
[7321]257class StudentBaseDisplayFormPage(SIRPDisplayFormPage):
[6631]258    """ Page to display student base data
259    """
[6622]260    grok.context(IStudent)
261    grok.name('index')
[6660]262    grok.require('waeup.viewStudent')
[6695]263    grok.template('basepage')
[6756]264    form_fields = grok.AutoFields(IStudentBase).omit('password')
[6642]265    pnav = 4
[6622]266
267    @property
268    def label(self):
[7364]269        return '%s: Base Data' % self.context.display_fullname
[6631]270
[6699]271    @property
272    def hasPassword(self):
273        if self.context.password:
274            return 'set'
275        return 'unset'
276
[7229]277class ContactStudentForm(ContactAdminForm):
278    grok.context(IStudent)
[7230]279    grok.name('contactstudent')
[7275]280    grok.require('waeup.viewStudent')
[7229]281    pnav = 4
282    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
283
[7275]284    def update(self, subject=u''):
285        self.form_fields.get('subject').field.default = subject
286        self.subject = subject
287        return
288
[7229]289    def label(self):
[7364]290        return u'Send message to %s' % self.context.display_fullname
[7229]291
[7459]292    @action('Send message now', style='primary')
[7229]293    def send(self, *args, **data):
[7234]294        try:
[7403]295            email = self.request.principal.email
[7234]296        except AttributeError:
[7403]297            email = self.config.email_admin
298        usertype = getattr(self.request.principal,
299                           'user_type', 'system').title()
[7358]300        sirp_utils = getUtility(ISIRPUtils)
[7404]301        success = sirp_utils.sendContactForm(
[7403]302                self.request.principal.title,email,
303                self.context.display_fullname,self.context.email,
304                self.request.principal.id,usertype,
305                self.config.name,
306                data['body'],data['subject'])
[7229]307        if success:
308            self.flash('Your message has been sent.')
309        else:
310            self.flash('An smtp server error occurred.')
311        return
312
[7321]313class StudentBaseManageFormPage(SIRPEditFormPage):
[7133]314    """ View to manage student base data
[6631]315    """
316    grok.context(IStudent)
[7133]317    grok.name('manage_base')
[7136]318    grok.require('waeup.manageStudent')
[6631]319    form_fields = grok.AutoFields(IStudentBase).omit('student_id')
[6695]320    grok.template('basemanagepage')
321    label = 'Manage base data'
[6642]322    pnav = 4
[6631]323
[6638]324    def update(self):
325        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]326        tabs.need()
[7490]327        self.tab1 = self.tab2 = ''
328        qs = self.request.get('QUERY_STRING', '')
329        if not qs:
330            qs = 'tab1'
331        setattr(self, qs, 'active')
[6638]332        super(StudentBaseManageFormPage, self).update()
333        self.wf_info = IWorkflowInfo(self.context)
334        return
335
336    def getTransitions(self):
337        """Return a list of dicts of allowed transition ids and titles.
338
339        Each list entry provides keys ``name`` and ``title`` for
340        internal name and (human readable) title of a single
341        transition.
342        """
343        allowed_transitions = self.wf_info.getManualTransitions()
[7687]344        return [dict(name='', title=_('No transition'))] +[
[6638]345            dict(name=x, title=y) for x, y in allowed_transitions]
346
[7459]347    @action('Save', style='primary')
[6638]348    def save(self, **data):
[6701]349        form = self.request.form
[6790]350        password = form.get('password', None)
351        password_ctl = form.get('control_password', None)
352        if password:
[7147]353            validator = getUtility(IPasswordValidator)
354            errors = validator.validate_password(password, password_ctl)
355            if errors:
356                self.flash( ' '.join(errors))
357                return
358        changed_fields = self.applyData(self.context, **data)
[6771]359        # Turn list of lists into single list
360        if changed_fields:
361            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
[7147]362        else:
363            changed_fields = []
364        if password:
365            # Now we know that the form has no errors and can set password ...
366            IUserAccount(self.context).setPassword(password)
367            changed_fields.append('password')
368        # ... and execute transition
[6638]369        if form.has_key('transition') and form['transition']:
370            transition_id = form['transition']
371            self.wf_info.fireTransition(transition_id)
[7147]372        fields_string = ' + '.join(changed_fields)
[6638]373        self.flash('Form has been saved.')
[6644]374        if fields_string:
[6943]375            write_log_message(self, 'saved: % s' % fields_string)
[6638]376        return
377
[7321]378class StudentClearanceDisplayFormPage(SIRPDisplayFormPage):
[6631]379    """ Page to display student clearance data
380    """
381    grok.context(IStudent)
382    grok.name('view_clearance')
[6660]383    grok.require('waeup.viewStudent')
[6695]384    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
[6650]385    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[6642]386    pnav = 4
[6631]387
388    @property
389    def label(self):
[7364]390        return '%s: Clearance Data' % self.context.display_fullname
[6631]391
[7277]392class ExportPDFClearanceSlipPage(grok.View):
393    """Deliver a PDF slip of the context.
394    """
395    grok.context(IStudent)
396    grok.name('clearance.pdf')
397    grok.require('waeup.viewStudent')
398    form_fields = grok.AutoFields(IStudentClearance).omit('clearance_locked')
[7318]399    form_fields['date_of_birth'].custom_widget = FriendlyDateDisplayWidget('le')
[7277]400    prefix = 'form'
[7318]401    title = 'Clearance Data'
[7277]402
403    @property
404    def label(self):
[7364]405        return 'Clearance Slip of %s' % self.context.display_fullname
[7277]406
407    def render(self):
408        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
409            self.request)
410        students_utils = getUtility(IStudentsUtils)
411        return students_utils.renderPDF(
[7318]412            self, 'clearance.pdf',
[7310]413            self.context.getStudent(), studentview)
[7277]414
[7321]415class StudentClearanceManageFormPage(SIRPEditFormPage):
[6631]416    """ Page to edit student clearance data
417    """
418    grok.context(IStudent)
419    grok.name('edit_clearance')
[7136]420    grok.require('waeup.manageStudent')
[7134]421    grok.template('clearanceeditpage')
[6631]422    form_fields = grok.AutoFields(IStudentClearance)
[6695]423    label = 'Manage clearance data'
[6642]424    pnav = 4
[6650]425    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
426
427    def update(self):
428        datepicker.need() # Enable jQuery datepicker in date fields.
[7134]429        tabs.need()
[7490]430        self.tab1 = self.tab2 = ''
431        qs = self.request.get('QUERY_STRING', '')
432        if not qs:
433            qs = 'tab1'
434        setattr(self, qs, 'active')
[6650]435        return super(StudentClearanceManageFormPage, self).update()
436
[7459]437    @action('Save', style='primary')
[6695]438    def save(self, **data):
[6762]439        msave(self, **data)
[6695]440        return
441
[7459]442class StudentClearPage(UtilityView, grok.View):
[7158]443    """ Clear student by clearance officer
444    """
445    grok.context(IStudent)
446    grok.name('clear')
447    grok.require('waeup.clearStudent')
448
449    def update(self):
450        if self.context.state == REQUESTED:
451            IWorkflowInfo(self.context).fireTransition('clear')
452            self.flash('Student has been cleared.')
453        else:
454            self.flash('Student is in the wrong state.')
455        self.redirect(self.url(self.context,'view_clearance'))
456        return
457
458    def render(self):
459        return
460
[7459]461class StudentRejectClearancePage(UtilityView, grok.View):
[7158]462    """ Reject clearance by clearance officers
463    """
464    grok.context(IStudent)
465    grok.name('reject_clearance')
466    grok.require('waeup.clearStudent')
467
468    def update(self):
469        if self.context.state == CLEARED:
470            IWorkflowInfo(self.context).fireTransition('reset4')
[7275]471            message = 'Clearance has been annulled'
472            self.flash(message)
[7158]473        elif self.context.state == REQUESTED:
474            IWorkflowInfo(self.context).fireTransition('reset3')
[7275]475            message = 'Clearance request has been rejected'
476            self.flash(message)
[7158]477        else:
478            self.flash('Student is in the wrong state.')
[7334]479            self.redirect(self.url(self.context,'view_clearance'))
[7275]480            return
481        args = {'subject':message}
482        self.redirect(self.url(self.context) +
483            '/contactstudent?%s' % urlencode(args))
[7158]484        return
485
486    def render(self):
487        return
488
[7321]489class StudentPersonalDisplayFormPage(SIRPDisplayFormPage):
[6631]490    """ Page to display student personal data
491    """
492    grok.context(IStudent)
493    grok.name('view_personal')
[6660]494    grok.require('waeup.viewStudent')
[6631]495    form_fields = grok.AutoFields(IStudentPersonal)
[7386]496    form_fields['perm_address'].custom_widget = BytesDisplayWidget
[6642]497    pnav = 4
[6631]498
499    @property
500    def label(self):
[7364]501        return '%s: Personal Data' % self.context.display_fullname
[6631]502
[7321]503class StudentPersonalManageFormPage(SIRPEditFormPage):
[6631]504    """ Page to edit student clearance data
505    """
506    grok.context(IStudent)
507    grok.name('edit_personal')
[6660]508    grok.require('waeup.viewStudent')
[6631]509    form_fields = grok.AutoFields(IStudentPersonal)
[6695]510    label = 'Manage personal data'
[6642]511    pnav = 4
[6631]512
[7459]513    @action('Save', style='primary')
[6762]514    def save(self, **data):
515        msave(self, **data)
516        return
517
[7321]518class StudyCourseDisplayFormPage(SIRPDisplayFormPage):
[6635]519    """ Page to display the student study course data
520    """
521    grok.context(IStudentStudyCourse)
522    grok.name('index')
[6660]523    grok.require('waeup.viewStudent')
[6635]524    form_fields = grok.AutoFields(IStudentStudyCourse)
[6775]525    grok.template('studycoursepage')
[6642]526    pnav = 4
[6635]527
528    @property
529    def label(self):
[7364]530        return '%s: Study Course' % self.context.__parent__.display_fullname
[6635]531
[6912]532    @property
533    def current_mode(self):
[7641]534        if self.context.certificate is not None:
[7681]535            studymodes_dict = getUtility(ISIRPUtils).getStudyModesDict()
536            return studymodes_dict[self.context.certificate.study_mode]
[7171]537        return
[7642]538
[7171]539    @property
540    def department(self):
[7205]541        if self.context.certificate is not None:
[7171]542            return self.context.certificate.__parent__.__parent__
543        return
[6912]544
[7171]545    @property
546    def faculty(self):
[7205]547        if self.context.certificate is not None:
[7171]548            return self.context.certificate.__parent__.__parent__.__parent__
549        return
550
[7321]551class StudyCourseManageFormPage(SIRPEditFormPage):
[6649]552    """ Page to edit the student study course data
553    """
554    grok.context(IStudentStudyCourse)
[6775]555    grok.name('manage')
[7136]556    grok.require('waeup.manageStudent')
[6775]557    grok.template('studycoursemanagepage')
[6649]558    form_fields = grok.AutoFields(IStudentStudyCourse)
[6695]559    label = 'Manage study course'
[6649]560    pnav = 4
[6775]561    taboneactions = ['Save','Cancel']
562    tabtwoactions = ['Remove selected levels','Cancel']
563    tabthreeactions = ['Add study level']
[6649]564
[6775]565    def update(self):
566        super(StudyCourseManageFormPage, self).update()
567        tabs.need()
[7484]568        self.tab1 = self.tab2 = ''
569        qs = self.request.get('QUERY_STRING', '')
570        if not qs:
571            qs = 'tab1'
572        setattr(self, qs, 'active')
[7490]573        warning.need()
574        datatable.need()
575        return
[6775]576
[7459]577    @action('Save', style='primary')
[6761]578    def save(self, **data):
[6762]579        msave(self, **data)
[6761]580        return
581
[6775]582    @property
583    def level_dict(self):
584        studylevelsource = StudyLevelSource().factory
585        for code in studylevelsource.getValues(self.context):
586            title = studylevelsource.getTitle(self.context, code)
587            yield(dict(code=code, title=title))
588
[7459]589    @action('Add study level')
[6774]590    def addStudyLevel(self, **data):
[6775]591        level_code = self.request.form.get('addlevel', None)
[6774]592        studylevel = StudentStudyLevel()
[6775]593        studylevel.level = int(level_code)
594        try:
[6782]595            self.context.addStudentStudyLevel(
596                self.context.certificate,studylevel)
[7484]597            self.flash('Study level has been added.')
[6775]598        except KeyError:
599            self.flash('This level exists.')
[7484]600        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6774]601        return
602
[7329]603    @jsaction('Remove selected levels')
[6775]604    def delStudyLevels(self, **data):
605        form = self.request.form
606        if form.has_key('val_id'):
607            child_id = form['val_id']
608        else:
609            self.flash('No study level selected.')
[7484]610            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6775]611            return
612        if not isinstance(child_id, list):
613            child_id = [child_id]
614        deleted = []
615        for id in child_id:
616            try:
617                del self.context[id]
618                deleted.append(id)
619            except:
620                self.flash('Could not delete %s: %s: %s' % (
621                        id, sys.exc_info()[0], sys.exc_info()[1]))
622        if len(deleted):
623            self.flash('Successfully removed: %s' % ', '.join(deleted))
[7484]624        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6775]625        return
[6774]626
[7321]627class StudyLevelDisplayFormPage(SIRPDisplayFormPage):
[6774]628    """ Page to display student study levels
629    """
630    grok.context(IStudentStudyLevel)
631    grok.name('index')
632    grok.require('waeup.viewStudent')
[6775]633    form_fields = grok.AutoFields(IStudentStudyLevel)
[6783]634    grok.template('studylevelpage')
[6774]635    pnav = 4
636
[7310]637    def update(self):
638        super(StudyLevelDisplayFormPage, self).update()
639        datatable.need()
640        return
641
[6774]642    @property
643    def label(self):
[6776]644        return '%s: Study Level %s' % (
[7364]645            self.context.getStudent().display_fullname,self.context.level_title)
[6774]646
[6803]647    @property
648    def total_credits(self):
649        total_credits = 0
650        for key, val in self.context.items():
651            total_credits += val.credits
652        return total_credits
653
[7459]654class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
[7028]655    """Deliver a PDF slip of the context.
656    """
657    grok.context(IStudentStudyLevel)
658    grok.name('course_registration.pdf')
659    grok.require('waeup.viewStudent')
660    form_fields = grok.AutoFields(IStudentStudyLevel)
661    prefix = 'form'
[7318]662    title = 'Level Data'
663    content_title = 'Course List'
[7028]664
665    @property
666    def label(self):
667        return 'Course Registration Slip %s' % self.context.level_title
668
669    def render(self):
670        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
671            self.request)
[7150]672        students_utils = getUtility(IStudentsUtils)
[7318]673        tabledata = sorted(self.context.values(),
674            key=lambda value: str(value.semester) + value.code)
[7186]675        return students_utils.renderPDF(
[7318]676            self, 'course_registration.pdf',
[7310]677            self.context.getStudent(), studentview,
678            tableheader=[('Sem.','semester', 1.5),('Code','code', 2.5),
679                         ('Title','title', 5),
680                         ('Dept.','dcode', 1.5), ('Faculty','fcode', 1.5),
681                         ('Cred.', 'credits', 1.5),
[7665]682                         ('Mand.', 'mandatory', 1.5),
[7310]683                         ('Score', 'score', 1.5),('Auto', 'automatic', 1.5)
[7304]684                         ],
[7318]685            tabledata=tabledata)
[7028]686
[7321]687class StudyLevelManageFormPage(SIRPEditFormPage):
[6792]688    """ Page to edit the student study level data
689    """
690    grok.context(IStudentStudyLevel)
691    grok.name('manage')
[7136]692    grok.require('waeup.manageStudent')
[6792]693    grok.template('studylevelmanagepage')
694    form_fields = grok.AutoFields(IStudentStudyLevel)
695    pnav = 4
696    taboneactions = ['Save','Cancel']
[6795]697    tabtwoactions = ['Add course ticket','Remove selected tickets','Cancel']
[6792]698
699    def update(self):
700        super(StudyLevelManageFormPage, self).update()
701        tabs.need()
[7484]702        self.tab1 = self.tab2 = ''
703        qs = self.request.get('QUERY_STRING', '')
704        if not qs:
705            qs = 'tab1'
706        setattr(self, qs, 'active')
[7490]707        warning.need()
708        datatable.need()
[6792]709        return
710
711    @property
712    def label(self):
713        return 'Manage study level %s' % self.context.level_title
714
[7459]715    @action('Save', style='primary')
[6792]716    def save(self, **data):
717        msave(self, **data)
718        return
719
[7459]720    @action('Add course ticket')
[6795]721    def addCourseTicket(self, **data):
722        self.redirect(self.url(self.context, '@@add'))
[6792]723
[7329]724    @jsaction('Remove selected tickets')
[6792]725    def delCourseTicket(self, **data):
726        form = self.request.form
727        if form.has_key('val_id'):
728            child_id = form['val_id']
729        else:
730            self.flash('No ticket selected.')
[7484]731            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6792]732            return
733        if not isinstance(child_id, list):
734            child_id = [child_id]
735        deleted = []
736        for id in child_id:
737            try:
738                del self.context[id]
739                deleted.append(id)
740            except:
741                self.flash('Could not delete %s: %s: %s' % (
742                        id, sys.exc_info()[0], sys.exc_info()[1]))
743        if len(deleted):
744            self.flash('Successfully removed: %s' % ', '.join(deleted))
[7484]745        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]746        return
747
[7459]748class ValidateCoursesPage(UtilityView, grok.View):
[7334]749    """ Validate course list by course adviser
750    """
751    grok.context(IStudentStudyLevel)
752    grok.name('validate_courses')
753    grok.require('waeup.validateStudent')
754
755    def update(self):
756        if str(self.context.__parent__.current_level) != self.context.__name__:
757            self.flash('This level does not correspond current level.')
758        elif self.context.getStudent().state == REGISTERED:
[7642]759            IWorkflowInfo(self.context.getStudent()).fireTransition(
760                'validate_courses')
[7337]761            self.flash('Course list has been validated.')
[7334]762        else:
763            self.flash('Student is in the wrong state.')
764        self.redirect(self.url(self.context))
765        return
766
767    def render(self):
768        return
769
[7459]770class RejectCoursesPage(UtilityView, grok.View):
[7334]771    """ Reject course list by course adviser
772    """
773    grok.context(IStudentStudyLevel)
774    grok.name('reject_courses')
775    grok.require('waeup.validateStudent')
776
777    def update(self):
778        if str(self.context.__parent__.current_level) != self.context.__name__:
779            self.flash('This level does not correspond current level.')
780            self.redirect(self.url(self.context))
781            return
782        elif self.context.getStudent().state == VALIDATED:
783            IWorkflowInfo(self.context.getStudent()).fireTransition('reset8')
784            message = 'Course list request has been annulled'
785            self.flash(message)
786        elif self.context.getStudent().state == REGISTERED:
787            IWorkflowInfo(self.context.getStudent()).fireTransition('reset7')
788            message = 'Course list request has been rejected'
789            self.flash(message)
790        else:
791            self.flash('Student is in the wrong state.')
792            self.redirect(self.url(self.context))
793            return
794        args = {'subject':message}
795        self.redirect(self.url(self.context.getStudent()) +
796            '/contactstudent?%s' % urlencode(args))
797        return
798
799    def render(self):
800        return
801
[7321]802class CourseTicketAddFormPage(SIRPAddFormPage):
[6808]803    """Add a course ticket.
[6795]804    """
805    grok.context(IStudentStudyLevel)
806    grok.name('add')
[7136]807    grok.require('waeup.manageStudent')
[6795]808    label = 'Add course ticket'
[6808]809    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[7663]810        'grade', 'score', 'automatic', 'carry_over')
[6795]811    pnav = 4
812
[7459]813    @action('Add course ticket')
[6795]814    def addCourseTicket(self, **data):
815        ticket = CourseTicket()
816        course = data['course']
[6802]817        ticket.automatic = False
[6795]818        ticket.code = course.code
819        ticket.title = course.title
[7304]820        ticket.fcode = course.__parent__.__parent__.__parent__.code
821        ticket.dcode = course.__parent__.__parent__.code
[6795]822        ticket.credits = course.credits
823        ticket.passmark = course.passmark
824        ticket.semester = course.semester
825        try:
826            self.context.addCourseTicket(ticket)
827        except KeyError:
828            self.flash('The ticket exists.')
829            return
[6799]830        self.flash('Successfully added %s.' % ticket.code)
[7484]831        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6795]832        return
833
[7459]834    @action('Cancel')
[6795]835    def cancel(self, **data):
836        self.redirect(self.url(self.context))
837
[7321]838class CourseTicketDisplayFormPage(SIRPDisplayFormPage):
[6796]839    """ Page to display course tickets
840    """
841    grok.context(ICourseTicket)
842    grok.name('index')
843    grok.require('waeup.viewStudent')
844    form_fields = grok.AutoFields(ICourseTicket)
845    grok.template('courseticketpage')
846    pnav = 4
847
848    @property
849    def label(self):
850        return '%s: Course Ticket %s' % (
[7364]851            self.context.getStudent().display_fullname,self.context.code)
[6796]852
[7321]853class CourseTicketManageFormPage(SIRPEditFormPage):
[6796]854    """ Page to manage course tickets
855    """
856    grok.context(ICourseTicket)
857    grok.name('manage')
[7136]858    grok.require('waeup.manageStudent')
[6796]859    form_fields = grok.AutoFields(ICourseTicket)
860    grok.template('courseticketmanagepage')
861    pnav = 4
862
863    @property
864    def label(self):
865        return 'Manage course ticket %s' % self.context.code
866
[7459]867    @action('Save', style='primary')
[6796]868    def save(self, **data):
869        msave(self, **data)
870        return
871
[7321]872class PaymentsManageFormPage(SIRPEditFormPage):
[6869]873    """ Page to manage the student payments
[7642]874
875    This manage form page is for both students and students officers.
[6869]876    """
877    grok.context(IStudentPaymentsContainer)
[6940]878    grok.name('index')
[7181]879    grok.require('waeup.payStudent')
[6869]880    form_fields = grok.AutoFields(IStudentPaymentsContainer)
881    grok.template('paymentsmanagepage')
882    pnav = 4
883
[6940]884    def unremovable(self, ticket):
[7251]885        usertype = getattr(self.request.principal, 'user_type', None)
886        if not usertype:
887            return False
888        return (self.request.principal.user_type == 'student' and ticket.r_code)
[6940]889
[6869]890    @property
891    def label(self):
[7364]892        return '%s: Payments' % self.context.__parent__.display_fullname
[6869]893
894    def update(self):
895        super(PaymentsManageFormPage, self).update()
896        datatable.need()
[7329]897        warning.need()
[6869]898        return
899
[7329]900    @jsaction('Remove selected tickets')
[6869]901    def delPaymentTicket(self, **data):
902        form = self.request.form
903        if form.has_key('val_id'):
904            child_id = form['val_id']
905        else:
906            self.flash('No payment selected.')
[6940]907            self.redirect(self.url(self.context))
[6869]908            return
909        if not isinstance(child_id, list):
910            child_id = [child_id]
911        deleted = []
912        for id in child_id:
[6992]913            # Students are not allowed to remove used payment tickets
[6940]914            if not self.unremovable(self.context[id]):
915                try:
916                    del self.context[id]
917                    deleted.append(id)
918                except:
919                    self.flash('Could not delete %s: %s: %s' % (
920                            id, sys.exc_info()[0], sys.exc_info()[1]))
[6869]921        if len(deleted):
922            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6943]923            write_log_message(self,'removed: % s' % ', '.join(deleted))
[6940]924        self.redirect(self.url(self.context))
[6869]925        return
926
[7459]927    @action('Add online payment ticket')
[6869]928    def addPaymentTicket(self, **data):
929        self.redirect(self.url(self.context, '@@addop'))
930
[7321]931class OnlinePaymentAddFormPage(SIRPAddFormPage):
[6869]932    """ Page to add an online payment ticket
933    """
934    grok.context(IStudentPaymentsContainer)
935    grok.name('addop')
[7181]936    grok.require('waeup.payStudent')
[6877]937    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]938        'p_category')
939    label = 'Add online payment'
940    pnav = 4
[7642]941
[7459]942    @action('Create ticket', style='primary')
[6869]943    def createTicket(self, **data):
[7024]944        p_category = data['p_category']
945        student = self.context.__parent__
946        if p_category == 'bed_allocation' and student[
947            'studycourse'].current_session != grok.getSite()[
948            'configuration'].accommodation_session:
949                self.flash(
[7642]950                    'Your current session does not match '
951                    'accommodation session.')
[7024]952                self.redirect(self.url(self.context))
953                return
[7150]954        students_utils = getUtility(IStudentsUtils)
[7186]955        pay_details  = students_utils.getPaymentDetails(
[7024]956            p_category,student)
[6994]957        if pay_details['error']:
958            self.flash(pay_details['error'])
[6940]959            self.redirect(self.url(self.context))
[6920]960            return
[7025]961        p_item = pay_details['p_item']
962        p_session = pay_details['p_session']
963        for key in self.context.keys():
964            ticket = self.context[key]
[7248]965            if ticket.p_state == 'paid' and\
966               ticket.p_category == p_category and \
[7025]967               ticket.p_item == p_item and \
968               ticket.p_session == p_session:
969                  self.flash(
[7251]970                      'This type of payment has already been made.')
[7025]971                  self.redirect(self.url(self.context))
972                  return
973        payment = createObject(u'waeup.StudentOnlinePayment')
974        self.applyData(payment, **data)
975        timestamp = "%d" % int(time()*1000)
976        payment.p_id = "p%s" % timestamp
977        payment.p_item = p_item
978        payment.p_session = p_session
[6994]979        payment.amount_auth = pay_details['amount']
980        payment.surcharge_1 = pay_details['surcharge_1']
981        payment.surcharge_2 = pay_details['surcharge_2']
982        payment.surcharge_3 = pay_details['surcharge_3']
[6869]983        self.context[payment.p_id] = payment
984        self.flash('Payment ticket created.')
[6940]985        self.redirect(self.url(self.context))
[6869]986        return
987
[7321]988class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
[6869]989    """ Page to view an online payment ticket
990    """
[6877]991    grok.context(IStudentOnlinePayment)
[6869]992    grok.name('index')
993    grok.require('waeup.viewStudent')
[6877]994    form_fields = grok.AutoFields(IStudentOnlinePayment)
[7642]995    form_fields[
996        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
997    form_fields[
998        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6869]999    pnav = 4
1000
1001    @property
1002    def label(self):
1003        return '%s: Online Payment Ticket %s' % (
[7364]1004            self.context.getStudent().display_fullname,self.context.p_id)
[6869]1005
[7459]1006class OnlinePaymentCallbackPage(UtilityView, grok.View):
[6930]1007    """ Callback view
1008    """
1009    grok.context(IStudentOnlinePayment)
1010    grok.name('callback')
1011    grok.require('waeup.payStudent')
1012
1013    # This update method simulates a valid callback und must be
1014    # specified in the customization package. The parameters must be taken
1015    # from the incoming request.
1016    def update(self):
[7026]1017        if self.context.p_state == 'paid':
1018            self.flash('This ticket has already been paid.')
1019            return
[6936]1020        student = self.context.getStudent()
[6943]1021        write_log_message(self,'valid callback: %s' % self.context.p_id)
[6930]1022        self.context.r_amount_approved = self.context.amount_auth
1023        self.context.r_card_num = u'0000'
1024        self.context.r_code = u'00'
1025        self.context.p_state = 'paid'
1026        self.context.payment_date = datetime.now()
1027        if self.context.p_category == 'clearance':
[6936]1028            # Create CLR access code
[6937]1029            pin, error = create_accesscode('CLR',0,student.student_id)
[6936]1030            if error:
[6940]1031                self.flash('Valid callback received. ' + error)
[6936]1032                return
1033            self.context.ac = pin
[6930]1034        elif self.context.p_category == 'schoolfee':
[6936]1035            # Create SFE access code
[6940]1036            pin, error = create_accesscode('SFE',0,student.student_id)
[6936]1037            if error:
[6940]1038                self.flash('Valid callback received. ' + error)
[6936]1039                return
1040            self.context.ac = pin
[6994]1041        elif self.context.p_category == 'bed_allocation':
1042            # Create HOS access code
1043            pin, error = create_accesscode('HOS',0,student.student_id)
1044            if error:
1045                self.flash('Valid callback received. ' + error)
1046                return
1047            self.context.ac = pin
[6940]1048        self.flash('Valid callback received.')
1049        return
[6930]1050
1051    def render(self):
[6940]1052        self.redirect(self.url(self.context, '@@index'))
[6930]1053        return
1054
[7459]1055class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7019]1056    """Deliver a PDF slip of the context.
1057    """
1058    grok.context(IStudentOnlinePayment)
1059    grok.name('payment_receipt.pdf')
1060    grok.require('waeup.viewStudent')
1061    form_fields = grok.AutoFields(IStudentOnlinePayment)
1062    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1063    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1064    prefix = 'form'
[7318]1065    title = 'Payment Data'
[7019]1066
1067    @property
1068    def label(self):
1069        return 'Online Payment Receipt %s' % self.context.p_id
1070
1071    def render(self):
[7028]1072        if self.context.p_state != 'paid':
1073            self.flash('Ticket not yet paid.')
1074            self.redirect(self.url(self.context))
1075            return
[7019]1076        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1077            self.request)
[7150]1078        students_utils = getUtility(IStudentsUtils)
[7318]1079        return students_utils.renderPDF(self, 'payment_receipt.pdf',
[7310]1080            self.context.getStudent(), studentview)
[7019]1081
[6992]1082
[7321]1083class AccommodationManageFormPage(SIRPEditFormPage):
[7009]1084    """ Page to manage bed tickets.
[7642]1085
1086    This manage form page is for both students and students officers.
[6635]1087    """
1088    grok.context(IStudentAccommodation)
1089    grok.name('index')
[7181]1090    grok.require('waeup.handleAccommodation')
[6635]1091    form_fields = grok.AutoFields(IStudentAccommodation)
[6992]1092    grok.template('accommodationmanagepage')
[6642]1093    pnav = 4
[7017]1094    officers_only_actions = ['Remove selected']
[6635]1095
1096    @property
1097    def label(self):
[7364]1098        return '%s: Accommodation' % self.context.__parent__.display_fullname
[6637]1099
[6992]1100    def update(self):
1101        super(AccommodationManageFormPage, self).update()
1102        datatable.need()
[7329]1103        warning.need()
[6992]1104        return
1105
[7329]1106    @jsaction('Remove selected')
[7009]1107    def delBedTickets(self, **data):
[7240]1108        if getattr(self.request.principal, 'user_type', None) == 'student':
[7017]1109            self.flash('You are not allowed to remove bed tickets.')
1110            self.redirect(self.url(self.context))
1111            return
[6992]1112        form = self.request.form
1113        if form.has_key('val_id'):
1114            child_id = form['val_id']
1115        else:
1116            self.flash('No bed ticket selected.')
1117            self.redirect(self.url(self.context))
1118            return
1119        if not isinstance(child_id, list):
1120            child_id = [child_id]
1121        deleted = []
1122        for id in child_id:
[7068]1123            del self.context[id]
1124            deleted.append(id)
[6992]1125        if len(deleted):
1126            self.flash('Successfully removed: %s' % ', '.join(deleted))
1127            write_log_message(self,'removed: % s' % ', '.join(deleted))
1128        self.redirect(self.url(self.context))
1129        return
1130
[7009]1131    @property
1132    def selected_actions(self):
[7240]1133        if getattr(self.request.principal, 'user_type', None) == 'student':
[7642]1134            return [action for action in self.actions
1135                    if not action.label in self.officers_only_actions]
1136        return self.actions
[7009]1137
[7321]1138class BedTicketAddPage(SIRPPage):
[6992]1139    """ Page to add an online payment ticket
1140    """
1141    grok.context(IStudentAccommodation)
1142    grok.name('add')
[7181]1143    grok.require('waeup.handleAccommodation')
[6992]1144    grok.template('enterpin')
[6993]1145    ac_prefix = 'HOS'
[6992]1146    label = 'Add bed ticket'
1147    pnav = 4
1148    buttonname = 'Create bed ticket'
[6993]1149    notice = ''
[6992]1150
1151    def update(self, SUBMIT=None):
[6996]1152        student = self.context.getStudent()
[7150]1153        students_utils = getUtility(IStudentsUtils)
[7186]1154        acc_details  = students_utils.getAccommodationDetails(student)
[7369]1155        if not acc_details:
1156            self.flash("Your data are incomplete.")
1157            self.redirect(self.url(self.context))
1158            return
[6996]1159        if not student.state in acc_details['allowed_states']:
[7015]1160            self.flash("You are in the wrong registration state.")
[6992]1161            self.redirect(self.url(self.context))
1162            return
[7642]1163        if student['studycourse'].current_session != acc_details[
1164            'booking_session']:
[7061]1165            self.flash(
1166                'Your current session does not match accommodation session.')
1167            self.redirect(self.url(self.context))
1168            return
1169        if str(acc_details['booking_session']) in self.context.keys():
[7642]1170            self.flash(
1171                'You already booked a bed space in current '
1172                'accommodation session.')
[7004]1173            self.redirect(self.url(self.context))
1174            return
[6992]1175        self.ac_series = self.request.form.get('ac_series', None)
1176        self.ac_number = self.request.form.get('ac_number', None)
1177        if SUBMIT is None:
1178            return
1179        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1180        code = get_access_code(pin)
1181        if not code:
1182            self.flash('Activation code is invalid.')
1183            return
[7060]1184        # Search and book bed
[6997]1185        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1186        entries = cat.searchResults(
[7003]1187            owner=(student.student_id,student.student_id))
1188        if len(entries):
[7060]1189            # If bed space has bee manually allocated use this bed
[7003]1190            bed = [entry for entry in entries][0]
[7060]1191        else:
1192            # else search for other available beds
1193            entries = cat.searchResults(
1194                bed_type=(acc_details['bt'],acc_details['bt']))
1195            available_beds = [
1196                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1197            if available_beds:
[7150]1198                students_utils = getUtility(IStudentsUtils)
[7186]1199                bed = students_utils.selectBed(available_beds)
[7060]1200                bed.bookBed(student.student_id)
1201            else:
1202                self.flash('There is no free bed in your category %s.'
1203                            % acc_details['bt'])
1204                return
[6992]1205        # Mark pin as used (this also fires a pin related transition)
1206        if code.state == USED:
1207            self.flash('Activation code has already been used.')
1208            return
1209        else:
[7642]1210            comment = u"AC invalidated for %s" % (
1211                self.context.getStudent().student_id,)
[6992]1212            # Here we know that the ac is in state initialized so we do not
1213            # expect an exception, but the owner might be different
1214            if not invalidate_accesscode(
1215                pin,comment,self.context.getStudent().student_id):
1216                self.flash('You are not the owner of this access code.')
1217                return
[7060]1218        # Create bed ticket
[6992]1219        bedticket = createObject(u'waeup.BedTicket')
1220        bedticket.booking_code = pin
[6994]1221        bedticket.booking_session = acc_details['booking_session']
[6996]1222        bedticket.bed_type = acc_details['bt']
[7006]1223        bedticket.bed = bed
[6996]1224        hall_title = bed.__parent__.hostel_name
1225        coordinates = bed.getBedCoordinates()[1:]
1226        block, room_nr, bed_nr = coordinates
[7068]1227        bedticket.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1228            hall_title, block, room_nr, bed_nr, bed.bed_type)
[6996]1229        key = str(acc_details['booking_session'])
1230        self.context[key] = bedticket
1231        self.flash('Bed ticket created and bed booked: %s'
1232            % bedticket.bed_coordinates)
[6992]1233        self.redirect(self.url(self.context))
1234        return
1235
[7321]1236class BedTicketDisplayFormPage(SIRPDisplayFormPage):
[6994]1237    """ Page to display bed tickets
1238    """
1239    grok.context(IBedTicket)
1240    grok.name('index')
[7181]1241    grok.require('waeup.handleAccommodation')
[6994]1242    form_fields = grok.AutoFields(IBedTicket)
1243    form_fields[
1244        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1245    pnav = 4
1246
1247    @property
1248    def label(self):
1249        return 'Bed Ticket for Session %s' % self.context.getSessionString()
1250
[7459]1251class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
[7027]1252    """Deliver a PDF slip of the context.
1253    """
1254    grok.context(IBedTicket)
1255    grok.name('bed_allocation.pdf')
[7181]1256    grok.require('waeup.handleAccommodation')
[7027]1257    form_fields = grok.AutoFields(IBedTicket)
1258    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1259    prefix = 'form'
[7318]1260    title = 'Bed Allocation Data'
[7027]1261
1262    @property
1263    def label(self):
[7318]1264        return 'Bed Allocation: %s' % self.context.bed_coordinates
[7027]1265
1266    def render(self):
1267        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1268            self.request)
[7150]1269        students_utils = getUtility(IStudentsUtils)
[7186]1270        return students_utils.renderPDF(
[7318]1271            self, 'bed_allocation.pdf',
[7310]1272            self.context.getStudent(), studentview)
[7027]1273
[7459]1274class BedTicketRelocationPage(UtilityView, grok.View):
[7015]1275    """ Callback view
1276    """
1277    grok.context(IBedTicket)
1278    grok.name('relocate')
1279    grok.require('waeup.manageHostels')
1280
[7059]1281    # Relocate student if student parameters have changed or the bed_type
1282    # of the bed has changed
[7015]1283    def update(self):
1284        student = self.context.getStudent()
[7150]1285        students_utils = getUtility(IStudentsUtils)
[7186]1286        acc_details  = students_utils.getAccommodationDetails(student)
[7068]1287        if self.context.bed != None and \
1288              'reserved' in self.context.bed.bed_type:
1289            self.flash("Students in reserved beds can't be relocated.")
1290            self.redirect(self.url(self.context))
1291            return
[7059]1292        if acc_details['bt'] == self.context.bed_type and \
[7068]1293                self.context.bed != None and \
[7059]1294                self.context.bed.bed_type == self.context.bed_type:
[7068]1295            self.flash("Student can't be relocated.")
1296            self.redirect(self.url(self.context))
[7015]1297            return
[7068]1298        # Search a bed
[7015]1299        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1300        entries = cat.searchResults(
[7068]1301            owner=(student.student_id,student.student_id))
1302        if len(entries) and self.context.bed == None:
1303            # If booking has been cancelled but other bed space has been
1304            # manually allocated after cancellation use this bed
1305            new_bed = [entry for entry in entries][0]
1306        else:
1307            # Search for other available beds
1308            entries = cat.searchResults(
1309                bed_type=(acc_details['bt'],acc_details['bt']))
1310            available_beds = [
1311                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1312            if available_beds:
[7150]1313                students_utils = getUtility(IStudentsUtils)
[7186]1314                new_bed = students_utils.selectBed(available_beds)
[7068]1315                new_bed.bookBed(student.student_id)
1316            else:
1317                self.flash('There is no free bed in your category %s.'
1318                            % acc_details['bt'])
1319                self.redirect(self.url(self.context))
1320                return
[7642]1321        # Release old bed if exists
[7068]1322        if self.context.bed != None:
1323            self.context.bed.owner = NOT_OCCUPIED
1324            notify(grok.ObjectModifiedEvent(self.context.bed))
[7015]1325        # Alocate new bed
1326        self.context.bed_type = acc_details['bt']
[7068]1327        self.context.bed = new_bed
1328        hall_title = new_bed.__parent__.hostel_name
1329        coordinates = new_bed.getBedCoordinates()[1:]
[7015]1330        block, room_nr, bed_nr = coordinates
[7068]1331        self.context.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1332            hall_title, block, room_nr, bed_nr, new_bed.bed_type)
1333        self.flash('Student relocated: %s' % self.context.bed_coordinates)
[7015]1334        self.redirect(self.url(self.context))
1335        return
1336
1337    def render(self):
1338        return
1339
[7321]1340class StudentHistoryPage(SIRPPage):
[6637]1341    """ Page to display student clearance data
1342    """
1343    grok.context(IStudent)
1344    grok.name('history')
[6660]1345    grok.require('waeup.viewStudent')
[6637]1346    grok.template('studenthistory')
[6642]1347    pnav = 4
[6637]1348
1349    @property
1350    def label(self):
[7364]1351        return '%s: History' % self.context.display_fullname
[6694]1352
1353# Pages for students only
1354
[7321]1355class StudentBaseEditFormPage(SIRPEditFormPage):
[7133]1356    """ View to edit student base data
1357    """
1358    grok.context(IStudent)
1359    grok.name('edit_base')
1360    grok.require('waeup.handleStudent')
1361    form_fields = grok.AutoFields(IStudentBase).select(
1362        'email', 'phone')
1363    label = 'Edit base data'
1364    pnav = 4
1365
[7459]1366    @action('Save', style='primary')
[7133]1367    def save(self, **data):
1368        msave(self, **data)
1369        return
1370
[7321]1371class StudentChangePasswordPage(SIRPEditFormPage):
[7144]1372    """ View to manage student base data
[6756]1373    """
1374    grok.context(IStudent)
[7114]1375    grok.name('change_password')
[6694]1376    grok.require('waeup.handleStudent')
[7144]1377    grok.template('change_password')
[6694]1378    label = 'Change password'
1379    pnav = 4
1380
[7459]1381    @action('Save', style='primary')
[7144]1382    def save(self, **data):
1383        form = self.request.form
1384        password = form.get('change_password', None)
1385        password_ctl = form.get('change_password_repeat', None)
1386        if password:
[7147]1387            validator = getUtility(IPasswordValidator)
1388            errors = validator.validate_password(password, password_ctl)
1389            if not errors:
1390                IUserAccount(self.context).setPassword(password)
1391                write_log_message(self, 'saved: password')
1392                self.flash('Password changed.')
[6756]1393            else:
[7147]1394                self.flash( ' '.join(errors))
[6756]1395        return
1396
[7321]1397class StudentFilesUploadPage(SIRPPage):
[7114]1398    """ View to upload files by student
1399    """
1400    grok.context(IStudent)
1401    grok.name('change_portrait')
[7127]1402    grok.require('waeup.uploadStudentFile')
[7114]1403    grok.template('filesuploadpage')
1404    label = 'Upload portrait'
1405    pnav = 4
1406
[7133]1407    def update(self):
[7539]1408        if self.context.getStudent().state != ADMITTED:
[7145]1409            emit_lock_message(self)
[7133]1410            return
1411        super(StudentFilesUploadPage, self).update()
1412        return
1413
[7321]1414class StartClearancePage(SIRPPage):
[6770]1415    grok.context(IStudent)
1416    grok.name('start_clearance')
1417    grok.require('waeup.handleStudent')
1418    grok.template('enterpin')
1419    label = 'Start clearance'
1420    ac_prefix = 'CLR'
1421    notice = ''
1422    pnav = 4
1423    buttonname = 'Start clearance now'
1424
[7133]1425    @property
1426    def all_required_fields_filled(self):
1427        if self.context.email and self.context.phone:
1428            return True
1429        return False
1430
1431    @property
1432    def portrait_uploaded(self):
1433        store = getUtility(IExtFileStore)
1434        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1435            return True
1436        return False
1437
[6770]1438    def update(self, SUBMIT=None):
[7671]1439        if not self.context.state == ADMITTED:
[6936]1440            self.flash("Wrong state.")
1441            self.redirect(self.url(self.context))
1442            return
[7133]1443        if not self.portrait_uploaded:
1444            self.flash("No portrait uploaded.")
1445            self.redirect(self.url(self.context, 'change_portrait'))
1446            return
1447        if not self.all_required_fields_filled:
1448            self.flash("Not all required fields filled.")
1449            self.redirect(self.url(self.context, 'edit_base'))
1450            return
[6770]1451        self.ac_series = self.request.form.get('ac_series', None)
1452        self.ac_number = self.request.form.get('ac_number', None)
1453
1454        if SUBMIT is None:
1455            return
1456        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1457        code = get_access_code(pin)
1458        if not code:
[6936]1459            self.flash('Activation code is invalid.')
[6770]1460            return
1461        # Mark pin as used (this also fires a pin related transition)
1462        # and fire transition start_clearance
1463        if code.state == USED:
[6936]1464            self.flash('Activation code has already been used.')
[6770]1465            return
1466        else:
1467            comment = u"AC invalidated for %s" % self.context.student_id
1468            # Here we know that the ac is in state initialized so we do not
[6927]1469            # expect an exception, but the owner might be different
1470            if not invalidate_accesscode(pin,comment,self.context.student_id):
1471                self.flash('You are not the owner of this access code.')
1472                return
[6770]1473            self.context.clr_code = pin
1474        IWorkflowInfo(self.context).fireTransition('start_clearance')
[6771]1475        self.flash('Clearance process has been started.')
[6770]1476        self.redirect(self.url(self.context,'cedit'))
1477        return
1478
[6695]1479class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1480    """ View to edit student clearance data by student
1481    """
1482    grok.context(IStudent)
1483    grok.name('cedit')
1484    grok.require('waeup.handleStudent')
[6756]1485    form_fields = grok.AutoFields(
[7538]1486        IStudentClearance).omit('clearance_locked')
[6722]1487    label = 'Edit clearance data'
[6695]1488    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[6718]1489
1490    def update(self):
1491        if self.context.clearance_locked:
[7145]1492            emit_lock_message(self)
[6718]1493            return
1494        return super(StudentClearanceEditFormPage, self).update()
[6719]1495
[7459]1496    @action('Save', style='primary')
[6722]1497    def save(self, **data):
1498        self.applyData(self.context, **data)
[6771]1499        self.flash('Clearance form has been saved.')
[6722]1500        return
1501
[7253]1502    def dataNotComplete(self):
[7642]1503        """To be implemented in the customization package.
1504        """
[7253]1505        return False
1506
[7459]1507    @action('Save and request clearance', style='primary')
[7186]1508    def requestClearance(self, **data):
[6722]1509        self.applyData(self.context, **data)
[7253]1510        if self.dataNotComplete():
1511            self.flash(self.dataNotComplete())
1512            return
[6771]1513        self.flash('Clearance form has been saved.')
[6769]1514        self.redirect(self.url(self.context,'request_clearance'))
[6722]1515        return
1516
[7321]1517class RequestClearancePage(SIRPPage):
[6769]1518    grok.context(IStudent)
1519    grok.name('request_clearance')
1520    grok.require('waeup.handleStudent')
1521    grok.template('enterpin')
1522    label = 'Request clearance'
1523    notice = 'Enter the CLR access code used for starting clearance.'
1524    ac_prefix = 'CLR'
1525    pnav = 4
1526    buttonname = 'Request clearance now'
1527
1528    def update(self, SUBMIT=None):
1529        self.ac_series = self.request.form.get('ac_series', None)
1530        self.ac_number = self.request.form.get('ac_number', None)
1531        if SUBMIT is None:
1532            return
1533        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1534        if self.context.clr_code != pin:
1535            self.flash("This isn't your CLR access code.")
1536            return
1537        state = IWorkflowState(self.context).getState()
1538        # This shouldn't happen, but the application officer
1539        # might have forgotten to lock the form after changing the state
1540        if state != CLEARANCE:
1541            self.flash('This form cannot be submitted. Wrong state!')
1542            return
1543        IWorkflowInfo(self.context).fireTransition('request_clearance')
1544        self.flash('Clearance has been requested.')
1545        self.redirect(self.url(self.context))
[6789]1546        return
[6806]1547
[7321]1548class StartCourseRegistrationPage(SIRPPage):
[6944]1549    grok.context(IStudentStudyCourse)
1550    grok.name('start_course_registration')
1551    grok.require('waeup.handleStudent')
1552    grok.template('enterpin')
1553    label = 'Start course registration'
1554    ac_prefix = 'SFE'
1555    notice = ''
1556    pnav = 4
1557    buttonname = 'Start course registration now'
1558
1559    def update(self, SUBMIT=None):
1560        if not self.context.getStudent().state in (CLEARED,RETURNING):
1561            self.flash("Wrong state.")
1562            self.redirect(self.url(self.context))
1563            return
1564        self.ac_series = self.request.form.get('ac_series', None)
1565        self.ac_number = self.request.form.get('ac_number', None)
1566
1567        if SUBMIT is None:
1568            return
1569        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1570        code = get_access_code(pin)
1571        if not code:
1572            self.flash('Activation code is invalid.')
1573            return
1574        # Mark pin as used (this also fires a pin related transition)
1575        # and fire transition start_clearance
1576        if code.state == USED:
1577            self.flash('Activation code has already been used.')
1578            return
1579        else:
[7642]1580            comment = u"AC invalidated for %s" % (
1581                self.context.getStudent().student_id,)
[6944]1582            # Here we know that the ac is in state initialized so we do not
1583            # expect an exception, but the owner might be different
1584            if not invalidate_accesscode(
1585                pin,comment,self.context.getStudent().student_id):
1586                self.flash('You are not the owner of this access code.')
1587                return
1588        if self.context.getStudent().state == CLEARED:
1589            IWorkflowInfo(self.context.getStudent()).fireTransition(
1590                'pay_first_school_fee')
1591        elif self.context.getStudent().state == RETURNING:
1592            IWorkflowInfo(self.context.getStudent()).fireTransition(
1593                'pay_school_fee')
1594        self.flash('Course registration has been started.')
1595        self.redirect(self.url(self.context))
1596        return
1597
[7321]1598class AddStudyLevelFormPage(SIRPEditFormPage):
[6806]1599    """ Page for students to add current study levels
1600    """
1601    grok.context(IStudentStudyCourse)
1602    grok.name('add')
1603    grok.require('waeup.handleStudent')
1604    grok.template('studyleveladdpage')
1605    form_fields = grok.AutoFields(IStudentStudyCourse)
1606    pnav = 4
1607
1608    @property
1609    def label(self):
1610        studylevelsource = StudyLevelSource().factory
1611        code = self.context.current_level
1612        title = studylevelsource.getTitle(self.context, code)
1613        return 'Add current level %s' % title
1614
1615    def update(self):
[7539]1616        if self.context.getStudent().state != PAID:
[7145]1617            emit_lock_message(self)
[6806]1618            return
1619        super(AddStudyLevelFormPage, self).update()
1620        return
1621
[7459]1622    @action('Create course list now', style='primary')
[6806]1623    def addStudyLevel(self, **data):
1624        studylevel = StudentStudyLevel()
1625        studylevel.level = self.context.current_level
1626        studylevel.level_session = self.context.current_session
1627        try:
1628            self.context.addStudentStudyLevel(
1629                self.context.certificate,studylevel)
1630        except KeyError:
1631            self.flash('This level exists.')
1632        self.redirect(self.url(self.context))
1633        return
[6808]1634
[7321]1635class StudyLevelEditFormPage(SIRPEditFormPage):
[6808]1636    """ Page to edit the student study level data by students
1637    """
1638    grok.context(IStudentStudyLevel)
1639    grok.name('edit')
1640    grok.require('waeup.handleStudent')
1641    grok.template('studyleveleditpage')
1642    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1643        'level_session', 'level_verdict')
1644    pnav = 4
1645
1646    def update(self):
[7539]1647        if self.context.getStudent().state != PAID:
1648            emit_lock_message(self)
1649            return
[6808]1650        super(StudyLevelEditFormPage, self).update()
1651        datatable.need()
[7329]1652        warning.need()
[6808]1653        return
1654
1655    @property
1656    def label(self):
[7642]1657        return 'Add and remove course tickets of study level %s' % (
1658            self.context.level_title,)
[6808]1659
1660    @property
1661    def total_credits(self):
1662        total_credits = 0
1663        for key, val in self.context.items():
1664            total_credits += val.credits
1665        return total_credits
1666
[7459]1667    @action('Add course ticket')
[6808]1668    def addCourseTicket(self, **data):
1669        self.redirect(self.url(self.context, 'ctadd'))
1670
[7329]1671    @jsaction('Remove selected tickets')
[6808]1672    def delCourseTicket(self, **data):
1673        form = self.request.form
1674        if form.has_key('val_id'):
1675            child_id = form['val_id']
1676        else:
1677            self.flash('No ticket selected.')
1678            self.redirect(self.url(self.context, '@@edit'))
1679            return
1680        if not isinstance(child_id, list):
1681            child_id = [child_id]
1682        deleted = []
1683        for id in child_id:
[6940]1684            # Students are not allowed to remove core tickets
[7665]1685            if not self.context[id].mandatory:
[6808]1686                try:
1687                    del self.context[id]
1688                    deleted.append(id)
1689                except:
1690                    self.flash('Could not delete %s: %s: %s' % (
1691                            id, sys.exc_info()[0], sys.exc_info()[1]))
1692        if len(deleted):
1693            self.flash('Successfully removed: %s' % ', '.join(deleted))
1694        self.redirect(self.url(self.context, u'@@edit'))
1695        return
1696
[7459]1697    @action('Register course list', style='primary')
[7186]1698    def RegisterCourses(self, **data):
[7642]1699        IWorkflowInfo(self.context.getStudent()).fireTransition(
1700            'register_courses')
[6810]1701        self.flash('Course list has been registered.')
1702        self.redirect(self.url(self.context))
1703        return
1704
[6808]1705class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1706    """Add a course ticket by student.
1707    """
1708    grok.name('ctadd')
1709    grok.require('waeup.handleStudent')
1710    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[7665]1711        'grade', 'score', 'mandatory', 'automatic', 'carry_over')
[6808]1712
[7539]1713    def update(self):
1714        if self.context.getStudent().state != PAID:
1715            emit_lock_message(self)
1716            return
1717        super(CourseTicketAddFormPage2, self).update()
1718        return
1719
[7459]1720    @action('Add course ticket')
[6808]1721    def addCourseTicket(self, **data):
[7642]1722        # Safety belt
[7539]1723        if self.context.getStudent().state != PAID:
1724            return
[6808]1725        ticket = CourseTicket()
1726        course = data['course']
[7642]1727        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1728            setattr(ticket, name, getattr(course, name))
[6808]1729        ticket.automatic = False
1730        try:
1731            self.context.addCourseTicket(ticket)
1732        except KeyError:
1733            self.flash('The ticket exists.')
1734            return
1735        self.flash('Successfully added %s.' % ticket.code)
1736        self.redirect(self.url(self.context, u'@@edit'))
1737        return
[7369]1738
1739class ChangePasswordRequestPage(SIRPForm):
1740    """Captcha'd page for students to request a password change.
1741    """
1742    grok.context(IUniversity)
1743    grok.name('changepw')
1744    grok.require('waeup.Anonymous')
1745    grok.template('changepw')
1746    label = 'Change my password'
1747    form_fields = grok.AutoFields(IStudentChangePassword)
1748
1749    def update(self):
1750        # Handle captcha
1751        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1752        self.captcha_result = self.captcha.verify(self.request)
1753        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1754        return
1755
[7462]1756    @action('Get new login credentials', style='primary')
[7369]1757    def request(self, **data):
1758        if not self.captcha_result.is_valid:
1759            # Captcha will display error messages automatically.
1760            # No need to flash something.
1761            return
1762        # Search student
1763        cat = queryUtility(ICatalog, name='students_catalog')
1764        reg_number = data['reg_number']
1765        email = data['email']
1766        results = cat.searchResults(
1767            reg_number=(reg_number, reg_number),
1768            email=(email,email))
1769        if len(results) == 0:
1770            self.flash('No student record found.')
1771            return
1772        student = list(results)[0]
1773        # Change password
1774        sirp_utils = getUtility(ISIRPUtils)
1775        pwd = sirp_utils.genPassword()
1776        IUserAccount(student).setPassword(pwd)
1777        # Send email with new redentials
1778        msg = 'You have successfully changed your password for the'
1779        login_url = self.url(grok.getSite(), 'login')
[7407]1780        success = sirp_utils.sendCredentials(
1781            IUserAccount(student),pwd,login_url,msg)
[7369]1782        if success:
1783            self.flash('An email with your user name and password ' +
[7407]1784                'has been sent to %s.' % email)
[7369]1785        else:
1786            self.flash('An smtp server error occurred.')
[7638]1787        return
1788
[7660]1789class SetPasswordPage(SIRPPage):
1790    grok.context(ISIRPObject)
1791    grok.name('setpassword')
1792    grok.require('waeup.Anonymous')
1793    grok.template('setpassword')
1794    label = 'Set password for first-time login'
1795    ac_prefix = 'PWD'
1796    pnav = 0
1797
1798    def update(self, SUBMIT=None):
1799        self.reg_number = self.request.form.get('reg_number', None)
1800        self.ac_series = self.request.form.get('ac_series', None)
1801        self.ac_number = self.request.form.get('ac_number', None)
1802
1803        if SUBMIT is None:
1804            return
1805        hitlist = search(query=self.reg_number,
1806            searchtype='reg_number', view=self)
1807        if not hitlist:
1808            self.flash('No student found.')
1809            return
1810        if len(hitlist) != 1:   # Cannot happen but anyway
1811            self.flash('More than one student found.')
1812            return
1813        student = hitlist[0].context
1814        self.student_id = student.student_id
1815        student_pw = student.password
1816        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1817        code = get_access_code(pin)
1818        if not code:
1819            self.flash('Access code is invalid.')
1820            return
1821        if student_pw and pin == student.adm_code:
1822            self.flash('Password has already been set. Your Student Id is %s'
1823                % self.student_id)
1824            return
1825        elif student_pw:
1826            self.flash(
1827                'Password has already been set. You are using the '
1828                'wrong Access Code.')
1829            return
1830        # Mark pin as used (this also fires a pin related transition)
1831        # and set student password
1832        if code.state == USED:
1833            self.flash('Access code has already been used.')
1834            return
1835        else:
1836            comment = u"AC invalidated for %s" % self.student_id
1837            # Here we know that the ac is in state initialized so we do not
1838            # expect an exception
1839            invalidate_accesscode(pin,comment)
1840            IUserAccount(student).setPassword(self.ac_number)
1841            student.adm_code = pin
1842        self.flash('Password has been set. Your Student Id is %s'
1843            % self.student_id)
1844        return
Note: See TracBrowser for help on using the repository browser.