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

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

Display carry_over field on pages.

  • Property svn:keywords set to Id
File size: 64.9 KB
RevLine 
[7191]1## $Id: browser.py 7663 2012-02-17 08:34:39Z 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)
[6621]43from waeup.sirp.widgets.datewidget import (
[6869]44    FriendlyDateWidget, FriendlyDateDisplayWidget,
45    FriendlyDatetimeDisplayWidget)
[6912]46from waeup.sirp.university.vocabularies import study_modes
[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()
344        return [dict(name='', title='No transition')] +[
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:
[7171]535            current_mode = study_modes.getTermByToken(
536                self.context.certificate.study_mode).title
537            return current_mode
538        return
[7642]539
[7171]540    @property
541    def department(self):
[7205]542        if self.context.certificate is not None:
[7171]543            return self.context.certificate.__parent__.__parent__
544        return
[6912]545
[7171]546    @property
547    def faculty(self):
[7205]548        if self.context.certificate is not None:
[7171]549            return self.context.certificate.__parent__.__parent__.__parent__
550        return
551
[7321]552class StudyCourseManageFormPage(SIRPEditFormPage):
[6649]553    """ Page to edit the student study course data
554    """
555    grok.context(IStudentStudyCourse)
[6775]556    grok.name('manage')
[7136]557    grok.require('waeup.manageStudent')
[6775]558    grok.template('studycoursemanagepage')
[6649]559    form_fields = grok.AutoFields(IStudentStudyCourse)
[6695]560    label = 'Manage study course'
[6649]561    pnav = 4
[6775]562    taboneactions = ['Save','Cancel']
563    tabtwoactions = ['Remove selected levels','Cancel']
564    tabthreeactions = ['Add study level']
[6649]565
[6775]566    def update(self):
567        super(StudyCourseManageFormPage, self).update()
568        tabs.need()
[7484]569        self.tab1 = self.tab2 = ''
570        qs = self.request.get('QUERY_STRING', '')
571        if not qs:
572            qs = 'tab1'
573        setattr(self, qs, 'active')
[7490]574        warning.need()
575        datatable.need()
576        return
[6775]577
[7459]578    @action('Save', style='primary')
[6761]579    def save(self, **data):
[6762]580        msave(self, **data)
[6761]581        return
582
[6775]583    @property
584    def level_dict(self):
585        studylevelsource = StudyLevelSource().factory
586        for code in studylevelsource.getValues(self.context):
587            title = studylevelsource.getTitle(self.context, code)
588            yield(dict(code=code, title=title))
589
[7459]590    @action('Add study level')
[6774]591    def addStudyLevel(self, **data):
[6775]592        level_code = self.request.form.get('addlevel', None)
[6774]593        studylevel = StudentStudyLevel()
[6775]594        studylevel.level = int(level_code)
595        try:
[6782]596            self.context.addStudentStudyLevel(
597                self.context.certificate,studylevel)
[7484]598            self.flash('Study level has been added.')
[6775]599        except KeyError:
600            self.flash('This level exists.')
[7484]601        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6774]602        return
603
[7329]604    @jsaction('Remove selected levels')
[6775]605    def delStudyLevels(self, **data):
606        form = self.request.form
607        if form.has_key('val_id'):
608            child_id = form['val_id']
609        else:
610            self.flash('No study level selected.')
[7484]611            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6775]612            return
613        if not isinstance(child_id, list):
614            child_id = [child_id]
615        deleted = []
616        for id in child_id:
617            try:
618                del self.context[id]
619                deleted.append(id)
620            except:
621                self.flash('Could not delete %s: %s: %s' % (
622                        id, sys.exc_info()[0], sys.exc_info()[1]))
623        if len(deleted):
624            self.flash('Successfully removed: %s' % ', '.join(deleted))
[7484]625        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6775]626        return
[6774]627
[7321]628class StudyLevelDisplayFormPage(SIRPDisplayFormPage):
[6774]629    """ Page to display student study levels
630    """
631    grok.context(IStudentStudyLevel)
632    grok.name('index')
633    grok.require('waeup.viewStudent')
[6775]634    form_fields = grok.AutoFields(IStudentStudyLevel)
[6783]635    grok.template('studylevelpage')
[6774]636    pnav = 4
637
[7310]638    def update(self):
639        super(StudyLevelDisplayFormPage, self).update()
640        datatable.need()
641        return
642
[6774]643    @property
644    def label(self):
[6776]645        return '%s: Study Level %s' % (
[7364]646            self.context.getStudent().display_fullname,self.context.level_title)
[6774]647
[6803]648    @property
649    def total_credits(self):
650        total_credits = 0
651        for key, val in self.context.items():
652            total_credits += val.credits
653        return total_credits
654
[7459]655class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
[7028]656    """Deliver a PDF slip of the context.
657    """
658    grok.context(IStudentStudyLevel)
659    grok.name('course_registration.pdf')
660    grok.require('waeup.viewStudent')
661    form_fields = grok.AutoFields(IStudentStudyLevel)
662    prefix = 'form'
[7318]663    title = 'Level Data'
664    content_title = 'Course List'
[7028]665
666    @property
667    def label(self):
668        return 'Course Registration Slip %s' % self.context.level_title
669
670    def render(self):
671        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
672            self.request)
[7150]673        students_utils = getUtility(IStudentsUtils)
[7318]674        tabledata = sorted(self.context.values(),
675            key=lambda value: str(value.semester) + value.code)
[7186]676        return students_utils.renderPDF(
[7318]677            self, 'course_registration.pdf',
[7310]678            self.context.getStudent(), studentview,
679            tableheader=[('Sem.','semester', 1.5),('Code','code', 2.5),
680                         ('Title','title', 5),
681                         ('Dept.','dcode', 1.5), ('Faculty','fcode', 1.5),
682                         ('Cred.', 'credits', 1.5),
683                         ('Mand.', 'core_or_elective', 1.5),
684                         ('Score', 'score', 1.5),('Auto', 'automatic', 1.5)
[7304]685                         ],
[7318]686            tabledata=tabledata)
[7028]687
[7321]688class StudyLevelManageFormPage(SIRPEditFormPage):
[6792]689    """ Page to edit the student study level data
690    """
691    grok.context(IStudentStudyLevel)
692    grok.name('manage')
[7136]693    grok.require('waeup.manageStudent')
[6792]694    grok.template('studylevelmanagepage')
695    form_fields = grok.AutoFields(IStudentStudyLevel)
696    pnav = 4
697    taboneactions = ['Save','Cancel']
[6795]698    tabtwoactions = ['Add course ticket','Remove selected tickets','Cancel']
[6792]699
700    def update(self):
701        super(StudyLevelManageFormPage, self).update()
702        tabs.need()
[7484]703        self.tab1 = self.tab2 = ''
704        qs = self.request.get('QUERY_STRING', '')
705        if not qs:
706            qs = 'tab1'
707        setattr(self, qs, 'active')
[7490]708        warning.need()
709        datatable.need()
[6792]710        return
711
712    @property
713    def label(self):
714        return 'Manage study level %s' % self.context.level_title
715
[7459]716    @action('Save', style='primary')
[6792]717    def save(self, **data):
718        msave(self, **data)
719        return
720
[7459]721    @action('Add course ticket')
[6795]722    def addCourseTicket(self, **data):
723        self.redirect(self.url(self.context, '@@add'))
[6792]724
[7329]725    @jsaction('Remove selected tickets')
[6792]726    def delCourseTicket(self, **data):
727        form = self.request.form
728        if form.has_key('val_id'):
729            child_id = form['val_id']
730        else:
731            self.flash('No ticket selected.')
[7484]732            self.redirect(self.url(self.context, '@@manage')+'?tab2')
[6792]733            return
734        if not isinstance(child_id, list):
735            child_id = [child_id]
736        deleted = []
737        for id in child_id:
738            try:
739                del self.context[id]
740                deleted.append(id)
741            except:
742                self.flash('Could not delete %s: %s: %s' % (
743                        id, sys.exc_info()[0], sys.exc_info()[1]))
744        if len(deleted):
745            self.flash('Successfully removed: %s' % ', '.join(deleted))
[7484]746        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6792]747        return
748
[7459]749class ValidateCoursesPage(UtilityView, grok.View):
[7334]750    """ Validate course list by course adviser
751    """
752    grok.context(IStudentStudyLevel)
753    grok.name('validate_courses')
754    grok.require('waeup.validateStudent')
755
756    def update(self):
757        if str(self.context.__parent__.current_level) != self.context.__name__:
758            self.flash('This level does not correspond current level.')
759        elif self.context.getStudent().state == REGISTERED:
[7642]760            IWorkflowInfo(self.context.getStudent()).fireTransition(
761                'validate_courses')
[7337]762            self.flash('Course list has been validated.')
[7334]763        else:
764            self.flash('Student is in the wrong state.')
765        self.redirect(self.url(self.context))
766        return
767
768    def render(self):
769        return
770
[7459]771class RejectCoursesPage(UtilityView, grok.View):
[7334]772    """ Reject course list by course adviser
773    """
774    grok.context(IStudentStudyLevel)
775    grok.name('reject_courses')
776    grok.require('waeup.validateStudent')
777
778    def update(self):
779        if str(self.context.__parent__.current_level) != self.context.__name__:
780            self.flash('This level does not correspond current level.')
781            self.redirect(self.url(self.context))
782            return
783        elif self.context.getStudent().state == VALIDATED:
784            IWorkflowInfo(self.context.getStudent()).fireTransition('reset8')
785            message = 'Course list request has been annulled'
786            self.flash(message)
787        elif self.context.getStudent().state == REGISTERED:
788            IWorkflowInfo(self.context.getStudent()).fireTransition('reset7')
789            message = 'Course list request has been rejected'
790            self.flash(message)
791        else:
792            self.flash('Student is in the wrong state.')
793            self.redirect(self.url(self.context))
794            return
795        args = {'subject':message}
796        self.redirect(self.url(self.context.getStudent()) +
797            '/contactstudent?%s' % urlencode(args))
798        return
799
800    def render(self):
801        return
802
[7321]803class CourseTicketAddFormPage(SIRPAddFormPage):
[6808]804    """Add a course ticket.
[6795]805    """
806    grok.context(IStudentStudyLevel)
807    grok.name('add')
[7136]808    grok.require('waeup.manageStudent')
[6795]809    label = 'Add course ticket'
[6808]810    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[7663]811        'grade', 'score', 'automatic', 'carry_over')
[6795]812    pnav = 4
813
[7459]814    @action('Add course ticket')
[6795]815    def addCourseTicket(self, **data):
816        ticket = CourseTicket()
817        course = data['course']
[6802]818        ticket.automatic = False
[6795]819        ticket.code = course.code
820        ticket.title = course.title
[7304]821        ticket.fcode = course.__parent__.__parent__.__parent__.code
822        ticket.dcode = course.__parent__.__parent__.code
[6795]823        ticket.credits = course.credits
824        ticket.passmark = course.passmark
825        ticket.semester = course.semester
826        try:
827            self.context.addCourseTicket(ticket)
828        except KeyError:
829            self.flash('The ticket exists.')
830            return
[6799]831        self.flash('Successfully added %s.' % ticket.code)
[7484]832        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
[6795]833        return
834
[7459]835    @action('Cancel')
[6795]836    def cancel(self, **data):
837        self.redirect(self.url(self.context))
838
[7321]839class CourseTicketDisplayFormPage(SIRPDisplayFormPage):
[6796]840    """ Page to display course tickets
841    """
842    grok.context(ICourseTicket)
843    grok.name('index')
844    grok.require('waeup.viewStudent')
845    form_fields = grok.AutoFields(ICourseTicket)
846    grok.template('courseticketpage')
847    pnav = 4
848
849    @property
850    def label(self):
851        return '%s: Course Ticket %s' % (
[7364]852            self.context.getStudent().display_fullname,self.context.code)
[6796]853
[7321]854class CourseTicketManageFormPage(SIRPEditFormPage):
[6796]855    """ Page to manage course tickets
856    """
857    grok.context(ICourseTicket)
858    grok.name('manage')
[7136]859    grok.require('waeup.manageStudent')
[6796]860    form_fields = grok.AutoFields(ICourseTicket)
861    grok.template('courseticketmanagepage')
862    pnav = 4
863
864    @property
865    def label(self):
866        return 'Manage course ticket %s' % self.context.code
867
[7459]868    @action('Save', style='primary')
[6796]869    def save(self, **data):
870        msave(self, **data)
871        return
872
[7321]873class PaymentsManageFormPage(SIRPEditFormPage):
[6869]874    """ Page to manage the student payments
[7642]875
876    This manage form page is for both students and students officers.
[6869]877    """
878    grok.context(IStudentPaymentsContainer)
[6940]879    grok.name('index')
[7181]880    grok.require('waeup.payStudent')
[6869]881    form_fields = grok.AutoFields(IStudentPaymentsContainer)
882    grok.template('paymentsmanagepage')
883    pnav = 4
884
[6940]885    def unremovable(self, ticket):
[7251]886        usertype = getattr(self.request.principal, 'user_type', None)
887        if not usertype:
888            return False
889        return (self.request.principal.user_type == 'student' and ticket.r_code)
[6940]890
[6869]891    @property
892    def label(self):
[7364]893        return '%s: Payments' % self.context.__parent__.display_fullname
[6869]894
895    def update(self):
896        super(PaymentsManageFormPage, self).update()
897        datatable.need()
[7329]898        warning.need()
[6869]899        return
900
[7329]901    @jsaction('Remove selected tickets')
[6869]902    def delPaymentTicket(self, **data):
903        form = self.request.form
904        if form.has_key('val_id'):
905            child_id = form['val_id']
906        else:
907            self.flash('No payment selected.')
[6940]908            self.redirect(self.url(self.context))
[6869]909            return
910        if not isinstance(child_id, list):
911            child_id = [child_id]
912        deleted = []
913        for id in child_id:
[6992]914            # Students are not allowed to remove used payment tickets
[6940]915            if not self.unremovable(self.context[id]):
916                try:
917                    del self.context[id]
918                    deleted.append(id)
919                except:
920                    self.flash('Could not delete %s: %s: %s' % (
921                            id, sys.exc_info()[0], sys.exc_info()[1]))
[6869]922        if len(deleted):
923            self.flash('Successfully removed: %s' % ', '.join(deleted))
[6943]924            write_log_message(self,'removed: % s' % ', '.join(deleted))
[6940]925        self.redirect(self.url(self.context))
[6869]926        return
927
[7459]928    @action('Add online payment ticket')
[6869]929    def addPaymentTicket(self, **data):
930        self.redirect(self.url(self.context, '@@addop'))
931
[7321]932class OnlinePaymentAddFormPage(SIRPAddFormPage):
[6869]933    """ Page to add an online payment ticket
934    """
935    grok.context(IStudentPaymentsContainer)
936    grok.name('addop')
[7181]937    grok.require('waeup.payStudent')
[6877]938    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
[6869]939        'p_category')
940    label = 'Add online payment'
941    pnav = 4
[7642]942
[7459]943    @action('Create ticket', style='primary')
[6869]944    def createTicket(self, **data):
[7024]945        p_category = data['p_category']
946        student = self.context.__parent__
947        if p_category == 'bed_allocation' and student[
948            'studycourse'].current_session != grok.getSite()[
949            'configuration'].accommodation_session:
950                self.flash(
[7642]951                    'Your current session does not match '
952                    'accommodation session.')
[7024]953                self.redirect(self.url(self.context))
954                return
[7150]955        students_utils = getUtility(IStudentsUtils)
[7186]956        pay_details  = students_utils.getPaymentDetails(
[7024]957            p_category,student)
[6994]958        if pay_details['error']:
959            self.flash(pay_details['error'])
[6940]960            self.redirect(self.url(self.context))
[6920]961            return
[7025]962        p_item = pay_details['p_item']
963        p_session = pay_details['p_session']
964        for key in self.context.keys():
965            ticket = self.context[key]
[7248]966            if ticket.p_state == 'paid' and\
967               ticket.p_category == p_category and \
[7025]968               ticket.p_item == p_item and \
969               ticket.p_session == p_session:
970                  self.flash(
[7251]971                      'This type of payment has already been made.')
[7025]972                  self.redirect(self.url(self.context))
973                  return
974        payment = createObject(u'waeup.StudentOnlinePayment')
975        self.applyData(payment, **data)
976        timestamp = "%d" % int(time()*1000)
977        payment.p_id = "p%s" % timestamp
978        payment.p_item = p_item
979        payment.p_session = p_session
[6994]980        payment.amount_auth = pay_details['amount']
981        payment.surcharge_1 = pay_details['surcharge_1']
982        payment.surcharge_2 = pay_details['surcharge_2']
983        payment.surcharge_3 = pay_details['surcharge_3']
[6869]984        self.context[payment.p_id] = payment
985        self.flash('Payment ticket created.')
[6940]986        self.redirect(self.url(self.context))
[6869]987        return
988
[7321]989class OnlinePaymentDisplayFormPage(SIRPDisplayFormPage):
[6869]990    """ Page to view an online payment ticket
991    """
[6877]992    grok.context(IStudentOnlinePayment)
[6869]993    grok.name('index')
994    grok.require('waeup.viewStudent')
[6877]995    form_fields = grok.AutoFields(IStudentOnlinePayment)
[7642]996    form_fields[
997        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
998    form_fields[
999        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
[6869]1000    pnav = 4
1001
1002    @property
1003    def label(self):
1004        return '%s: Online Payment Ticket %s' % (
[7364]1005            self.context.getStudent().display_fullname,self.context.p_id)
[6869]1006
[7459]1007class OnlinePaymentCallbackPage(UtilityView, grok.View):
[6930]1008    """ Callback view
1009    """
1010    grok.context(IStudentOnlinePayment)
1011    grok.name('callback')
1012    grok.require('waeup.payStudent')
1013
1014    # This update method simulates a valid callback und must be
1015    # specified in the customization package. The parameters must be taken
1016    # from the incoming request.
1017    def update(self):
[7026]1018        if self.context.p_state == 'paid':
1019            self.flash('This ticket has already been paid.')
1020            return
[6936]1021        student = self.context.getStudent()
[6943]1022        write_log_message(self,'valid callback: %s' % self.context.p_id)
[6930]1023        self.context.r_amount_approved = self.context.amount_auth
1024        self.context.r_card_num = u'0000'
1025        self.context.r_code = u'00'
1026        self.context.p_state = 'paid'
1027        self.context.payment_date = datetime.now()
1028        if self.context.p_category == 'clearance':
[6936]1029            # Create CLR access code
[6937]1030            pin, error = create_accesscode('CLR',0,student.student_id)
[6936]1031            if error:
[6940]1032                self.flash('Valid callback received. ' + error)
[6936]1033                return
1034            self.context.ac = pin
[6930]1035        elif self.context.p_category == 'schoolfee':
[6936]1036            # Create SFE access code
[6940]1037            pin, error = create_accesscode('SFE',0,student.student_id)
[6936]1038            if error:
[6940]1039                self.flash('Valid callback received. ' + error)
[6936]1040                return
1041            self.context.ac = pin
[6994]1042        elif self.context.p_category == 'bed_allocation':
1043            # Create HOS access code
1044            pin, error = create_accesscode('HOS',0,student.student_id)
1045            if error:
1046                self.flash('Valid callback received. ' + error)
1047                return
1048            self.context.ac = pin
[6940]1049        self.flash('Valid callback received.')
1050        return
[6930]1051
1052    def render(self):
[6940]1053        self.redirect(self.url(self.context, '@@index'))
[6930]1054        return
1055
[7459]1056class ExportPDFPaymentSlipPage(UtilityView, grok.View):
[7019]1057    """Deliver a PDF slip of the context.
1058    """
1059    grok.context(IStudentOnlinePayment)
1060    grok.name('payment_receipt.pdf')
1061    grok.require('waeup.viewStudent')
1062    form_fields = grok.AutoFields(IStudentOnlinePayment)
1063    form_fields['creation_date'].custom_widget = FriendlyDateDisplayWidget('le')
1064    form_fields['payment_date'].custom_widget = FriendlyDateDisplayWidget('le')
1065    prefix = 'form'
[7318]1066    title = 'Payment Data'
[7019]1067
1068    @property
1069    def label(self):
1070        return 'Online Payment Receipt %s' % self.context.p_id
1071
1072    def render(self):
[7028]1073        if self.context.p_state != 'paid':
1074            self.flash('Ticket not yet paid.')
1075            self.redirect(self.url(self.context))
1076            return
[7019]1077        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1078            self.request)
[7150]1079        students_utils = getUtility(IStudentsUtils)
[7318]1080        return students_utils.renderPDF(self, 'payment_receipt.pdf',
[7310]1081            self.context.getStudent(), studentview)
[7019]1082
[6992]1083
[7321]1084class AccommodationManageFormPage(SIRPEditFormPage):
[7009]1085    """ Page to manage bed tickets.
[7642]1086
1087    This manage form page is for both students and students officers.
[6635]1088    """
1089    grok.context(IStudentAccommodation)
1090    grok.name('index')
[7181]1091    grok.require('waeup.handleAccommodation')
[6635]1092    form_fields = grok.AutoFields(IStudentAccommodation)
[6992]1093    grok.template('accommodationmanagepage')
[6642]1094    pnav = 4
[7017]1095    officers_only_actions = ['Remove selected']
[6635]1096
1097    @property
1098    def label(self):
[7364]1099        return '%s: Accommodation' % self.context.__parent__.display_fullname
[6637]1100
[6992]1101    def update(self):
1102        super(AccommodationManageFormPage, self).update()
1103        datatable.need()
[7329]1104        warning.need()
[6992]1105        return
1106
[7329]1107    @jsaction('Remove selected')
[7009]1108    def delBedTickets(self, **data):
[7240]1109        if getattr(self.request.principal, 'user_type', None) == 'student':
[7017]1110            self.flash('You are not allowed to remove bed tickets.')
1111            self.redirect(self.url(self.context))
1112            return
[6992]1113        form = self.request.form
1114        if form.has_key('val_id'):
1115            child_id = form['val_id']
1116        else:
1117            self.flash('No bed ticket selected.')
1118            self.redirect(self.url(self.context))
1119            return
1120        if not isinstance(child_id, list):
1121            child_id = [child_id]
1122        deleted = []
1123        for id in child_id:
[7068]1124            del self.context[id]
1125            deleted.append(id)
[6992]1126        if len(deleted):
1127            self.flash('Successfully removed: %s' % ', '.join(deleted))
1128            write_log_message(self,'removed: % s' % ', '.join(deleted))
1129        self.redirect(self.url(self.context))
1130        return
1131
[7009]1132    @property
1133    def selected_actions(self):
[7240]1134        if getattr(self.request.principal, 'user_type', None) == 'student':
[7642]1135            return [action for action in self.actions
1136                    if not action.label in self.officers_only_actions]
1137        return self.actions
[7009]1138
[7321]1139class BedTicketAddPage(SIRPPage):
[6992]1140    """ Page to add an online payment ticket
1141    """
1142    grok.context(IStudentAccommodation)
1143    grok.name('add')
[7181]1144    grok.require('waeup.handleAccommodation')
[6992]1145    grok.template('enterpin')
[6993]1146    ac_prefix = 'HOS'
[6992]1147    label = 'Add bed ticket'
1148    pnav = 4
1149    buttonname = 'Create bed ticket'
[6993]1150    notice = ''
[6992]1151
1152    def update(self, SUBMIT=None):
[6996]1153        student = self.context.getStudent()
[7150]1154        students_utils = getUtility(IStudentsUtils)
[7186]1155        acc_details  = students_utils.getAccommodationDetails(student)
[7369]1156        if not acc_details:
1157            self.flash("Your data are incomplete.")
1158            self.redirect(self.url(self.context))
1159            return
[6996]1160        if not student.state in acc_details['allowed_states']:
[7015]1161            self.flash("You are in the wrong registration state.")
[6992]1162            self.redirect(self.url(self.context))
1163            return
[7642]1164        if student['studycourse'].current_session != acc_details[
1165            'booking_session']:
[7061]1166            self.flash(
1167                'Your current session does not match accommodation session.')
1168            self.redirect(self.url(self.context))
1169            return
1170        if str(acc_details['booking_session']) in self.context.keys():
[7642]1171            self.flash(
1172                'You already booked a bed space in current '
1173                'accommodation session.')
[7004]1174            self.redirect(self.url(self.context))
1175            return
[6992]1176        self.ac_series = self.request.form.get('ac_series', None)
1177        self.ac_number = self.request.form.get('ac_number', None)
1178        if SUBMIT is None:
1179            return
1180        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1181        code = get_access_code(pin)
1182        if not code:
1183            self.flash('Activation code is invalid.')
1184            return
[7060]1185        # Search and book bed
[6997]1186        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1187        entries = cat.searchResults(
[7003]1188            owner=(student.student_id,student.student_id))
1189        if len(entries):
[7060]1190            # If bed space has bee manually allocated use this bed
[7003]1191            bed = [entry for entry in entries][0]
[7060]1192        else:
1193            # else search for other available beds
1194            entries = cat.searchResults(
1195                bed_type=(acc_details['bt'],acc_details['bt']))
1196            available_beds = [
1197                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1198            if available_beds:
[7150]1199                students_utils = getUtility(IStudentsUtils)
[7186]1200                bed = students_utils.selectBed(available_beds)
[7060]1201                bed.bookBed(student.student_id)
1202            else:
1203                self.flash('There is no free bed in your category %s.'
1204                            % acc_details['bt'])
1205                return
[6992]1206        # Mark pin as used (this also fires a pin related transition)
1207        if code.state == USED:
1208            self.flash('Activation code has already been used.')
1209            return
1210        else:
[7642]1211            comment = u"AC invalidated for %s" % (
1212                self.context.getStudent().student_id,)
[6992]1213            # Here we know that the ac is in state initialized so we do not
1214            # expect an exception, but the owner might be different
1215            if not invalidate_accesscode(
1216                pin,comment,self.context.getStudent().student_id):
1217                self.flash('You are not the owner of this access code.')
1218                return
[7060]1219        # Create bed ticket
[6992]1220        bedticket = createObject(u'waeup.BedTicket')
1221        bedticket.booking_code = pin
[6994]1222        bedticket.booking_session = acc_details['booking_session']
[6996]1223        bedticket.bed_type = acc_details['bt']
[7006]1224        bedticket.bed = bed
[6996]1225        hall_title = bed.__parent__.hostel_name
1226        coordinates = bed.getBedCoordinates()[1:]
1227        block, room_nr, bed_nr = coordinates
[7068]1228        bedticket.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1229            hall_title, block, room_nr, bed_nr, bed.bed_type)
[6996]1230        key = str(acc_details['booking_session'])
1231        self.context[key] = bedticket
1232        self.flash('Bed ticket created and bed booked: %s'
1233            % bedticket.bed_coordinates)
[6992]1234        self.redirect(self.url(self.context))
1235        return
1236
[7321]1237class BedTicketDisplayFormPage(SIRPDisplayFormPage):
[6994]1238    """ Page to display bed tickets
1239    """
1240    grok.context(IBedTicket)
1241    grok.name('index')
[7181]1242    grok.require('waeup.handleAccommodation')
[6994]1243    form_fields = grok.AutoFields(IBedTicket)
1244    form_fields[
1245        'booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1246    pnav = 4
1247
1248    @property
1249    def label(self):
1250        return 'Bed Ticket for Session %s' % self.context.getSessionString()
1251
[7459]1252class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
[7027]1253    """Deliver a PDF slip of the context.
1254    """
1255    grok.context(IBedTicket)
1256    grok.name('bed_allocation.pdf')
[7181]1257    grok.require('waeup.handleAccommodation')
[7027]1258    form_fields = grok.AutoFields(IBedTicket)
1259    form_fields['booking_date'].custom_widget = FriendlyDateDisplayWidget('le')
1260    prefix = 'form'
[7318]1261    title = 'Bed Allocation Data'
[7027]1262
1263    @property
1264    def label(self):
[7318]1265        return 'Bed Allocation: %s' % self.context.bed_coordinates
[7027]1266
1267    def render(self):
1268        studentview = StudentBaseDisplayFormPage(self.context.getStudent(),
1269            self.request)
[7150]1270        students_utils = getUtility(IStudentsUtils)
[7186]1271        return students_utils.renderPDF(
[7318]1272            self, 'bed_allocation.pdf',
[7310]1273            self.context.getStudent(), studentview)
[7027]1274
[7459]1275class BedTicketRelocationPage(UtilityView, grok.View):
[7015]1276    """ Callback view
1277    """
1278    grok.context(IBedTicket)
1279    grok.name('relocate')
1280    grok.require('waeup.manageHostels')
1281
[7059]1282    # Relocate student if student parameters have changed or the bed_type
1283    # of the bed has changed
[7015]1284    def update(self):
1285        student = self.context.getStudent()
[7150]1286        students_utils = getUtility(IStudentsUtils)
[7186]1287        acc_details  = students_utils.getAccommodationDetails(student)
[7068]1288        if self.context.bed != None and \
1289              'reserved' in self.context.bed.bed_type:
1290            self.flash("Students in reserved beds can't be relocated.")
1291            self.redirect(self.url(self.context))
1292            return
[7059]1293        if acc_details['bt'] == self.context.bed_type and \
[7068]1294                self.context.bed != None and \
[7059]1295                self.context.bed.bed_type == self.context.bed_type:
[7068]1296            self.flash("Student can't be relocated.")
1297            self.redirect(self.url(self.context))
[7015]1298            return
[7068]1299        # Search a bed
[7015]1300        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1301        entries = cat.searchResults(
[7068]1302            owner=(student.student_id,student.student_id))
1303        if len(entries) and self.context.bed == None:
1304            # If booking has been cancelled but other bed space has been
1305            # manually allocated after cancellation use this bed
1306            new_bed = [entry for entry in entries][0]
1307        else:
1308            # Search for other available beds
1309            entries = cat.searchResults(
1310                bed_type=(acc_details['bt'],acc_details['bt']))
1311            available_beds = [
1312                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1313            if available_beds:
[7150]1314                students_utils = getUtility(IStudentsUtils)
[7186]1315                new_bed = students_utils.selectBed(available_beds)
[7068]1316                new_bed.bookBed(student.student_id)
1317            else:
1318                self.flash('There is no free bed in your category %s.'
1319                            % acc_details['bt'])
1320                self.redirect(self.url(self.context))
1321                return
[7642]1322        # Release old bed if exists
[7068]1323        if self.context.bed != None:
1324            self.context.bed.owner = NOT_OCCUPIED
1325            notify(grok.ObjectModifiedEvent(self.context.bed))
[7015]1326        # Alocate new bed
1327        self.context.bed_type = acc_details['bt']
[7068]1328        self.context.bed = new_bed
1329        hall_title = new_bed.__parent__.hostel_name
1330        coordinates = new_bed.getBedCoordinates()[1:]
[7015]1331        block, room_nr, bed_nr = coordinates
[7068]1332        self.context.bed_coordinates = '%s, Block %s, Room %s, Bed %s (%s)' % (
1333            hall_title, block, room_nr, bed_nr, new_bed.bed_type)
1334        self.flash('Student relocated: %s' % self.context.bed_coordinates)
[7015]1335        self.redirect(self.url(self.context))
1336        return
1337
1338    def render(self):
1339        return
1340
[7321]1341class StudentHistoryPage(SIRPPage):
[6637]1342    """ Page to display student clearance data
1343    """
1344    grok.context(IStudent)
1345    grok.name('history')
[6660]1346    grok.require('waeup.viewStudent')
[6637]1347    grok.template('studenthistory')
[6642]1348    pnav = 4
[6637]1349
1350    @property
1351    def label(self):
[7364]1352        return '%s: History' % self.context.display_fullname
[6694]1353
1354# Pages for students only
1355
[7321]1356class StudentBaseEditFormPage(SIRPEditFormPage):
[7133]1357    """ View to edit student base data
1358    """
1359    grok.context(IStudent)
1360    grok.name('edit_base')
1361    grok.require('waeup.handleStudent')
1362    form_fields = grok.AutoFields(IStudentBase).select(
1363        'email', 'phone')
1364    label = 'Edit base data'
1365    pnav = 4
1366
[7459]1367    @action('Save', style='primary')
[7133]1368    def save(self, **data):
1369        msave(self, **data)
1370        return
1371
[7321]1372class StudentChangePasswordPage(SIRPEditFormPage):
[7144]1373    """ View to manage student base data
[6756]1374    """
1375    grok.context(IStudent)
[7114]1376    grok.name('change_password')
[6694]1377    grok.require('waeup.handleStudent')
[7144]1378    grok.template('change_password')
[6694]1379    label = 'Change password'
1380    pnav = 4
1381
[7459]1382    @action('Save', style='primary')
[7144]1383    def save(self, **data):
1384        form = self.request.form
1385        password = form.get('change_password', None)
1386        password_ctl = form.get('change_password_repeat', None)
1387        if password:
[7147]1388            validator = getUtility(IPasswordValidator)
1389            errors = validator.validate_password(password, password_ctl)
1390            if not errors:
1391                IUserAccount(self.context).setPassword(password)
1392                write_log_message(self, 'saved: password')
1393                self.flash('Password changed.')
[6756]1394            else:
[7147]1395                self.flash( ' '.join(errors))
[6756]1396        return
1397
[7321]1398class StudentFilesUploadPage(SIRPPage):
[7114]1399    """ View to upload files by student
1400    """
1401    grok.context(IStudent)
1402    grok.name('change_portrait')
[7127]1403    grok.require('waeup.uploadStudentFile')
[7114]1404    grok.template('filesuploadpage')
1405    label = 'Upload portrait'
1406    pnav = 4
1407
[7133]1408    def update(self):
[7539]1409        if self.context.getStudent().state != ADMITTED:
[7145]1410            emit_lock_message(self)
[7133]1411            return
1412        super(StudentFilesUploadPage, self).update()
1413        return
1414
[7321]1415class StartClearancePage(SIRPPage):
[6770]1416    grok.context(IStudent)
1417    grok.name('start_clearance')
1418    grok.require('waeup.handleStudent')
1419    grok.template('enterpin')
1420    label = 'Start clearance'
1421    ac_prefix = 'CLR'
1422    notice = ''
1423    pnav = 4
1424    buttonname = 'Start clearance now'
1425
[7133]1426    @property
1427    def all_required_fields_filled(self):
1428        if self.context.email and self.context.phone:
1429            return True
1430        return False
1431
1432    @property
1433    def portrait_uploaded(self):
1434        store = getUtility(IExtFileStore)
1435        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1436            return True
1437        return False
1438
[6770]1439    def update(self, SUBMIT=None):
[6936]1440        if not self.context.state == 'admitted':
1441            self.flash("Wrong state.")
1442            self.redirect(self.url(self.context))
1443            return
[7133]1444        if not self.portrait_uploaded:
1445            self.flash("No portrait uploaded.")
1446            self.redirect(self.url(self.context, 'change_portrait'))
1447            return
1448        if not self.all_required_fields_filled:
1449            self.flash("Not all required fields filled.")
1450            self.redirect(self.url(self.context, 'edit_base'))
1451            return
[6770]1452        self.ac_series = self.request.form.get('ac_series', None)
1453        self.ac_number = self.request.form.get('ac_number', None)
1454
1455        if SUBMIT is None:
1456            return
1457        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1458        code = get_access_code(pin)
1459        if not code:
[6936]1460            self.flash('Activation code is invalid.')
[6770]1461            return
1462        # Mark pin as used (this also fires a pin related transition)
1463        # and fire transition start_clearance
1464        if code.state == USED:
[6936]1465            self.flash('Activation code has already been used.')
[6770]1466            return
1467        else:
1468            comment = u"AC invalidated for %s" % self.context.student_id
1469            # Here we know that the ac is in state initialized so we do not
[6927]1470            # expect an exception, but the owner might be different
1471            if not invalidate_accesscode(pin,comment,self.context.student_id):
1472                self.flash('You are not the owner of this access code.')
1473                return
[6770]1474            self.context.clr_code = pin
1475        IWorkflowInfo(self.context).fireTransition('start_clearance')
[6771]1476        self.flash('Clearance process has been started.')
[6770]1477        self.redirect(self.url(self.context,'cedit'))
1478        return
1479
[6695]1480class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
1481    """ View to edit student clearance data by student
1482    """
1483    grok.context(IStudent)
1484    grok.name('cedit')
1485    grok.require('waeup.handleStudent')
[6756]1486    form_fields = grok.AutoFields(
[7538]1487        IStudentClearance).omit('clearance_locked')
[6722]1488    label = 'Edit clearance data'
[6695]1489    form_fields['date_of_birth'].custom_widget = FriendlyDateWidget('le-year')
[6718]1490
1491    def update(self):
1492        if self.context.clearance_locked:
[7145]1493            emit_lock_message(self)
[6718]1494            return
1495        return super(StudentClearanceEditFormPage, self).update()
[6719]1496
[7459]1497    @action('Save', style='primary')
[6722]1498    def save(self, **data):
1499        self.applyData(self.context, **data)
[6771]1500        self.flash('Clearance form has been saved.')
[6722]1501        return
1502
[7253]1503    def dataNotComplete(self):
[7642]1504        """To be implemented in the customization package.
1505        """
[7253]1506        return False
1507
[7459]1508    @action('Save and request clearance', style='primary')
[7186]1509    def requestClearance(self, **data):
[6722]1510        self.applyData(self.context, **data)
[7253]1511        if self.dataNotComplete():
1512            self.flash(self.dataNotComplete())
1513            return
[6771]1514        self.flash('Clearance form has been saved.')
[6769]1515        self.redirect(self.url(self.context,'request_clearance'))
[6722]1516        return
1517
[7321]1518class RequestClearancePage(SIRPPage):
[6769]1519    grok.context(IStudent)
1520    grok.name('request_clearance')
1521    grok.require('waeup.handleStudent')
1522    grok.template('enterpin')
1523    label = 'Request clearance'
1524    notice = 'Enter the CLR access code used for starting clearance.'
1525    ac_prefix = 'CLR'
1526    pnav = 4
1527    buttonname = 'Request clearance now'
1528
1529    def update(self, SUBMIT=None):
1530        self.ac_series = self.request.form.get('ac_series', None)
1531        self.ac_number = self.request.form.get('ac_number', None)
1532        if SUBMIT is None:
1533            return
1534        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1535        if self.context.clr_code != pin:
1536            self.flash("This isn't your CLR access code.")
1537            return
1538        state = IWorkflowState(self.context).getState()
1539        # This shouldn't happen, but the application officer
1540        # might have forgotten to lock the form after changing the state
1541        if state != CLEARANCE:
1542            self.flash('This form cannot be submitted. Wrong state!')
1543            return
1544        IWorkflowInfo(self.context).fireTransition('request_clearance')
1545        self.flash('Clearance has been requested.')
1546        self.redirect(self.url(self.context))
[6789]1547        return
[6806]1548
[7321]1549class StartCourseRegistrationPage(SIRPPage):
[6944]1550    grok.context(IStudentStudyCourse)
1551    grok.name('start_course_registration')
1552    grok.require('waeup.handleStudent')
1553    grok.template('enterpin')
1554    label = 'Start course registration'
1555    ac_prefix = 'SFE'
1556    notice = ''
1557    pnav = 4
1558    buttonname = 'Start course registration now'
1559
1560    def update(self, SUBMIT=None):
1561        if not self.context.getStudent().state in (CLEARED,RETURNING):
1562            self.flash("Wrong state.")
1563            self.redirect(self.url(self.context))
1564            return
1565        self.ac_series = self.request.form.get('ac_series', None)
1566        self.ac_number = self.request.form.get('ac_number', None)
1567
1568        if SUBMIT is None:
1569            return
1570        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1571        code = get_access_code(pin)
1572        if not code:
1573            self.flash('Activation code is invalid.')
1574            return
1575        # Mark pin as used (this also fires a pin related transition)
1576        # and fire transition start_clearance
1577        if code.state == USED:
1578            self.flash('Activation code has already been used.')
1579            return
1580        else:
[7642]1581            comment = u"AC invalidated for %s" % (
1582                self.context.getStudent().student_id,)
[6944]1583            # Here we know that the ac is in state initialized so we do not
1584            # expect an exception, but the owner might be different
1585            if not invalidate_accesscode(
1586                pin,comment,self.context.getStudent().student_id):
1587                self.flash('You are not the owner of this access code.')
1588                return
1589        if self.context.getStudent().state == CLEARED:
1590            IWorkflowInfo(self.context.getStudent()).fireTransition(
1591                'pay_first_school_fee')
1592        elif self.context.getStudent().state == RETURNING:
1593            IWorkflowInfo(self.context.getStudent()).fireTransition(
1594                'pay_school_fee')
1595        self.flash('Course registration has been started.')
1596        self.redirect(self.url(self.context))
1597        return
1598
[7321]1599class AddStudyLevelFormPage(SIRPEditFormPage):
[6806]1600    """ Page for students to add current study levels
1601    """
1602    grok.context(IStudentStudyCourse)
1603    grok.name('add')
1604    grok.require('waeup.handleStudent')
1605    grok.template('studyleveladdpage')
1606    form_fields = grok.AutoFields(IStudentStudyCourse)
1607    pnav = 4
1608
1609    @property
1610    def label(self):
1611        studylevelsource = StudyLevelSource().factory
1612        code = self.context.current_level
1613        title = studylevelsource.getTitle(self.context, code)
1614        return 'Add current level %s' % title
1615
1616    def update(self):
[7539]1617        if self.context.getStudent().state != PAID:
[7145]1618            emit_lock_message(self)
[6806]1619            return
1620        super(AddStudyLevelFormPage, self).update()
1621        return
1622
[7459]1623    @action('Create course list now', style='primary')
[6806]1624    def addStudyLevel(self, **data):
1625        studylevel = StudentStudyLevel()
1626        studylevel.level = self.context.current_level
1627        studylevel.level_session = self.context.current_session
1628        try:
1629            self.context.addStudentStudyLevel(
1630                self.context.certificate,studylevel)
1631        except KeyError:
1632            self.flash('This level exists.')
1633        self.redirect(self.url(self.context))
1634        return
[6808]1635
[7321]1636class StudyLevelEditFormPage(SIRPEditFormPage):
[6808]1637    """ Page to edit the student study level data by students
1638    """
1639    grok.context(IStudentStudyLevel)
1640    grok.name('edit')
1641    grok.require('waeup.handleStudent')
1642    grok.template('studyleveleditpage')
1643    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1644        'level_session', 'level_verdict')
1645    pnav = 4
1646
1647    def update(self):
[7539]1648        if self.context.getStudent().state != PAID:
1649            emit_lock_message(self)
1650            return
[6808]1651        super(StudyLevelEditFormPage, self).update()
1652        datatable.need()
[7329]1653        warning.need()
[6808]1654        return
1655
1656    @property
1657    def label(self):
[7642]1658        return 'Add and remove course tickets of study level %s' % (
1659            self.context.level_title,)
[6808]1660
1661    @property
1662    def total_credits(self):
1663        total_credits = 0
1664        for key, val in self.context.items():
1665            total_credits += val.credits
1666        return total_credits
1667
[7459]1668    @action('Add course ticket')
[6808]1669    def addCourseTicket(self, **data):
1670        self.redirect(self.url(self.context, 'ctadd'))
1671
[7329]1672    @jsaction('Remove selected tickets')
[6808]1673    def delCourseTicket(self, **data):
1674        form = self.request.form
1675        if form.has_key('val_id'):
1676            child_id = form['val_id']
1677        else:
1678            self.flash('No ticket selected.')
1679            self.redirect(self.url(self.context, '@@edit'))
1680            return
1681        if not isinstance(child_id, list):
1682            child_id = [child_id]
1683        deleted = []
1684        for id in child_id:
[6940]1685            # Students are not allowed to remove core tickets
[6808]1686            if not self.context[id].core_or_elective:
1687                try:
1688                    del self.context[id]
1689                    deleted.append(id)
1690                except:
1691                    self.flash('Could not delete %s: %s: %s' % (
1692                            id, sys.exc_info()[0], sys.exc_info()[1]))
1693        if len(deleted):
1694            self.flash('Successfully removed: %s' % ', '.join(deleted))
1695        self.redirect(self.url(self.context, u'@@edit'))
1696        return
1697
[7459]1698    @action('Register course list', style='primary')
[7186]1699    def RegisterCourses(self, **data):
[7642]1700        IWorkflowInfo(self.context.getStudent()).fireTransition(
1701            'register_courses')
[6810]1702        self.flash('Course list has been registered.')
1703        self.redirect(self.url(self.context))
1704        return
1705
[6808]1706class CourseTicketAddFormPage2(CourseTicketAddFormPage):
1707    """Add a course ticket by student.
1708    """
1709    grok.name('ctadd')
1710    grok.require('waeup.handleStudent')
1711    form_fields = grok.AutoFields(ICourseTicketAdd).omit(
[7663]1712        'grade', 'score', 'core_or_elective', 'automatic', 'carry_over')
[6808]1713
[7539]1714    def update(self):
1715        if self.context.getStudent().state != PAID:
1716            emit_lock_message(self)
1717            return
1718        super(CourseTicketAddFormPage2, self).update()
1719        return
1720
[7459]1721    @action('Add course ticket')
[6808]1722    def addCourseTicket(self, **data):
[7642]1723        # Safety belt
[7539]1724        if self.context.getStudent().state != PAID:
1725            return
[6808]1726        ticket = CourseTicket()
1727        course = data['course']
[7642]1728        for name in ['code', 'title', 'credits', 'passmark', 'semester']:
1729            setattr(ticket, name, getattr(course, name))
[6808]1730        ticket.automatic = False
1731        try:
1732            self.context.addCourseTicket(ticket)
1733        except KeyError:
1734            self.flash('The ticket exists.')
1735            return
1736        self.flash('Successfully added %s.' % ticket.code)
1737        self.redirect(self.url(self.context, u'@@edit'))
1738        return
[7369]1739
1740class ChangePasswordRequestPage(SIRPForm):
1741    """Captcha'd page for students to request a password change.
1742    """
1743    grok.context(IUniversity)
1744    grok.name('changepw')
1745    grok.require('waeup.Anonymous')
1746    grok.template('changepw')
1747    label = 'Change my password'
1748    form_fields = grok.AutoFields(IStudentChangePassword)
1749
1750    def update(self):
1751        # Handle captcha
1752        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1753        self.captcha_result = self.captcha.verify(self.request)
1754        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1755        return
1756
[7462]1757    @action('Get new login credentials', style='primary')
[7369]1758    def request(self, **data):
1759        if not self.captcha_result.is_valid:
1760            # Captcha will display error messages automatically.
1761            # No need to flash something.
1762            return
1763        # Search student
1764        cat = queryUtility(ICatalog, name='students_catalog')
1765        reg_number = data['reg_number']
1766        email = data['email']
1767        results = cat.searchResults(
1768            reg_number=(reg_number, reg_number),
1769            email=(email,email))
1770        if len(results) == 0:
1771            self.flash('No student record found.')
1772            return
1773        student = list(results)[0]
1774        # Change password
1775        sirp_utils = getUtility(ISIRPUtils)
1776        pwd = sirp_utils.genPassword()
1777        IUserAccount(student).setPassword(pwd)
1778        # Send email with new redentials
1779        msg = 'You have successfully changed your password for the'
1780        login_url = self.url(grok.getSite(), 'login')
[7407]1781        success = sirp_utils.sendCredentials(
1782            IUserAccount(student),pwd,login_url,msg)
[7369]1783        if success:
1784            self.flash('An email with your user name and password ' +
[7407]1785                'has been sent to %s.' % email)
[7369]1786        else:
1787            self.flash('An smtp server error occurred.')
[7638]1788        return
1789
[7660]1790class SetPasswordPage(SIRPPage):
1791    grok.context(ISIRPObject)
1792    grok.name('setpassword')
1793    grok.require('waeup.Anonymous')
1794    grok.template('setpassword')
1795    label = 'Set password for first-time login'
1796    ac_prefix = 'PWD'
1797    pnav = 0
1798
1799    def update(self, SUBMIT=None):
1800        self.reg_number = self.request.form.get('reg_number', None)
1801        self.ac_series = self.request.form.get('ac_series', None)
1802        self.ac_number = self.request.form.get('ac_number', None)
1803
1804        if SUBMIT is None:
1805            return
1806        hitlist = search(query=self.reg_number,
1807            searchtype='reg_number', view=self)
1808        if not hitlist:
1809            self.flash('No student found.')
1810            return
1811        if len(hitlist) != 1:   # Cannot happen but anyway
1812            self.flash('More than one student found.')
1813            return
1814        student = hitlist[0].context
1815        self.student_id = student.student_id
1816        student_pw = student.password
1817        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1818        code = get_access_code(pin)
1819        if not code:
1820            self.flash('Access code is invalid.')
1821            return
1822        if student_pw and pin == student.adm_code:
1823            self.flash('Password has already been set. Your Student Id is %s'
1824                % self.student_id)
1825            return
1826        elif student_pw:
1827            self.flash(
1828                'Password has already been set. You are using the '
1829                'wrong Access Code.')
1830            return
1831        # Mark pin as used (this also fires a pin related transition)
1832        # and set student password
1833        if code.state == USED:
1834            self.flash('Access code has already been used.')
1835            return
1836        else:
1837            comment = u"AC invalidated for %s" % self.student_id
1838            # Here we know that the ac is in state initialized so we do not
1839            # expect an exception
1840            invalidate_accesscode(pin,comment)
1841            IUserAccount(student).setPassword(self.ac_number)
1842            student.adm_code = pin
1843        self.flash('Password has been set. Your Student Id is %s'
1844            % self.student_id)
1845        return
Note: See TracBrowser for help on using the repository browser.