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

Last change on this file since 10577 was 10531, checked in by Henrik Bettermann, 11 years ago

Allow signature tables in transcripts.

  • Property svn:keywords set to Id
File size: 119.1 KB
Line 
1## $Id: browser.py 10531 2013-08-24 06:41:02Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import sys
21import grok
22import pytz
23from urllib import urlencode
24from datetime import datetime
25from zope.event import notify
26from zope.i18n import translate
27from zope.catalog.interfaces import ICatalog
28from zope.component import queryUtility, getUtility, createObject
29from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
30from zope.formlib.textwidgets import BytesDisplayWidget
31from zope.security import checkPermission
32from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
33from waeup.kofa.accesscodes import (
34    invalidate_accesscode, get_access_code)
35from waeup.kofa.accesscodes.workflow import USED
36from waeup.kofa.browser.layout import (
37    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
38    KofaForm, NullValidator)
39from waeup.kofa.browser.breadcrumbs import Breadcrumb
40from waeup.kofa.browser.pages import ContactAdminForm, ExportCSVView, doll_up
41from waeup.kofa.browser.resources import (
42    datepicker, datatable, tabs, warning, toggleall)
43from waeup.kofa.browser.layout import jsaction, action, UtilityView
44from waeup.kofa.browser.interfaces import ICaptchaManager
45from waeup.kofa.hostels.hostel import NOT_OCCUPIED
46from waeup.kofa.interfaces import (
47    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
48    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
49    academic_sessions_vocab, IJobManager, IDataCenter)
50from waeup.kofa.interfaces import MessageFactory as _
51from waeup.kofa.widgets.datewidget import (
52    FriendlyDateWidget, FriendlyDateDisplayWidget,
53    FriendlyDatetimeDisplayWidget)
54from waeup.kofa.mandates.mandate import PasswordMandate
55from waeup.kofa.university.interfaces import (
56    IDepartment, ICertificate, ICourse)
57from waeup.kofa.university.department import (
58    VirtualDepartmentExportJobContainer,)
59from waeup.kofa.university.facultiescontainer import (
60    VirtualFacultiesExportJobContainer, FacultiesContainer)
61from waeup.kofa.university.certificate import (
62    VirtualCertificateExportJobContainer,)
63from waeup.kofa.university.course import (
64    VirtualCourseExportJobContainer,)
65from waeup.kofa.university.vocabularies import course_levels
66from waeup.kofa.utils.batching import VirtualExportJobContainer
67from waeup.kofa.utils.helpers import get_current_principal, to_timezone, now
68from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
69from waeup.kofa.students.interfaces import (
70    IStudentsContainer, IStudent,
71    IUGStudentClearance,IPGStudentClearance,
72    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
73    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
74    IStudentAccommodation, IStudentStudyLevel,
75    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
76    IStudentOnlinePayment, IStudentPreviousPayment, IStudentBalancePayment,
77    IBedTicket, IStudentsUtils, IStudentRequestPW, IStudentTranscript
78    )
79from waeup.kofa.students.catalog import search, StudentQueryResultItem
80from waeup.kofa.students.export import EXPORTER_NAMES
81from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
82from waeup.kofa.students.vocabularies import StudyLevelSource
83from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
84    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
85    GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS)
86
87
88grok.context(IKofaObject) # Make IKofaObject the default context
89
90# Save function used for save methods in pages
91def msave(view, **data):
92    changed_fields = view.applyData(view.context, **data)
93    # Turn list of lists into single list
94    if changed_fields:
95        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
96    # Inform catalog if certificate has changed
97    # (applyData does this only for the context)
98    if 'certificate' in changed_fields:
99        notify(grok.ObjectModifiedEvent(view.context.student))
100    fields_string = ' + '.join(changed_fields)
101    view.flash(_('Form has been saved.'))
102    if fields_string:
103        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
104    return
105
106def emit_lock_message(view):
107    """Flash a lock message.
108    """
109    view.flash(_('The requested form is locked (read-only).'))
110    view.redirect(view.url(view.context))
111    return
112
113def translated_values(view):
114    """Translate course ticket attribute values to be displayed on
115    studylevel pages.
116    """
117    lang = view.request.cookies.get('kofa.language')
118    for value in view.context.values():
119        # We have to unghostify (according to Tres Seaver) the __dict__
120        # by activating the object, otherwise value_dict will be empty
121        # when calling the first time.
122        value._p_activate()
123        value_dict = dict([i for i in value.__dict__.items()])
124        value_dict['removable_by_student'] = value.removable_by_student
125        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
126            target_language=lang)
127        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
128            target_language=lang)
129        value_dict['automatic'] = translate(str(value.automatic), 'zope',
130            target_language=lang)
131        value_dict['grade'] = value.grade
132        value_dict['weight'] = value.weight
133        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
134        value_dict['semester'] = semester_dict[
135            value.semester].replace('mester', 'm.')
136        yield value_dict
137
138def clearance_disabled_message(student):
139    try:
140        session_config = grok.getSite()[
141            'configuration'][str(student.current_session)]
142    except KeyError:
143        return _('Session configuration object is not available.')
144    if not session_config.clearance_enabled:
145        return _('Clearance is disabled for this session.')
146    return None
147
148
149def addCourseTicket(view, course=None):
150    students_utils = getUtility(IStudentsUtils)
151    ticket = createObject(u'waeup.CourseTicket')
152    ticket.automatic = False
153    ticket.carry_over = False
154    max_credits = students_utils.maxCreditsExceeded(view.context, course)
155    if max_credits:
156        view.flash(_(
157            'Total credits exceed ${a}.',
158            mapping = {'a': max_credits}))
159        return False
160    try:
161        view.context.addCourseTicket(ticket, course)
162    except KeyError:
163        view.flash(_('The ticket exists.'))
164        return False
165    view.flash(_('Successfully added ${a}.',
166        mapping = {'a':ticket.code}))
167    view.context.writeLogMessage(
168        view,'added: %s|%s|%s' % (
169        ticket.code, ticket.level, ticket.level_session))
170    return True
171
172def level_dict(studycourse):
173    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
174    level_dict = {}
175    studylevelsource = StudyLevelSource().factory
176    for code in studylevelsource.getValues(studycourse):
177        title = translate(studylevelsource.getTitle(studycourse, code),
178            'waeup.kofa', target_language=portal_language)
179        level_dict[code] = title
180    return level_dict
181
182class StudentsBreadcrumb(Breadcrumb):
183    """A breadcrumb for the students container.
184    """
185    grok.context(IStudentsContainer)
186    title = _('Students')
187
188    @property
189    def target(self):
190        user = get_current_principal()
191        if getattr(user, 'user_type', None) == 'student':
192            return None
193        return self.viewname
194
195class StudentBreadcrumb(Breadcrumb):
196    """A breadcrumb for the student container.
197    """
198    grok.context(IStudent)
199
200    def title(self):
201        return self.context.display_fullname
202
203class SudyCourseBreadcrumb(Breadcrumb):
204    """A breadcrumb for the student study course.
205    """
206    grok.context(IStudentStudyCourse)
207
208    def title(self):
209        if self.context.is_current:
210            return _('Study Course')
211        else:
212            return _('Previous Study Course')
213
214class PaymentsBreadcrumb(Breadcrumb):
215    """A breadcrumb for the student payments folder.
216    """
217    grok.context(IStudentPaymentsContainer)
218    title = _('Payments')
219
220class OnlinePaymentBreadcrumb(Breadcrumb):
221    """A breadcrumb for payments.
222    """
223    grok.context(IStudentOnlinePayment)
224
225    @property
226    def title(self):
227        return self.context.p_id
228
229class AccommodationBreadcrumb(Breadcrumb):
230    """A breadcrumb for the student accommodation folder.
231    """
232    grok.context(IStudentAccommodation)
233    title = _('Accommodation')
234
235class BedTicketBreadcrumb(Breadcrumb):
236    """A breadcrumb for bed tickets.
237    """
238    grok.context(IBedTicket)
239
240    @property
241    def title(self):
242        return _('Bed Ticket ${a}',
243            mapping = {'a':self.context.getSessionString()})
244
245class StudyLevelBreadcrumb(Breadcrumb):
246    """A breadcrumb for course lists.
247    """
248    grok.context(IStudentStudyLevel)
249
250    @property
251    def title(self):
252        return self.context.level_title
253
254class StudentsContainerPage(KofaPage):
255    """The standard view for student containers.
256    """
257    grok.context(IStudentsContainer)
258    grok.name('index')
259    grok.require('waeup.viewStudentsContainer')
260    grok.template('containerpage')
261    label = _('Student Section')
262    search_button = _('Search')
263    pnav = 4
264
265    def update(self, *args, **kw):
266        datatable.need()
267        form = self.request.form
268        self.hitlist = []
269        if form.get('searchtype', None) == 'suspended':
270            self.searchtype = form['searchtype']
271            self.searchterm = None
272        elif form.get('searchtype', None) == 'transcript':
273            self.searchtype = form['searchtype']
274            self.searchterm = None
275        elif 'searchterm' in form and form['searchterm']:
276            self.searchterm = form['searchterm']
277            self.searchtype = form['searchtype']
278        elif 'old_searchterm' in form:
279            self.searchterm = form['old_searchterm']
280            self.searchtype = form['old_searchtype']
281        else:
282            if 'search' in form:
283                self.flash(_('Empty search string'))
284            return
285        if self.searchtype == 'current_session':
286            try:
287                self.searchterm = int(self.searchterm)
288            except ValueError:
289                self.flash(_('Only year dates allowed (e.g. 2011).'))
290                return
291        self.hitlist = search(query=self.searchterm,
292            searchtype=self.searchtype, view=self)
293        if not self.hitlist:
294            self.flash(_('No student found.'))
295        return
296
297class StudentsContainerManagePage(KofaPage):
298    """The manage page for student containers.
299    """
300    grok.context(IStudentsContainer)
301    grok.name('manage')
302    grok.require('waeup.manageStudent')
303    grok.template('containermanagepage')
304    pnav = 4
305    label = _('Manage student section')
306    search_button = _('Search')
307    remove_button = _('Remove selected')
308
309    def update(self, *args, **kw):
310        datatable.need()
311        toggleall.need()
312        warning.need()
313        form = self.request.form
314        self.hitlist = []
315        if form.get('searchtype', None) == 'suspended':
316            self.searchtype = form['searchtype']
317            self.searchterm = None
318        elif 'searchterm' in form and form['searchterm']:
319            self.searchterm = form['searchterm']
320            self.searchtype = form['searchtype']
321        elif 'old_searchterm' in form:
322            self.searchterm = form['old_searchterm']
323            self.searchtype = form['old_searchtype']
324        else:
325            if 'search' in form:
326                self.flash(_('Empty search string'))
327            return
328        if self.searchtype == 'current_session':
329            try:
330                self.searchterm = int(self.searchterm)
331            except ValueError:
332                self.flash('Only year dates allowed (e.g. 2011).')
333                return
334        if not 'entries' in form:
335            self.hitlist = search(query=self.searchterm,
336                searchtype=self.searchtype, view=self)
337            if not self.hitlist:
338                self.flash(_('No student found.'))
339            if 'remove' in form:
340                self.flash(_('No item selected.'))
341            return
342        entries = form['entries']
343        if isinstance(entries, basestring):
344            entries = [entries]
345        deleted = []
346        for entry in entries:
347            if 'remove' in form:
348                del self.context[entry]
349                deleted.append(entry)
350        self.hitlist = search(query=self.searchterm,
351            searchtype=self.searchtype, view=self)
352        if len(deleted):
353            self.flash(_('Successfully removed: ${a}',
354                mapping = {'a':', '.join(deleted)}))
355        return
356
357class StudentAddFormPage(KofaAddFormPage):
358    """Add-form to add a student.
359    """
360    grok.context(IStudentsContainer)
361    grok.require('waeup.manageStudent')
362    grok.name('addstudent')
363    form_fields = grok.AutoFields(IStudent).select(
364        'firstname', 'middlename', 'lastname', 'reg_number')
365    label = _('Add student')
366    pnav = 4
367
368    @action(_('Create student record'), style='primary')
369    def addStudent(self, **data):
370        student = createObject(u'waeup.Student')
371        self.applyData(student, **data)
372        self.context.addStudent(student)
373        self.flash(_('Student record created.'))
374        self.redirect(self.url(self.context[student.student_id], 'index'))
375        return
376
377class LoginAsStudentStep1(KofaEditFormPage):
378    """ View to temporarily set a student password.
379    """
380    grok.context(IStudent)
381    grok.name('loginasstep1')
382    grok.require('waeup.loginAsStudent')
383    grok.template('loginasstep1')
384    pnav = 4
385
386    def label(self):
387        return _(u'Set temporary password for ${a}',
388            mapping = {'a':self.context.display_fullname})
389
390    @action('Set password now', style='primary')
391    def setPassword(self, *args, **data):
392        kofa_utils = getUtility(IKofaUtils)
393        password = kofa_utils.genPassword()
394        self.context.setTempPassword(self.request.principal.id, password)
395        self.context.writeLogMessage(
396            self, 'temp_password generated: %s' % password)
397        args = {'password':password}
398        self.redirect(self.url(self.context) +
399            '/loginasstep2?%s' % urlencode(args))
400        return
401
402class LoginAsStudentStep2(KofaPage):
403    """ View to temporarily login as student with a temporary password.
404    """
405    grok.context(IStudent)
406    grok.name('loginasstep2')
407    grok.require('waeup.Public')
408    grok.template('loginasstep2')
409    login_button = _('Login now')
410    pnav = 4
411
412    def label(self):
413        return _(u'Login as ${a}',
414            mapping = {'a':self.context.student_id})
415
416    def update(self, SUBMIT=None, password=None):
417        self.password = password
418        if SUBMIT is not None:
419            self.flash(_('You successfully logged in as student.'))
420            self.redirect(self.url(self.context))
421        return
422
423class StudentBaseDisplayFormPage(KofaDisplayFormPage):
424    """ Page to display student base data
425    """
426    grok.context(IStudent)
427    grok.name('index')
428    grok.require('waeup.viewStudent')
429    grok.template('basepage')
430    form_fields = grok.AutoFields(IStudentBase).omit(
431        'password', 'suspended', 'suspended_comment')
432    pnav = 4
433
434    @property
435    def label(self):
436        if self.context.suspended:
437            return _('${a}: Base Data (account deactivated)',
438                mapping = {'a':self.context.display_fullname})
439        return  _('${a}: Base Data',
440            mapping = {'a':self.context.display_fullname})
441
442    @property
443    def hasPassword(self):
444        if self.context.password:
445            return _('set')
446        return _('unset')
447
448class StudentBasePDFFormPage(KofaDisplayFormPage):
449    """ Page to display student base data in pdf files.
450    """
451
452    def __init__(self, context, request, omit_fields=()):
453        self.omit_fields = omit_fields
454        super(StudentBasePDFFormPage, self).__init__(context, request)
455
456    @property
457    def form_fields(self):
458        form_fields = grok.AutoFields(IStudentBase)
459        for field in self.omit_fields:
460            form_fields = form_fields.omit(field)
461        return form_fields
462
463class ContactStudentForm(ContactAdminForm):
464    grok.context(IStudent)
465    grok.name('contactstudent')
466    grok.require('waeup.viewStudent')
467    pnav = 4
468    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
469
470    def update(self, subject=u'', body=u''):
471        super(ContactStudentForm, self).update()
472        self.form_fields.get('subject').field.default = subject
473        self.form_fields.get('body').field.default = body
474        return
475
476    def label(self):
477        return _(u'Send message to ${a}',
478            mapping = {'a':self.context.display_fullname})
479
480    @action('Send message now', style='primary')
481    def send(self, *args, **data):
482        try:
483            email = self.request.principal.email
484        except AttributeError:
485            email = self.config.email_admin
486        usertype = getattr(self.request.principal,
487                           'user_type', 'system').title()
488        kofa_utils = getUtility(IKofaUtils)
489        success = kofa_utils.sendContactForm(
490                self.request.principal.title,email,
491                self.context.display_fullname,self.context.email,
492                self.request.principal.id,usertype,
493                self.config.name,
494                data['body'],data['subject'])
495        if success:
496            self.flash(_('Your message has been sent.'))
497        else:
498            self.flash(_('An smtp server error occurred.'))
499        return
500
501class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
502    """Deliver a PDF Admission slip.
503    """
504    grok.context(IStudent)
505    grok.name('admission_slip.pdf')
506    grok.require('waeup.viewStudent')
507    prefix = 'form'
508
509    omit_fields = ('date_of_birth',)
510
511    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
512
513    @property
514    def label(self):
515        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
516        return translate(_('Admission Letter of'),
517            'waeup.kofa', target_language=portal_language) \
518            + ' %s' % self.context.display_fullname
519
520    def render(self):
521        students_utils = getUtility(IStudentsUtils)
522        return students_utils.renderPDFAdmissionLetter(self,
523            self.context.student, omit_fields=self.omit_fields)
524
525class StudentBaseManageFormPage(KofaEditFormPage):
526    """ View to manage student base data
527    """
528    grok.context(IStudent)
529    grok.name('manage_base')
530    grok.require('waeup.manageStudent')
531    form_fields = grok.AutoFields(IStudentBase).omit(
532        'student_id', 'adm_code', 'suspended')
533    grok.template('basemanagepage')
534    label = _('Manage base data')
535    pnav = 4
536
537    def update(self):
538        datepicker.need() # Enable jQuery datepicker in date fields.
539        tabs.need()
540        self.tab1 = self.tab2 = ''
541        qs = self.request.get('QUERY_STRING', '')
542        if not qs:
543            qs = 'tab1'
544        setattr(self, qs, 'active')
545        super(StudentBaseManageFormPage, self).update()
546        self.wf_info = IWorkflowInfo(self.context)
547        return
548
549    @action(_('Save'), style='primary')
550    def save(self, **data):
551        form = self.request.form
552        password = form.get('password', None)
553        password_ctl = form.get('control_password', None)
554        if password:
555            validator = getUtility(IPasswordValidator)
556            errors = validator.validate_password(password, password_ctl)
557            if errors:
558                self.flash( ' '.join(errors))
559                return
560        changed_fields = self.applyData(self.context, **data)
561        # Turn list of lists into single list
562        if changed_fields:
563            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
564        else:
565            changed_fields = []
566        if password:
567            # Now we know that the form has no errors and can set password
568            IUserAccount(self.context).setPassword(password)
569            changed_fields.append('password')
570        fields_string = ' + '.join(changed_fields)
571        self.flash(_('Form has been saved.'))
572        if fields_string:
573            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
574        return
575
576class StudentTriggerTransitionFormPage(KofaEditFormPage):
577    """ View to manage student base data
578    """
579    grok.context(IStudent)
580    grok.name('trigtrans')
581    grok.require('waeup.triggerTransition')
582    grok.template('trigtrans')
583    label = _('Trigger registration transition')
584    pnav = 4
585
586    def getTransitions(self):
587        """Return a list of dicts of allowed transition ids and titles.
588
589        Each list entry provides keys ``name`` and ``title`` for
590        internal name and (human readable) title of a single
591        transition.
592        """
593        wf_info = IWorkflowInfo(self.context)
594        allowed_transitions = [t for t in wf_info.getManualTransitions()
595            if not t[0].startswith('pay')]
596        if self.context.is_postgrad and not self.context.is_special_postgrad:
597            allowed_transitions = [t for t in allowed_transitions
598                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
599        return [dict(name='', title=_('No transition'))] +[
600            dict(name=x, title=y) for x, y in allowed_transitions]
601
602    @action(_('Save'), style='primary')
603    def save(self, **data):
604        form = self.request.form
605        if 'transition' in form and form['transition']:
606            transition_id = form['transition']
607            wf_info = IWorkflowInfo(self.context)
608            wf_info.fireTransition(transition_id)
609        return
610
611class StudentActivatePage(UtilityView, grok.View):
612    """ Activate student account
613    """
614    grok.context(IStudent)
615    grok.name('activate')
616    grok.require('waeup.manageStudent')
617
618    def update(self):
619        self.context.suspended = False
620        self.context.writeLogMessage(self, 'account activated')
621        history = IObjectHistory(self.context)
622        history.addMessage('Student account activated')
623        self.flash(_('Student account has been activated.'))
624        self.redirect(self.url(self.context))
625        return
626
627    def render(self):
628        return
629
630class StudentDeactivatePage(UtilityView, grok.View):
631    """ Deactivate student account
632    """
633    grok.context(IStudent)
634    grok.name('deactivate')
635    grok.require('waeup.manageStudent')
636
637    def update(self):
638        self.context.suspended = True
639        self.context.writeLogMessage(self, 'account deactivated')
640        history = IObjectHistory(self.context)
641        history.addMessage('Student account deactivated')
642        self.flash(_('Student account has been deactivated.'))
643        self.redirect(self.url(self.context))
644        return
645
646    def render(self):
647        return
648
649class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
650    """ Page to display student clearance data
651    """
652    grok.context(IStudent)
653    grok.name('view_clearance')
654    grok.require('waeup.viewStudent')
655    pnav = 4
656
657    @property
658    def separators(self):
659        return getUtility(IStudentsUtils).SEPARATORS_DICT
660
661    @property
662    def form_fields(self):
663        if self.context.is_postgrad:
664            form_fields = grok.AutoFields(
665                IPGStudentClearance).omit('clearance_locked')
666        else:
667            form_fields = grok.AutoFields(
668                IUGStudentClearance).omit('clearance_locked')
669        if not getattr(self.context, 'officer_comment'):
670            form_fields = form_fields.omit('officer_comment')
671        else:
672            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
673        return form_fields
674
675    @property
676    def label(self):
677        return _('${a}: Clearance Data',
678            mapping = {'a':self.context.display_fullname})
679
680class ExportPDFClearanceSlipPage(grok.View):
681    """Deliver a PDF slip of the context.
682    """
683    grok.context(IStudent)
684    grok.name('clearance_slip.pdf')
685    grok.require('waeup.viewStudent')
686    prefix = 'form'
687    omit_fields = (
688        'password', 'suspended', 'phone',
689        'adm_code', 'suspended_comment',
690        'date_of_birth')
691
692    @property
693    def form_fields(self):
694        if self.context.is_postgrad:
695            form_fields = grok.AutoFields(
696                IPGStudentClearance).omit('clearance_locked')
697        else:
698            form_fields = grok.AutoFields(
699                IUGStudentClearance).omit('clearance_locked')
700        if not getattr(self.context, 'officer_comment'):
701            form_fields = form_fields.omit('officer_comment')
702        return form_fields
703
704    @property
705    def title(self):
706        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
707        return translate(_('Clearance Data'), 'waeup.kofa',
708            target_language=portal_language)
709
710    @property
711    def label(self):
712        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
713        return translate(_('Clearance Slip of'),
714            'waeup.kofa', target_language=portal_language) \
715            + ' %s' % self.context.display_fullname
716
717    # XXX: not used in waeup.kofa and thus not tested
718    def _signatures(self):
719        isStudent = getattr(
720            self.request.principal, 'user_type', None) == 'student'
721        if not isStudent and self.context.state in (CLEARED, ):
722            return ([_('Student Signature')],
723                    [_('Clearance Officer Signature')])
724        return
725
726    def _sigsInFooter(self):
727        isStudent = getattr(
728            self.request.principal, 'user_type', None) == 'student'
729        if not isStudent and self.context.state in (CLEARED, ):
730            return (_('Date, Student Signature'),
731                    _('Date, Clearance Officer Signature'),
732                    )
733        return ()
734
735    def render(self):
736        studentview = StudentBasePDFFormPage(self.context.student,
737            self.request, self.omit_fields)
738        students_utils = getUtility(IStudentsUtils)
739        return students_utils.renderPDF(
740            self, 'clearance_slip.pdf',
741            self.context.student, studentview, signatures=self._signatures(),
742            sigs_in_footer=self._sigsInFooter(),
743            omit_fields=self.omit_fields)
744
745class StudentClearanceManageFormPage(KofaEditFormPage):
746    """ Page to manage student clearance data
747    """
748    grok.context(IStudent)
749    grok.name('manage_clearance')
750    grok.require('waeup.manageStudent')
751    grok.template('clearanceeditpage')
752    label = _('Manage clearance data')
753    pnav = 4
754
755    @property
756    def separators(self):
757        return getUtility(IStudentsUtils).SEPARATORS_DICT
758
759    @property
760    def form_fields(self):
761        if self.context.is_postgrad:
762            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
763        else:
764            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
765        return form_fields
766
767    def update(self):
768        datepicker.need() # Enable jQuery datepicker in date fields.
769        tabs.need()
770        self.tab1 = self.tab2 = ''
771        qs = self.request.get('QUERY_STRING', '')
772        if not qs:
773            qs = 'tab1'
774        setattr(self, qs, 'active')
775        return super(StudentClearanceManageFormPage, self).update()
776
777    @action(_('Save'), style='primary')
778    def save(self, **data):
779        msave(self, **data)
780        return
781
782class StudentClearPage(UtilityView, grok.View):
783    """ Clear student by clearance officer
784    """
785    grok.context(IStudent)
786    grok.name('clear')
787    grok.require('waeup.clearStudent')
788
789    def update(self):
790        if clearance_disabled_message(self.context):
791            self.flash(clearance_disabled_message(self.context))
792            self.redirect(self.url(self.context,'view_clearance'))
793            return
794        if self.context.state == REQUESTED:
795            IWorkflowInfo(self.context).fireTransition('clear')
796            self.flash(_('Student has been cleared.'))
797        else:
798            self.flash(_('Student is in wrong state.'))
799        self.redirect(self.url(self.context,'view_clearance'))
800        return
801
802    def render(self):
803        return
804
805class StudentRejectClearancePage(KofaEditFormPage):
806    """ Reject clearance by clearance officers
807    """
808    grok.context(IStudent)
809    grok.name('reject_clearance')
810    label = _('Reject clearance')
811    grok.require('waeup.clearStudent')
812    form_fields = grok.AutoFields(
813        IUGStudentClearance).select('officer_comment')
814
815    def update(self):
816        if clearance_disabled_message(self.context):
817            self.flash(clearance_disabled_message(self.context))
818            self.redirect(self.url(self.context,'view_clearance'))
819            return
820        return super(StudentRejectClearancePage, self).update()
821
822    @action(_('Save comment and reject clearance now'), style='primary')
823    def reject(self, **data):
824        if self.context.state == CLEARED:
825            IWorkflowInfo(self.context).fireTransition('reset4')
826            message = _('Clearance has been annulled.')
827            self.flash(message)
828        elif self.context.state == REQUESTED:
829            IWorkflowInfo(self.context).fireTransition('reset3')
830            message = _('Clearance request has been rejected.')
831            self.flash(message)
832        else:
833            self.flash(_('Student is in wrong state.'))
834            self.redirect(self.url(self.context,'view_clearance'))
835            return
836        self.applyData(self.context, **data)
837        comment = data['officer_comment']
838        if comment:
839            self.context.writeLogMessage(
840                self, 'comment: %s' % comment.replace('\n', '<br>'))
841            args = {'subject':message, 'body':comment}
842        else:
843            args = {'subject':message,}
844        self.redirect(self.url(self.context) +
845            '/contactstudent?%s' % urlencode(args))
846        return
847
848
849class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
850    """ Page to display student personal data
851    """
852    grok.context(IStudent)
853    grok.name('view_personal')
854    grok.require('waeup.viewStudent')
855    form_fields = grok.AutoFields(IStudentPersonal)
856    form_fields['perm_address'].custom_widget = BytesDisplayWidget
857    form_fields[
858        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
859    pnav = 4
860
861    @property
862    def label(self):
863        return _('${a}: Personal Data',
864            mapping = {'a':self.context.display_fullname})
865
866class StudentPersonalManageFormPage(KofaEditFormPage):
867    """ Page to manage personal data
868    """
869    grok.context(IStudent)
870    grok.name('manage_personal')
871    grok.require('waeup.manageStudent')
872    form_fields = grok.AutoFields(IStudentPersonal)
873    form_fields['personal_updated'].for_display = True
874    form_fields[
875        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
876    label = _('Manage personal data')
877    pnav = 4
878
879    @action(_('Save'), style='primary')
880    def save(self, **data):
881        msave(self, **data)
882        return
883
884class StudentPersonalEditFormPage(KofaEditFormPage):
885    """ Page to edit personal data
886    """
887    grok.context(IStudent)
888    grok.name('edit_personal')
889    grok.require('waeup.handleStudent')
890    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
891    label = _('Edit personal data')
892    pnav = 4
893
894    @action(_('Save/Confirm'), style='primary')
895    def save(self, **data):
896        msave(self, **data)
897        self.context.personal_updated = datetime.utcnow()
898        return
899
900class StudyCourseDisplayFormPage(KofaDisplayFormPage):
901    """ Page to display the student study course data
902    """
903    grok.context(IStudentStudyCourse)
904    grok.name('index')
905    grok.require('waeup.viewStudent')
906    grok.template('studycoursepage')
907    pnav = 4
908
909    @property
910    def form_fields(self):
911        if self.context.is_postgrad:
912            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
913                'previous_verdict')
914        else:
915            form_fields = grok.AutoFields(IStudentStudyCourse)
916        return form_fields
917
918    @property
919    def label(self):
920        if self.context.is_current:
921            return _('${a}: Study Course',
922                mapping = {'a':self.context.__parent__.display_fullname})
923        else:
924            return _('${a}: Previous Study Course',
925                mapping = {'a':self.context.__parent__.display_fullname})
926
927    @property
928    def current_mode(self):
929        if self.context.certificate is not None:
930            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
931            return studymodes_dict[self.context.certificate.study_mode]
932        return
933
934    @property
935    def department(self):
936        if self.context.certificate is not None:
937            return self.context.certificate.__parent__.__parent__
938        return
939
940    @property
941    def faculty(self):
942        if self.context.certificate is not None:
943            return self.context.certificate.__parent__.__parent__.__parent__
944        return
945
946    @property
947    def prev_studycourses(self):
948        if self.context.is_current:
949            if self.context.__parent__.get('studycourse_2', None) is not None:
950                return (
951                        {'href':self.url(self.context.student) + '/studycourse_1',
952                        'title':_('First Study Course, ')},
953                        {'href':self.url(self.context.student) + '/studycourse_2',
954                        'title':_('Second Study Course')}
955                        )
956            if self.context.__parent__.get('studycourse_1', None) is not None:
957                return (
958                        {'href':self.url(self.context.student) + '/studycourse_1',
959                        'title':_('First Study Course')},
960                        )
961        return
962
963class StudyCourseManageFormPage(KofaEditFormPage):
964    """ Page to edit the student study course data
965    """
966    grok.context(IStudentStudyCourse)
967    grok.name('manage')
968    grok.require('waeup.manageStudent')
969    grok.template('studycoursemanagepage')
970    label = _('Manage study course')
971    pnav = 4
972    taboneactions = [_('Save'),_('Cancel')]
973    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
974    tabthreeactions = [_('Add study level')]
975
976    @property
977    def form_fields(self):
978        if self.context.is_postgrad:
979            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
980                'previous_verdict')
981        else:
982            form_fields = grok.AutoFields(IStudentStudyCourse)
983        return form_fields
984
985    def update(self):
986        if not self.context.is_current:
987            emit_lock_message(self)
988            return
989        super(StudyCourseManageFormPage, self).update()
990        tabs.need()
991        self.tab1 = self.tab2 = ''
992        qs = self.request.get('QUERY_STRING', '')
993        if not qs:
994            qs = 'tab1'
995        setattr(self, qs, 'active')
996        warning.need()
997        datatable.need()
998        return
999
1000    @action(_('Save'), style='primary')
1001    def save(self, **data):
1002        try:
1003            msave(self, **data)
1004        except ConstraintNotSatisfied:
1005            # The selected level might not exist in certificate
1006            self.flash(_('Current level not available for certificate.'))
1007            return
1008        notify(grok.ObjectModifiedEvent(self.context.__parent__))
1009        return
1010
1011    @property
1012    def level_dicts(self):
1013        studylevelsource = StudyLevelSource().factory
1014        for code in studylevelsource.getValues(self.context):
1015            title = studylevelsource.getTitle(self.context, code)
1016            yield(dict(code=code, title=title))
1017
1018    @property
1019    def session_dicts(self):
1020        yield(dict(code='', title='--'))
1021        for item in academic_sessions():
1022            code = item[1]
1023            title = item[0]
1024            yield(dict(code=code, title=title))
1025
1026    @action(_('Add study level'))
1027    def addStudyLevel(self, **data):
1028        level_code = self.request.form.get('addlevel', None)
1029        level_session = self.request.form.get('level_session', None)
1030        if not level_session:
1031            self.flash(_('You must select a session for the level.'))
1032            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1033            return
1034        studylevel = createObject(u'waeup.StudentStudyLevel')
1035        studylevel.level = int(level_code)
1036        studylevel.level_session = int(level_session)
1037        try:
1038            self.context.addStudentStudyLevel(
1039                self.context.certificate,studylevel)
1040            self.flash(_('Study level has been added.'))
1041        except KeyError:
1042            self.flash(_('This level exists.'))
1043        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1044        return
1045
1046    @jsaction(_('Remove selected levels'))
1047    def delStudyLevels(self, **data):
1048        form = self.request.form
1049        if 'val_id' in form:
1050            child_id = form['val_id']
1051        else:
1052            self.flash(_('No study level selected.'))
1053            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1054            return
1055        if not isinstance(child_id, list):
1056            child_id = [child_id]
1057        deleted = []
1058        for id in child_id:
1059            del self.context[id]
1060            deleted.append(id)
1061        if len(deleted):
1062            self.flash(_('Successfully removed: ${a}',
1063                mapping = {'a':', '.join(deleted)}))
1064            self.context.writeLogMessage(
1065                self,'removed: %s' % ', '.join(deleted))
1066        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1067        return
1068
1069class StudentTranscriptRequestPage(KofaPage):
1070    """ Page to transcript by student
1071    """
1072    grok.context(IStudent)
1073    grok.name('request_transcript')
1074    grok.require('waeup.handleStudent')
1075    grok.template('transcriptrequest')
1076    label = _('Request transcript')
1077    ac_prefix = 'TSC'
1078    notice = ''
1079    pnav = 4
1080    buttonname = _('Submit')
1081    with_ac = True
1082
1083    def update(self, SUBMIT=None):
1084        super(StudentTranscriptRequestPage, self).update()
1085        tabs.need()
1086        self.tab1 = self.tab2 = ''
1087        qs = self.request.get('QUERY_STRING', '')
1088        if not qs:
1089            qs = 'tab1'
1090        setattr(self, qs, 'active')
1091        if not self.context.state == GRADUATED:
1092            self.flash(_("Wrong state"))
1093            self.redirect(self.url(self.context))
1094            return
1095        if self.with_ac:
1096            self.ac_series = self.request.form.get('ac_series', None)
1097            self.ac_number = self.request.form.get('ac_number', None)
1098        if self.context.transcript_comment is not None:
1099            self.correspondence = self.context.transcript_comment.replace(
1100                '\n', '<br>')
1101        else:
1102            self.correspondence = ''
1103        if SUBMIT is None:
1104            return
1105        if self.with_ac:
1106            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1107            code = get_access_code(pin)
1108            if not code:
1109                self.flash(_('Activation code is invalid.'))
1110                return
1111            if code.state == USED:
1112                self.flash(_('Activation code has already been used.'))
1113                return
1114            # Mark pin as used (this also fires a pin related transition)
1115            # and fire transition start_clearance
1116            comment = _(u"invalidated")
1117            # Here we know that the ac is in state initialized so we do not
1118            # expect an exception, but the owner might be different
1119            if not invalidate_accesscode(pin, comment, self.context.student_id):
1120                self.flash(_('You are not the owner of this access code.'))
1121                return
1122            self.context.clr_code = pin
1123        IWorkflowInfo(self.context).fireTransition('request_transcript')
1124        comment = self.request.form.get('comment', '').replace('\r', '')
1125        address = self.request.form.get('address', '').replace('\r', '')
1126        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1127        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1128        old_transcript_comment = self.context.transcript_comment
1129        if old_transcript_comment == None:
1130            old_transcript_comment = ''
1131        self.context.transcript_comment = '''On %s %s wrote:
1132
1133%s
1134
1135Dispatch Address:
1136%s
1137
1138%s''' % (today, self.request.principal.id, comment, address,
1139         old_transcript_comment)
1140        self.context.writeLogMessage(
1141            self, 'comment: %s' % comment.replace('\n', '<br>'))
1142        self.flash(_('Transcript processing has been started.'))
1143        self.redirect(self.url(self.context))
1144        return
1145
1146class StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
1147    """ Page to process transcript requests
1148    """
1149    grok.context(IStudent)
1150    grok.name('process_transcript_request')
1151    grok.require('waeup.viewTranscript')
1152    grok.template('transcriptprocess')
1153    form_fields = grok.AutoFields(IStudentTranscript)
1154    label = _('Process transcript request')
1155    buttonname = _('Save comment and mark as processed')
1156    pnav = 4
1157
1158    def update(self, SUBMIT=None):
1159        super(StudentTranscriptRequestProcessFormPage, self).update()
1160        tabs.need()
1161        self.tab1 = self.tab2 = ''
1162        qs = self.request.get('QUERY_STRING', '')
1163        if not qs:
1164            qs = 'tab1'
1165        setattr(self, qs, 'active')
1166        if self.context.state != TRANSCRIPT:
1167            self.flash(_('Student is in wrong state.'))
1168            self.redirect(self.url(self.context))
1169            return
1170        if self.context.transcript_comment is not None:
1171            self.correspondence = self.context.transcript_comment.replace(
1172                '\n', '<br>')
1173        else:
1174            self.correspondence = ''
1175        if SUBMIT is None:
1176            return
1177        IWorkflowInfo(self.context).fireTransition('process_transcript')
1178        self.flash(_('Transcript request processed.'))
1179        comment = self.request.form.get('comment', '').replace('\r', '')
1180        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1181        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1182        old_transcript_comment = self.context.transcript_comment
1183        if old_transcript_comment == None:
1184            old_transcript_comment = ''
1185        self.context.transcript_comment = '''On %s %s wrote:
1186
1187%s
1188
1189%s''' % (today, self.request.principal.id, comment,
1190         old_transcript_comment)
1191        self.context.writeLogMessage(
1192            self, 'comment: %s' % comment.replace('\n', '<br>'))
1193        subject = _('Transcript processed')
1194        args = {'subject':subject, 'body':comment}
1195        self.redirect(self.url(self.context) +
1196            '/contactstudent?%s' % urlencode(args))
1197        return
1198
1199class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1200    """ Page to edit personal data by student
1201    """
1202    grok.context(IStudent)
1203    grok.name('manage_transcript_request')
1204    grok.require('waeup.manageStudent')
1205    form_fields = grok.AutoFields(IStudentTranscript)
1206    label = _('Manage transcript request')
1207    pnav = 4
1208
1209    @action(_('Save'), style='primary')
1210    def save(self, **data):
1211        msave(self, **data)
1212        return
1213
1214class StudyCourseTranscriptPage(KofaDisplayFormPage):
1215    """ Page to display the student's transcript.
1216    """
1217    grok.context(IStudentStudyCourseTranscript)
1218    grok.name('transcript')
1219    grok.require('waeup.viewTranscript')
1220    grok.template('transcript')
1221    pnav = 4
1222
1223    def update(self):
1224        if not self.context.student.transcript_enabled:
1225            self.flash(_('You are not allowed to view the transcript.'))
1226            self.redirect(self.url(self.context))
1227            return
1228        super(StudyCourseTranscriptPage, self).update()
1229        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1230        self.level_dict = level_dict(self.context)
1231        self.session_dict = dict(
1232            [(item[1], item[0]) for item in academic_sessions()])
1233        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1234        return
1235
1236    @property
1237    def label(self):
1238        # Here we know that the cookie has been set
1239        lang = self.request.cookies.get('kofa.language')
1240        return _('${a}: Transcript Data', mapping = {
1241            'a':self.context.student.display_fullname})
1242
1243class ExportPDFTranscriptPage(UtilityView, grok.View):
1244    """Deliver a PDF slip of the context.
1245    """
1246    grok.context(IStudentStudyCourse)
1247    grok.name('transcript.pdf')
1248    grok.require('waeup.viewTranscript')
1249    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1250    prefix = 'form'
1251    omit_fields = (
1252        'department', 'faculty', 'entry_session', 'certificate',
1253        'password', 'suspended', 'phone', 'email',
1254        'adm_code', 'suspended_comment')
1255
1256    def update(self):
1257        if not self.context.student.transcript_enabled:
1258            self.flash(_('You are not allowed to download the transcript.'))
1259            self.redirect(self.url(self.context))
1260            return
1261        super(ExportPDFTranscriptPage, self).update()
1262        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1263        self.level_dict = level_dict(self.context)
1264        self.session_dict = dict(
1265            [(item[1], item[0]) for item in academic_sessions()])
1266        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1267        return
1268
1269    @property
1270    def label(self):
1271        # Here we know that the cookie has been set
1272        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1273        return translate(_('Academic Transcript'),
1274            'waeup.kofa', target_language=portal_language)
1275
1276    def _sigsInFooter(self):
1277        return (_('CERTIFIED TRUE COPY'),)
1278
1279    def _signatures(self):
1280        return None
1281
1282    def render(self):
1283        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1284        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1285        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1286        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1287        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1288        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1289        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1290        studentview = StudentBasePDFFormPage(self.context.student,
1291            self.request, self.omit_fields)
1292        students_utils = getUtility(IStudentsUtils)
1293
1294        tableheader = [(Code,'code', 2.5),
1295                         (Title,'title', 7),
1296                         (Term, 'semester', 1.5),
1297                         (Cred, 'credits', 1.5),
1298                         (Score, 'score', 1.5),
1299                         (Grade, 'grade', 1.5),
1300                         ]
1301
1302        return students_utils.renderPDFTranscript(
1303            self, 'transcript.pdf',
1304            self.context.student, studentview,
1305            omit_fields=self.omit_fields,
1306            tableheader=tableheader,
1307            signatures=self._signatures(),
1308            sigs_in_footer=self._sigsInFooter(),
1309            )
1310
1311class StudentTransferFormPage(KofaAddFormPage):
1312    """Page to transfer the student.
1313    """
1314    grok.context(IStudent)
1315    grok.name('transfer')
1316    grok.require('waeup.manageStudent')
1317    label = _('Transfer student')
1318    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1319        'entry_mode', 'entry_session')
1320    pnav = 4
1321
1322    def update(self):
1323        super(StudentTransferFormPage, self).update()
1324        warning.need()
1325        return
1326
1327    @jsaction(_('Transfer'))
1328    def transferStudent(self, **data):
1329        error = self.context.transfer(**data)
1330        if error == -1:
1331            self.flash(_('Current level does not match certificate levels.'))
1332        elif error == -2:
1333            self.flash(_('Former study course record incomplete.'))
1334        elif error == -3:
1335            self.flash(_('Maximum number of transfers exceeded.'))
1336        else:
1337            self.flash(_('Successfully transferred.'))
1338        return
1339
1340class RevertTransferFormPage(KofaEditFormPage):
1341    """View that reverts the previous transfer.
1342    """
1343    grok.context(IStudent)
1344    grok.name('revert_transfer')
1345    grok.require('waeup.manageStudent')
1346    grok.template('reverttransfer')
1347    label = _('Revert previous transfer')
1348
1349    def update(self):
1350        warning.need()
1351        if not self.context.has_key('studycourse_1'):
1352            self.flash(_('No previous transfer.'))
1353            self.redirect(self.url(self.context))
1354            return
1355        return
1356
1357    @jsaction(_('Revert now'))
1358    def transferStudent(self, **data):
1359        self.context.revert_transfer()
1360        self.flash(_('Previous transfer reverted.'))
1361        self.redirect(self.url(self.context, 'studycourse'))
1362        return
1363
1364class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1365    """ Page to display student study levels
1366    """
1367    grok.context(IStudentStudyLevel)
1368    grok.name('index')
1369    grok.require('waeup.viewStudent')
1370    form_fields = grok.AutoFields(IStudentStudyLevel)
1371    form_fields[
1372        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1373    grok.template('studylevelpage')
1374    pnav = 4
1375
1376    def update(self):
1377        super(StudyLevelDisplayFormPage, self).update()
1378        datatable.need()
1379        return
1380
1381    @property
1382    def translated_values(self):
1383        return translated_values(self)
1384
1385    @property
1386    def label(self):
1387        # Here we know that the cookie has been set
1388        lang = self.request.cookies.get('kofa.language')
1389        level_title = translate(self.context.level_title, 'waeup.kofa',
1390            target_language=lang)
1391        return _('${a}: Study Level ${b}', mapping = {
1392            'a':self.context.student.display_fullname,
1393            'b':level_title})
1394
1395class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1396    """Deliver a PDF slip of the context.
1397    """
1398    grok.context(IStudentStudyLevel)
1399    grok.name('course_registration_slip.pdf')
1400    grok.require('waeup.viewStudent')
1401    form_fields = grok.AutoFields(IStudentStudyLevel)
1402    form_fields[
1403        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1404    prefix = 'form'
1405    omit_fields = (
1406        'password', 'suspended', 'phone', 'date_of_birth',
1407        'adm_code', 'sex', 'suspended_comment')
1408
1409    @property
1410    def title(self):
1411        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1412        return translate(_('Level Data'), 'waeup.kofa',
1413            target_language=portal_language)
1414
1415    @property
1416    def label(self):
1417        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1418        lang = self.request.cookies.get('kofa.language', portal_language)
1419        level_title = translate(self.context.level_title, 'waeup.kofa',
1420            target_language=lang)
1421        return translate(_('Course Registration Slip'),
1422            'waeup.kofa', target_language=portal_language) \
1423            + ' %s' % level_title
1424
1425    @property
1426    def tabletitle(self):
1427        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1428        tabletitle = []
1429        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1430            target_language=portal_language))
1431        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1432            target_language=portal_language))
1433        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1434            target_language=portal_language))
1435        return tabletitle
1436
1437    def render(self):
1438        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1439        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1440        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1441        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1442        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1443        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1444        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1445        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1446        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1447        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1448        studentview = StudentBasePDFFormPage(self.context.student,
1449            self.request, self.omit_fields)
1450        students_utils = getUtility(IStudentsUtils)
1451
1452        tabledata = []
1453        tableheader = []
1454        contenttitle = []
1455        for i in range(1,7):
1456            tabledata.append(sorted(
1457                [value for value in self.context.values() if value.semester == i],
1458                key=lambda value: str(value.semester) + value.code))
1459            tableheader.append([(Code,'code', 2.5),
1460                             (Title,'title', 5),
1461                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1462                             (Cred, 'credits', 1.5),
1463                             #(Mand, 'mandatory', 1.5),
1464                             (Score, 'score', 1.5),
1465                             (Grade, 'grade', 1.5),
1466                             #('Auto', 'automatic', 1.5)
1467                             ])
1468        return students_utils.renderPDF(
1469            self, 'course_registration_slip.pdf',
1470            self.context.student, studentview,
1471            tableheader=tableheader,
1472            tabledata=tabledata,
1473            omit_fields=self.omit_fields
1474            )
1475
1476class StudyLevelManageFormPage(KofaEditFormPage):
1477    """ Page to edit the student study level data
1478    """
1479    grok.context(IStudentStudyLevel)
1480    grok.name('manage')
1481    grok.require('waeup.manageStudent')
1482    grok.template('studylevelmanagepage')
1483    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1484        'validation_date', 'validated_by', 'total_credits', 'gpa')
1485    pnav = 4
1486    taboneactions = [_('Save'),_('Cancel')]
1487    tabtwoactions = [_('Add course ticket'),
1488        _('Remove selected tickets'),_('Cancel')]
1489
1490    def update(self, ADD=None, course=None):
1491        if not self.context.__parent__.is_current:
1492            emit_lock_message(self)
1493            return
1494        super(StudyLevelManageFormPage, self).update()
1495        tabs.need()
1496        self.tab1 = self.tab2 = ''
1497        qs = self.request.get('QUERY_STRING', '')
1498        if not qs:
1499            qs = 'tab1'
1500        setattr(self, qs, 'active')
1501        warning.need()
1502        datatable.need()
1503        if ADD is not None:
1504            if not course:
1505                self.flash(_('No valid course code entered.'))
1506                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1507                return
1508            cat = queryUtility(ICatalog, name='courses_catalog')
1509            result = cat.searchResults(code=(course, course))
1510            if len(result) != 1:
1511                self.flash(_('Course not found.'))
1512            else:
1513                course = list(result)[0]
1514                addCourseTicket(self, course)
1515            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1516        return
1517
1518    @property
1519    def translated_values(self):
1520        return translated_values(self)
1521
1522    @property
1523    def label(self):
1524        # Here we know that the cookie has been set
1525        lang = self.request.cookies.get('kofa.language')
1526        level_title = translate(self.context.level_title, 'waeup.kofa',
1527            target_language=lang)
1528        return _('Manage study level ${a}',
1529            mapping = {'a':level_title})
1530
1531    @action(_('Save'), style='primary')
1532    def save(self, **data):
1533        msave(self, **data)
1534        return
1535
1536    @jsaction(_('Remove selected tickets'))
1537    def delCourseTicket(self, **data):
1538        form = self.request.form
1539        if 'val_id' in form:
1540            child_id = form['val_id']
1541        else:
1542            self.flash(_('No ticket selected.'))
1543            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1544            return
1545        if not isinstance(child_id, list):
1546            child_id = [child_id]
1547        deleted = []
1548        for id in child_id:
1549            del self.context[id]
1550            deleted.append(id)
1551        if len(deleted):
1552            self.flash(_('Successfully removed: ${a}',
1553                mapping = {'a':', '.join(deleted)}))
1554            self.context.writeLogMessage(
1555                self,'removed: %s at %s' %
1556                (', '.join(deleted), self.context.level))
1557        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1558        return
1559
1560class ValidateCoursesPage(UtilityView, grok.View):
1561    """ Validate course list by course adviser
1562    """
1563    grok.context(IStudentStudyLevel)
1564    grok.name('validate_courses')
1565    grok.require('waeup.validateStudent')
1566
1567    def update(self):
1568        if not self.context.__parent__.is_current:
1569            emit_lock_message(self)
1570            return
1571        if str(self.context.__parent__.current_level) != self.context.__name__:
1572            self.flash(_('This level does not correspond current level.'))
1573        elif self.context.student.state == REGISTERED:
1574            IWorkflowInfo(self.context.student).fireTransition(
1575                'validate_courses')
1576            self.flash(_('Course list has been validated.'))
1577        else:
1578            self.flash(_('Student is in the wrong state.'))
1579        self.redirect(self.url(self.context))
1580        return
1581
1582    def render(self):
1583        return
1584
1585class RejectCoursesPage(UtilityView, grok.View):
1586    """ Reject course list by course adviser
1587    """
1588    grok.context(IStudentStudyLevel)
1589    grok.name('reject_courses')
1590    grok.require('waeup.validateStudent')
1591
1592    def update(self):
1593        if not self.context.__parent__.is_current:
1594            emit_lock_message(self)
1595            return
1596        if str(self.context.__parent__.current_level) != self.context.__name__:
1597            self.flash(_('This level does not correspond current level.'))
1598            self.redirect(self.url(self.context))
1599            return
1600        elif self.context.student.state == VALIDATED:
1601            IWorkflowInfo(self.context.student).fireTransition('reset8')
1602            message = _('Course list request has been annulled.')
1603            self.flash(message)
1604        elif self.context.student.state == REGISTERED:
1605            IWorkflowInfo(self.context.student).fireTransition('reset7')
1606            message = _('Course list request has been rejected:')
1607            self.flash(message)
1608        else:
1609            self.flash(_('Student is in the wrong state.'))
1610            self.redirect(self.url(self.context))
1611            return
1612        args = {'subject':message}
1613        self.redirect(self.url(self.context.student) +
1614            '/contactstudent?%s' % urlencode(args))
1615        return
1616
1617    def render(self):
1618        return
1619
1620class CourseTicketAddFormPage(KofaAddFormPage):
1621    """Add a course ticket.
1622    """
1623    grok.context(IStudentStudyLevel)
1624    grok.name('add')
1625    grok.require('waeup.manageStudent')
1626    label = _('Add course ticket')
1627    form_fields = grok.AutoFields(ICourseTicketAdd)
1628    pnav = 4
1629
1630    def update(self):
1631        if not self.context.__parent__.is_current:
1632            emit_lock_message(self)
1633            return
1634        super(CourseTicketAddFormPage, self).update()
1635        return
1636
1637    @action(_('Add course ticket'))
1638    def addCourseTicket(self, **data):
1639        course = data['course']
1640        success = addCourseTicket(self, course)
1641        if success:
1642            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1643        return
1644
1645    @action(_('Cancel'), validator=NullValidator)
1646    def cancel(self, **data):
1647        self.redirect(self.url(self.context))
1648
1649class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1650    """ Page to display course tickets
1651    """
1652    grok.context(ICourseTicket)
1653    grok.name('index')
1654    grok.require('waeup.viewStudent')
1655    form_fields = grok.AutoFields(ICourseTicket)
1656    grok.template('courseticketpage')
1657    pnav = 4
1658
1659    @property
1660    def label(self):
1661        return _('${a}: Course Ticket ${b}', mapping = {
1662            'a':self.context.student.display_fullname,
1663            'b':self.context.code})
1664
1665class CourseTicketManageFormPage(KofaEditFormPage):
1666    """ Page to manage course tickets
1667    """
1668    grok.context(ICourseTicket)
1669    grok.name('manage')
1670    grok.require('waeup.manageStudent')
1671    form_fields = grok.AutoFields(ICourseTicket)
1672    form_fields['title'].for_display = True
1673    form_fields['fcode'].for_display = True
1674    form_fields['dcode'].for_display = True
1675    form_fields['semester'].for_display = True
1676    form_fields['passmark'].for_display = True
1677    form_fields['credits'].for_display = True
1678    form_fields['mandatory'].for_display = False
1679    form_fields['automatic'].for_display = True
1680    form_fields['carry_over'].for_display = True
1681    pnav = 4
1682    grok.template('courseticketmanagepage')
1683
1684    @property
1685    def label(self):
1686        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1687
1688    @action('Save', style='primary')
1689    def save(self, **data):
1690        msave(self, **data)
1691        return
1692
1693class PaymentsManageFormPage(KofaEditFormPage):
1694    """ Page to manage the student payments
1695
1696    This manage form page is for both students and students officers.
1697    """
1698    grok.context(IStudentPaymentsContainer)
1699    grok.name('index')
1700    grok.require('waeup.viewStudent')
1701    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1702    grok.template('paymentsmanagepage')
1703    pnav = 4
1704
1705    @property
1706    def manage_payments_allowed(self):
1707        return checkPermission('waeup.payStudent', self.context)
1708
1709    def unremovable(self, ticket):
1710        usertype = getattr(self.request.principal, 'user_type', None)
1711        if not usertype:
1712            return False
1713        if not self.manage_payments_allowed:
1714            return True
1715        return (self.request.principal.user_type == 'student' and ticket.r_code)
1716
1717    @property
1718    def label(self):
1719        return _('${a}: Payments',
1720            mapping = {'a':self.context.__parent__.display_fullname})
1721
1722    def update(self):
1723        super(PaymentsManageFormPage, self).update()
1724        datatable.need()
1725        warning.need()
1726        return
1727
1728    @jsaction(_('Remove selected tickets'))
1729    def delPaymentTicket(self, **data):
1730        form = self.request.form
1731        if 'val_id' in form:
1732            child_id = form['val_id']
1733        else:
1734            self.flash(_('No payment selected.'))
1735            self.redirect(self.url(self.context))
1736            return
1737        if not isinstance(child_id, list):
1738            child_id = [child_id]
1739        deleted = []
1740        for id in child_id:
1741            # Students are not allowed to remove used payment tickets
1742            ticket = self.context.get(id, None)
1743            if ticket is not None and not self.unremovable(ticket):
1744                del self.context[id]
1745                deleted.append(id)
1746        if len(deleted):
1747            self.flash(_('Successfully removed: ${a}',
1748                mapping = {'a': ', '.join(deleted)}))
1749            self.context.writeLogMessage(
1750                self,'removed: %s' % ', '.join(deleted))
1751        self.redirect(self.url(self.context))
1752        return
1753
1754    #@action(_('Add online payment ticket'))
1755    #def addPaymentTicket(self, **data):
1756    #    self.redirect(self.url(self.context, '@@addop'))
1757
1758class OnlinePaymentAddFormPage(KofaAddFormPage):
1759    """ Page to add an online payment ticket
1760    """
1761    grok.context(IStudentPaymentsContainer)
1762    grok.name('addop')
1763    grok.template('onlinepaymentaddform')
1764    grok.require('waeup.payStudent')
1765    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1766        'p_category')
1767    label = _('Add online payment')
1768    pnav = 4
1769
1770    @property
1771    def selectable_categories(self):
1772        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1773        return sorted(categories.items())
1774
1775    @action(_('Create ticket'), style='primary')
1776    def createTicket(self, **data):
1777        p_category = data['p_category']
1778        previous_session = data.get('p_session', None)
1779        previous_level = data.get('p_level', None)
1780        student = self.context.__parent__
1781        if p_category == 'bed_allocation' and student[
1782            'studycourse'].current_session != grok.getSite()[
1783            'hostels'].accommodation_session:
1784                self.flash(
1785                    _('Your current session does not match ' + \
1786                    'accommodation session.'))
1787                return
1788        if 'maintenance' in p_category:
1789            current_session = str(student['studycourse'].current_session)
1790            if not current_session in student['accommodation']:
1791                self.flash(_('You have not yet booked accommodation.'))
1792                return
1793        students_utils = getUtility(IStudentsUtils)
1794        error, payment = students_utils.setPaymentDetails(
1795            p_category, student, previous_session, previous_level)
1796        if error is not None:
1797            self.flash(error)
1798            return
1799        self.context[payment.p_id] = payment
1800        self.flash(_('Payment ticket created.'))
1801        self.redirect(self.url(self.context))
1802        return
1803
1804    @action(_('Cancel'), validator=NullValidator)
1805    def cancel(self, **data):
1806        self.redirect(self.url(self.context))
1807
1808class PreviousPaymentAddFormPage(KofaAddFormPage):
1809    """ Page to add an online payment ticket for previous sessions
1810    """
1811    grok.context(IStudentPaymentsContainer)
1812    grok.name('addpp')
1813    grok.require('waeup.payStudent')
1814    form_fields = grok.AutoFields(IStudentPreviousPayment)
1815    label = _('Add previous session online payment')
1816    pnav = 4
1817
1818    def update(self):
1819        if self.context.student.before_payment:
1820            self.flash(_("No previous payment to be made."))
1821            self.redirect(self.url(self.context))
1822        super(PreviousPaymentAddFormPage, self).update()
1823        return
1824
1825    @action(_('Create ticket'), style='primary')
1826    def createTicket(self, **data):
1827        p_category = data['p_category']
1828        previous_session = data.get('p_session', None)
1829        previous_level = data.get('p_level', None)
1830        student = self.context.__parent__
1831        students_utils = getUtility(IStudentsUtils)
1832        error, payment = students_utils.setPaymentDetails(
1833            p_category, student, previous_session, previous_level)
1834        if error is not None:
1835            self.flash(error)
1836            return
1837        self.context[payment.p_id] = payment
1838        self.flash(_('Payment ticket created.'))
1839        self.redirect(self.url(self.context))
1840        return
1841
1842    @action(_('Cancel'), validator=NullValidator)
1843    def cancel(self, **data):
1844        self.redirect(self.url(self.context))
1845
1846class BalancePaymentAddFormPage(KofaAddFormPage):
1847    """ Page to add an online payment ticket for balance sessions
1848    """
1849    grok.context(IStudentPaymentsContainer)
1850    grok.name('addbp')
1851    grok.require('waeup.manageStudent')
1852    form_fields = grok.AutoFields(IStudentBalancePayment)
1853    label = _('Add balance')
1854    pnav = 4
1855
1856    @action(_('Create ticket'), style='primary')
1857    def createTicket(self, **data):
1858        p_category = data['p_category']
1859        balance_session = data.get('balance_session', None)
1860        balance_level = data.get('balance_level', None)
1861        balance_amount = data.get('balance_amount', None)
1862        student = self.context.__parent__
1863        students_utils = getUtility(IStudentsUtils)
1864        error, payment = students_utils.setBalanceDetails(
1865            p_category, student, balance_session,
1866            balance_level, balance_amount)
1867        if error is not None:
1868            self.flash(error)
1869            return
1870        self.context[payment.p_id] = payment
1871        self.flash(_('Payment ticket created.'))
1872        self.redirect(self.url(self.context))
1873        return
1874
1875    @action(_('Cancel'), validator=NullValidator)
1876    def cancel(self, **data):
1877        self.redirect(self.url(self.context))
1878
1879class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1880    """ Page to view an online payment ticket
1881    """
1882    grok.context(IStudentOnlinePayment)
1883    grok.name('index')
1884    grok.require('waeup.viewStudent')
1885    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1886    form_fields[
1887        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1888    form_fields[
1889        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1890    pnav = 4
1891
1892    @property
1893    def label(self):
1894        return _('${a}: Online Payment Ticket ${b}', mapping = {
1895            'a':self.context.student.display_fullname,
1896            'b':self.context.p_id})
1897
1898class OnlinePaymentApprovePage(UtilityView, grok.View):
1899    """ Callback view
1900    """
1901    grok.context(IStudentOnlinePayment)
1902    grok.name('approve')
1903    grok.require('waeup.managePortal')
1904
1905    def update(self):
1906        success, msg, log = self.context.approveStudentPayment()
1907        if log is not None:
1908            # Add log message to students.log
1909            self.context.writeLogMessage(self,log)
1910            # Add log message to payments.log
1911            self.context.logger.info(
1912                '%s,%s,%s,%s,%s,,,,,,' % (
1913                self.context.student.student_id,
1914                self.context.p_id, self.context.p_category,
1915                self.context.amount_auth, self.context.r_code))
1916        self.flash(msg)
1917        return
1918
1919    def render(self):
1920        self.redirect(self.url(self.context, '@@index'))
1921        return
1922
1923class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1924    """ Approval view for students.
1925
1926    This view is used for browser tests only and
1927    must be neutralized in custom pages!
1928    """
1929
1930    grok.name('fake_approve')
1931    grok.require('waeup.payStudent')
1932
1933class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1934    """Deliver a PDF slip of the context.
1935    """
1936    grok.context(IStudentOnlinePayment)
1937    grok.name('payment_slip.pdf')
1938    grok.require('waeup.viewStudent')
1939    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1940    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1941    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1942    prefix = 'form'
1943    note = None
1944    omit_fields = (
1945        'password', 'suspended', 'phone', 'date_of_birth',
1946        'adm_code', 'sex', 'suspended_comment')
1947
1948    @property
1949    def title(self):
1950        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1951        return translate(_('Payment Data'), 'waeup.kofa',
1952            target_language=portal_language)
1953
1954    @property
1955    def label(self):
1956        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1957        return translate(_('Online Payment Slip'),
1958            'waeup.kofa', target_language=portal_language) \
1959            + ' %s' % self.context.p_id
1960
1961    def render(self):
1962        #if self.context.p_state != 'paid':
1963        #    self.flash('Ticket not yet paid.')
1964        #    self.redirect(self.url(self.context))
1965        #    return
1966        studentview = StudentBasePDFFormPage(self.context.student,
1967            self.request, self.omit_fields)
1968        students_utils = getUtility(IStudentsUtils)
1969        return students_utils.renderPDF(self, 'payment_slip.pdf',
1970            self.context.student, studentview, note=self.note,
1971            omit_fields=self.omit_fields)
1972
1973
1974class AccommodationManageFormPage(KofaEditFormPage):
1975    """ Page to manage bed tickets.
1976
1977    This manage form page is for both students and students officers.
1978    """
1979    grok.context(IStudentAccommodation)
1980    grok.name('index')
1981    grok.require('waeup.handleAccommodation')
1982    form_fields = grok.AutoFields(IStudentAccommodation)
1983    grok.template('accommodationmanagepage')
1984    pnav = 4
1985    officers_only_actions = [_('Remove selected')]
1986
1987    @property
1988    def label(self):
1989        return _('${a}: Accommodation',
1990            mapping = {'a':self.context.__parent__.display_fullname})
1991
1992    def update(self):
1993        super(AccommodationManageFormPage, self).update()
1994        datatable.need()
1995        warning.need()
1996        return
1997
1998    @jsaction(_('Remove selected'))
1999    def delBedTickets(self, **data):
2000        if getattr(self.request.principal, 'user_type', None) == 'student':
2001            self.flash(_('You are not allowed to remove bed tickets.'))
2002            self.redirect(self.url(self.context))
2003            return
2004        form = self.request.form
2005        if 'val_id' in form:
2006            child_id = form['val_id']
2007        else:
2008            self.flash(_('No bed ticket selected.'))
2009            self.redirect(self.url(self.context))
2010            return
2011        if not isinstance(child_id, list):
2012            child_id = [child_id]
2013        deleted = []
2014        for id in child_id:
2015            del self.context[id]
2016            deleted.append(id)
2017        if len(deleted):
2018            self.flash(_('Successfully removed: ${a}',
2019                mapping = {'a':', '.join(deleted)}))
2020            self.context.writeLogMessage(
2021                self,'removed: % s' % ', '.join(deleted))
2022        self.redirect(self.url(self.context))
2023        return
2024
2025    @property
2026    def selected_actions(self):
2027        if getattr(self.request.principal, 'user_type', None) == 'student':
2028            return [action for action in self.actions
2029                    if not action.label in self.officers_only_actions]
2030        return self.actions
2031
2032class BedTicketAddPage(KofaPage):
2033    """ Page to add an online payment ticket
2034    """
2035    grok.context(IStudentAccommodation)
2036    grok.name('add')
2037    grok.require('waeup.handleAccommodation')
2038    grok.template('enterpin')
2039    ac_prefix = 'HOS'
2040    label = _('Add bed ticket')
2041    pnav = 4
2042    buttonname = _('Create bed ticket')
2043    notice = ''
2044    with_ac = True
2045
2046    def update(self, SUBMIT=None):
2047        student = self.context.student
2048        students_utils = getUtility(IStudentsUtils)
2049        acc_details  = students_utils.getAccommodationDetails(student)
2050        if acc_details.get('expired', False):
2051            startdate = acc_details.get('startdate')
2052            enddate = acc_details.get('enddate')
2053            if startdate and enddate:
2054                tz = getUtility(IKofaUtils).tzinfo
2055                startdate = to_timezone(
2056                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
2057                enddate = to_timezone(
2058                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
2059                self.flash(_("Outside booking period: ${a} - ${b}",
2060                    mapping = {'a': startdate, 'b': enddate}))
2061            else:
2062                self.flash(_("Outside booking period."))
2063            self.redirect(self.url(self.context))
2064            return
2065        if not acc_details:
2066            self.flash(_("Your data are incomplete."))
2067            self.redirect(self.url(self.context))
2068            return
2069        if not student.state in acc_details['allowed_states']:
2070            self.flash(_("You are in the wrong registration state."))
2071            self.redirect(self.url(self.context))
2072            return
2073        if student['studycourse'].current_session != acc_details[
2074            'booking_session']:
2075            self.flash(
2076                _('Your current session does not match accommodation session.'))
2077            self.redirect(self.url(self.context))
2078            return
2079        if str(acc_details['booking_session']) in self.context.keys():
2080            self.flash(
2081                _('You already booked a bed space in current ' \
2082                    + 'accommodation session.'))
2083            self.redirect(self.url(self.context))
2084            return
2085        if self.with_ac:
2086            self.ac_series = self.request.form.get('ac_series', None)
2087            self.ac_number = self.request.form.get('ac_number', None)
2088        if SUBMIT is None:
2089            return
2090        if self.with_ac:
2091            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2092            code = get_access_code(pin)
2093            if not code:
2094                self.flash(_('Activation code is invalid.'))
2095                return
2096        # Search and book bed
2097        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2098        entries = cat.searchResults(
2099            owner=(student.student_id,student.student_id))
2100        if len(entries):
2101            # If bed space has been manually allocated use this bed
2102            bed = [entry for entry in entries][0]
2103            # Safety belt for paranoids: Does this bed really exist on portal?
2104            # XXX: Can be remove if nobody complains.
2105            if bed.__parent__.__parent__ is None:
2106                self.flash(_('System error: Please contact the adminsitrator.'))
2107                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2108                return
2109        else:
2110            # else search for other available beds
2111            entries = cat.searchResults(
2112                bed_type=(acc_details['bt'],acc_details['bt']))
2113            available_beds = [
2114                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2115            if available_beds:
2116                students_utils = getUtility(IStudentsUtils)
2117                bed = students_utils.selectBed(available_beds)
2118                # Safety belt for paranoids: Does this bed really exist in portal?
2119                # XXX: Can be remove if nobody complains.
2120                if bed.__parent__.__parent__ is None:
2121                    self.flash(_('System error: Please contact the adminsitrator.'))
2122                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
2123                    return
2124                bed.bookBed(student.student_id)
2125            else:
2126                self.flash(_('There is no free bed in your category ${a}.',
2127                    mapping = {'a':acc_details['bt']}))
2128                return
2129        if self.with_ac:
2130            # Mark pin as used (this also fires a pin related transition)
2131            if code.state == USED:
2132                self.flash(_('Activation code has already been used.'))
2133                return
2134            else:
2135                comment = _(u'invalidated')
2136                # Here we know that the ac is in state initialized so we do not
2137                # expect an exception, but the owner might be different
2138                if not invalidate_accesscode(
2139                    pin,comment,self.context.student.student_id):
2140                    self.flash(_('You are not the owner of this access code.'))
2141                    return
2142        # Create bed ticket
2143        bedticket = createObject(u'waeup.BedTicket')
2144        if self.with_ac:
2145            bedticket.booking_code = pin
2146        bedticket.booking_session = acc_details['booking_session']
2147        bedticket.bed_type = acc_details['bt']
2148        bedticket.bed = bed
2149        hall_title = bed.__parent__.hostel_name
2150        coordinates = bed.coordinates[1:]
2151        block, room_nr, bed_nr = coordinates
2152        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2153            'a':hall_title, 'b':block,
2154            'c':room_nr, 'd':bed_nr,
2155            'e':bed.bed_type})
2156        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2157        bedticket.bed_coordinates = translate(
2158            bc, 'waeup.kofa',target_language=portal_language)
2159        self.context.addBedTicket(bedticket)
2160        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2161        self.flash(_('Bed ticket created and bed booked: ${a}',
2162            mapping = {'a':bedticket.display_coordinates}))
2163        self.redirect(self.url(self.context))
2164        return
2165
2166class BedTicketDisplayFormPage(KofaDisplayFormPage):
2167    """ Page to display bed tickets
2168    """
2169    grok.context(IBedTicket)
2170    grok.name('index')
2171    grok.require('waeup.handleAccommodation')
2172    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2173    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2174    pnav = 4
2175
2176    @property
2177    def label(self):
2178        return _('Bed Ticket for Session ${a}',
2179            mapping = {'a':self.context.getSessionString()})
2180
2181class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
2182    """Deliver a PDF slip of the context.
2183    """
2184    grok.context(IBedTicket)
2185    grok.name('bed_allocation_slip.pdf')
2186    grok.require('waeup.handleAccommodation')
2187    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2188    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2189    prefix = 'form'
2190    omit_fields = (
2191        'password', 'suspended', 'phone', 'adm_code',
2192        'suspended_comment', 'date_of_birth')
2193
2194    @property
2195    def title(self):
2196        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2197        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2198            target_language=portal_language)
2199
2200    @property
2201    def label(self):
2202        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2203        #return translate(_('Bed Allocation: '),
2204        #    'waeup.kofa', target_language=portal_language) \
2205        #    + ' %s' % self.context.bed_coordinates
2206        return translate(_('Bed Allocation Slip'),
2207            'waeup.kofa', target_language=portal_language) \
2208            + ' %s' % self.context.getSessionString()
2209
2210    def render(self):
2211        studentview = StudentBasePDFFormPage(self.context.student,
2212            self.request, self.omit_fields)
2213        students_utils = getUtility(IStudentsUtils)
2214        return students_utils.renderPDF(
2215            self, 'bed_allocation_slip.pdf',
2216            self.context.student, studentview,
2217            omit_fields=self.omit_fields)
2218
2219class BedTicketRelocationPage(UtilityView, grok.View):
2220    """ Callback view
2221    """
2222    grok.context(IBedTicket)
2223    grok.name('relocate')
2224    grok.require('waeup.manageHostels')
2225
2226    # Relocate student if student parameters have changed or the bed_type
2227    # of the bed has changed
2228    def update(self):
2229        student = self.context.student
2230        students_utils = getUtility(IStudentsUtils)
2231        acc_details  = students_utils.getAccommodationDetails(student)
2232        if self.context.bed != None and \
2233              'reserved' in self.context.bed.bed_type:
2234            self.flash(_("Students in reserved beds can't be relocated."))
2235            self.redirect(self.url(self.context))
2236            return
2237        if acc_details['bt'] == self.context.bed_type and \
2238                self.context.bed != None and \
2239                self.context.bed.bed_type == self.context.bed_type:
2240            self.flash(_("Student can't be relocated."))
2241            self.redirect(self.url(self.context))
2242            return
2243        # Search a bed
2244        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2245        entries = cat.searchResults(
2246            owner=(student.student_id,student.student_id))
2247        if len(entries) and self.context.bed == None:
2248            # If booking has been cancelled but other bed space has been
2249            # manually allocated after cancellation use this bed
2250            new_bed = [entry for entry in entries][0]
2251        else:
2252            # Search for other available beds
2253            entries = cat.searchResults(
2254                bed_type=(acc_details['bt'],acc_details['bt']))
2255            available_beds = [
2256                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2257            if available_beds:
2258                students_utils = getUtility(IStudentsUtils)
2259                new_bed = students_utils.selectBed(available_beds)
2260                new_bed.bookBed(student.student_id)
2261            else:
2262                self.flash(_('There is no free bed in your category ${a}.',
2263                    mapping = {'a':acc_details['bt']}))
2264                self.redirect(self.url(self.context))
2265                return
2266        # Release old bed if exists
2267        if self.context.bed != None:
2268            self.context.bed.owner = NOT_OCCUPIED
2269            notify(grok.ObjectModifiedEvent(self.context.bed))
2270        # Alocate new bed
2271        self.context.bed_type = acc_details['bt']
2272        self.context.bed = new_bed
2273        hall_title = new_bed.__parent__.hostel_name
2274        coordinates = new_bed.coordinates[1:]
2275        block, room_nr, bed_nr = coordinates
2276        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2277            'a':hall_title, 'b':block,
2278            'c':room_nr, 'd':bed_nr,
2279            'e':new_bed.bed_type})
2280        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2281        self.context.bed_coordinates = translate(
2282            bc, 'waeup.kofa',target_language=portal_language)
2283        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2284        self.flash(_('Student relocated: ${a}',
2285            mapping = {'a':self.context.display_coordinates}))
2286        self.redirect(self.url(self.context))
2287        return
2288
2289    def render(self):
2290        return
2291
2292class StudentHistoryPage(KofaPage):
2293    """ Page to display student clearance data
2294    """
2295    grok.context(IStudent)
2296    grok.name('history')
2297    grok.require('waeup.viewStudent')
2298    grok.template('studenthistory')
2299    pnav = 4
2300
2301    @property
2302    def label(self):
2303        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2304
2305# Pages for students only
2306
2307class StudentBaseEditFormPage(KofaEditFormPage):
2308    """ View to edit student base data
2309    """
2310    grok.context(IStudent)
2311    grok.name('edit_base')
2312    grok.require('waeup.handleStudent')
2313    form_fields = grok.AutoFields(IStudentBase).select(
2314        'email', 'phone')
2315    label = _('Edit base data')
2316    pnav = 4
2317
2318    @action(_('Save'), style='primary')
2319    def save(self, **data):
2320        msave(self, **data)
2321        return
2322
2323class StudentChangePasswordPage(KofaEditFormPage):
2324    """ View to manage student base data
2325    """
2326    grok.context(IStudent)
2327    grok.name('change_password')
2328    grok.require('waeup.handleStudent')
2329    grok.template('change_password')
2330    label = _('Change password')
2331    pnav = 4
2332
2333    @action(_('Save'), style='primary')
2334    def save(self, **data):
2335        form = self.request.form
2336        password = form.get('change_password', None)
2337        password_ctl = form.get('change_password_repeat', None)
2338        if password:
2339            validator = getUtility(IPasswordValidator)
2340            errors = validator.validate_password(password, password_ctl)
2341            if not errors:
2342                IUserAccount(self.context).setPassword(password)
2343                self.context.writeLogMessage(self, 'saved: password')
2344                self.flash(_('Password changed.'))
2345            else:
2346                self.flash( ' '.join(errors))
2347        return
2348
2349class StudentFilesUploadPage(KofaPage):
2350    """ View to upload files by student
2351    """
2352    grok.context(IStudent)
2353    grok.name('change_portrait')
2354    grok.require('waeup.uploadStudentFile')
2355    grok.template('filesuploadpage')
2356    label = _('Upload portrait')
2357    pnav = 4
2358
2359    def update(self):
2360        if self.context.student.state != ADMITTED:
2361            emit_lock_message(self)
2362            return
2363        super(StudentFilesUploadPage, self).update()
2364        return
2365
2366class StartClearancePage(KofaPage):
2367    grok.context(IStudent)
2368    grok.name('start_clearance')
2369    grok.require('waeup.handleStudent')
2370    grok.template('enterpin')
2371    label = _('Start clearance')
2372    ac_prefix = 'CLR'
2373    notice = ''
2374    pnav = 4
2375    buttonname = _('Start clearance now')
2376    with_ac = True
2377
2378    @property
2379    def all_required_fields_filled(self):
2380        if self.context.email and self.context.phone:
2381            return True
2382        return False
2383
2384    @property
2385    def portrait_uploaded(self):
2386        store = getUtility(IExtFileStore)
2387        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2388            return True
2389        return False
2390
2391    def update(self, SUBMIT=None):
2392        if not self.context.state == ADMITTED:
2393            self.flash(_("Wrong state"))
2394            self.redirect(self.url(self.context))
2395            return
2396        if not self.portrait_uploaded:
2397            self.flash(_("No portrait uploaded."))
2398            self.redirect(self.url(self.context, 'change_portrait'))
2399            return
2400        if not self.all_required_fields_filled:
2401            self.flash(_("Not all required fields filled."))
2402            self.redirect(self.url(self.context, 'edit_base'))
2403            return
2404        if self.with_ac:
2405            self.ac_series = self.request.form.get('ac_series', None)
2406            self.ac_number = self.request.form.get('ac_number', None)
2407        if SUBMIT is None:
2408            return
2409        if self.with_ac:
2410            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2411            code = get_access_code(pin)
2412            if not code:
2413                self.flash(_('Activation code is invalid.'))
2414                return
2415            if code.state == USED:
2416                self.flash(_('Activation code has already been used.'))
2417                return
2418            # Mark pin as used (this also fires a pin related transition)
2419            # and fire transition start_clearance
2420            comment = _(u"invalidated")
2421            # Here we know that the ac is in state initialized so we do not
2422            # expect an exception, but the owner might be different
2423            if not invalidate_accesscode(pin, comment, self.context.student_id):
2424                self.flash(_('You are not the owner of this access code.'))
2425                return
2426            self.context.clr_code = pin
2427        IWorkflowInfo(self.context).fireTransition('start_clearance')
2428        self.flash(_('Clearance process has been started.'))
2429        self.redirect(self.url(self.context,'cedit'))
2430        return
2431
2432class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2433    """ View to edit student clearance data by student
2434    """
2435    grok.context(IStudent)
2436    grok.name('cedit')
2437    grok.require('waeup.handleStudent')
2438    label = _('Edit clearance data')
2439
2440    @property
2441    def form_fields(self):
2442        if self.context.is_postgrad:
2443            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2444                'clearance_locked', 'clr_code', 'officer_comment')
2445        else:
2446            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2447                'clearance_locked', 'clr_code', 'officer_comment')
2448        return form_fields
2449
2450    def update(self):
2451        if self.context.clearance_locked:
2452            emit_lock_message(self)
2453            return
2454        return super(StudentClearanceEditFormPage, self).update()
2455
2456    @action(_('Save'), style='primary')
2457    def save(self, **data):
2458        self.applyData(self.context, **data)
2459        self.flash(_('Clearance form has been saved.'))
2460        return
2461
2462    def dataNotComplete(self):
2463        """To be implemented in the customization package.
2464        """
2465        return False
2466
2467    @action(_('Save and request clearance'), style='primary')
2468    def requestClearance(self, **data):
2469        self.applyData(self.context, **data)
2470        if self.dataNotComplete():
2471            self.flash(self.dataNotComplete())
2472            return
2473        self.flash(_('Clearance form has been saved.'))
2474        if self.context.clr_code:
2475            self.redirect(self.url(self.context, 'request_clearance'))
2476        else:
2477            # We bypass the request_clearance page if student
2478            # has been imported in state 'clearance started' and
2479            # no clr_code was entered before.
2480            state = IWorkflowState(self.context).getState()
2481            if state != CLEARANCE:
2482                # This shouldn't happen, but the application officer
2483                # might have forgotten to lock the form after changing the state
2484                self.flash(_('This form cannot be submitted. Wrong state!'))
2485                return
2486            IWorkflowInfo(self.context).fireTransition('request_clearance')
2487            self.flash(_('Clearance has been requested.'))
2488            self.redirect(self.url(self.context))
2489        return
2490
2491class RequestClearancePage(KofaPage):
2492    grok.context(IStudent)
2493    grok.name('request_clearance')
2494    grok.require('waeup.handleStudent')
2495    grok.template('enterpin')
2496    label = _('Request clearance')
2497    notice = _('Enter the CLR access code used for starting clearance.')
2498    ac_prefix = 'CLR'
2499    pnav = 4
2500    buttonname = _('Request clearance now')
2501    with_ac = True
2502
2503    def update(self, SUBMIT=None):
2504        if self.with_ac:
2505            self.ac_series = self.request.form.get('ac_series', None)
2506            self.ac_number = self.request.form.get('ac_number', None)
2507        if SUBMIT is None:
2508            return
2509        if self.with_ac:
2510            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2511            if self.context.clr_code and self.context.clr_code != pin:
2512                self.flash(_("This isn't your CLR access code."))
2513                return
2514        state = IWorkflowState(self.context).getState()
2515        if state != CLEARANCE:
2516            # This shouldn't happen, but the application officer
2517            # might have forgotten to lock the form after changing the state
2518            self.flash(_('This form cannot be submitted. Wrong state!'))
2519            return
2520        IWorkflowInfo(self.context).fireTransition('request_clearance')
2521        self.flash(_('Clearance has been requested.'))
2522        self.redirect(self.url(self.context))
2523        return
2524
2525class StartSessionPage(KofaPage):
2526    grok.context(IStudentStudyCourse)
2527    grok.name('start_session')
2528    grok.require('waeup.handleStudent')
2529    grok.template('enterpin')
2530    label = _('Start session')
2531    ac_prefix = 'SFE'
2532    notice = ''
2533    pnav = 4
2534    buttonname = _('Start now')
2535    with_ac = True
2536
2537    def update(self, SUBMIT=None):
2538        if not self.context.is_current:
2539            emit_lock_message(self)
2540            return
2541        super(StartSessionPage, self).update()
2542        if not self.context.next_session_allowed:
2543            self.flash(_("You are not entitled to start session."))
2544            self.redirect(self.url(self.context))
2545            return
2546        if self.with_ac:
2547            self.ac_series = self.request.form.get('ac_series', None)
2548            self.ac_number = self.request.form.get('ac_number', None)
2549        if SUBMIT is None:
2550            return
2551        if self.with_ac:
2552            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2553            code = get_access_code(pin)
2554            if not code:
2555                self.flash(_('Activation code is invalid.'))
2556                return
2557            # Mark pin as used (this also fires a pin related transition)
2558            if code.state == USED:
2559                self.flash(_('Activation code has already been used.'))
2560                return
2561            else:
2562                comment = _(u"invalidated")
2563                # Here we know that the ac is in state initialized so we do not
2564                # expect an error, but the owner might be different
2565                if not invalidate_accesscode(
2566                    pin,comment,self.context.student.student_id):
2567                    self.flash(_('You are not the owner of this access code.'))
2568                    return
2569        try:
2570            if self.context.student.state == CLEARED:
2571                IWorkflowInfo(self.context.student).fireTransition(
2572                    'pay_first_school_fee')
2573            elif self.context.student.state == RETURNING:
2574                IWorkflowInfo(self.context.student).fireTransition(
2575                    'pay_school_fee')
2576            elif self.context.student.state == PAID:
2577                IWorkflowInfo(self.context.student).fireTransition(
2578                    'pay_pg_fee')
2579        except ConstraintNotSatisfied:
2580            self.flash(_('An error occurred, please contact the system administrator.'))
2581            return
2582        self.flash(_('Session started.'))
2583        self.redirect(self.url(self.context))
2584        return
2585
2586class AddStudyLevelFormPage(KofaEditFormPage):
2587    """ Page for students to add current study levels
2588    """
2589    grok.context(IStudentStudyCourse)
2590    grok.name('add')
2591    grok.require('waeup.handleStudent')
2592    grok.template('studyleveladdpage')
2593    form_fields = grok.AutoFields(IStudentStudyCourse)
2594    pnav = 4
2595
2596    @property
2597    def label(self):
2598        studylevelsource = StudyLevelSource().factory
2599        code = self.context.current_level
2600        title = studylevelsource.getTitle(self.context, code)
2601        return _('Add current level ${a}', mapping = {'a':title})
2602
2603    def update(self):
2604        if not self.context.is_current:
2605            emit_lock_message(self)
2606            return
2607        if self.context.student.state != PAID:
2608            emit_lock_message(self)
2609            return
2610        super(AddStudyLevelFormPage, self).update()
2611        return
2612
2613    @action(_('Create course list now'), style='primary')
2614    def addStudyLevel(self, **data):
2615        studylevel = createObject(u'waeup.StudentStudyLevel')
2616        studylevel.level = self.context.current_level
2617        studylevel.level_session = self.context.current_session
2618        try:
2619            self.context.addStudentStudyLevel(
2620                self.context.certificate,studylevel)
2621        except KeyError:
2622            self.flash(_('This level exists.'))
2623        except RequiredMissing:
2624            self.flash(_('Your data are incomplete'))
2625        self.redirect(self.url(self.context))
2626        return
2627
2628class StudyLevelEditFormPage(KofaEditFormPage):
2629    """ Page to edit the student study level data by students
2630    """
2631    grok.context(IStudentStudyLevel)
2632    grok.name('edit')
2633    grok.require('waeup.editStudyLevel')
2634    grok.template('studyleveleditpage')
2635    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2636        'level_session', 'level_verdict')
2637    pnav = 4
2638
2639    def update(self, ADD=None, course=None):
2640        if not self.context.__parent__.is_current:
2641            emit_lock_message(self)
2642            return
2643        if self.context.student.state != PAID or \
2644            not self.context.is_current_level:
2645            emit_lock_message(self)
2646            return
2647        super(StudyLevelEditFormPage, self).update()
2648        datatable.need()
2649        warning.need()
2650        if ADD is not None:
2651            if not course:
2652                self.flash(_('No valid course code entered.'))
2653                return
2654            cat = queryUtility(ICatalog, name='courses_catalog')
2655            result = cat.searchResults(code=(course, course))
2656            if len(result) != 1:
2657                self.flash(_('Course not found.'))
2658                return
2659            course = list(result)[0]
2660            addCourseTicket(self, course)
2661        return
2662
2663    @property
2664    def label(self):
2665        # Here we know that the cookie has been set
2666        lang = self.request.cookies.get('kofa.language')
2667        level_title = translate(self.context.level_title, 'waeup.kofa',
2668            target_language=lang)
2669        return _('Edit course list of ${a}',
2670            mapping = {'a':level_title})
2671
2672    @property
2673    def translated_values(self):
2674        return translated_values(self)
2675
2676    def _delCourseTicket(self, **data):
2677        form = self.request.form
2678        if 'val_id' in form:
2679            child_id = form['val_id']
2680        else:
2681            self.flash(_('No ticket selected.'))
2682            self.redirect(self.url(self.context, '@@edit'))
2683            return
2684        if not isinstance(child_id, list):
2685            child_id = [child_id]
2686        deleted = []
2687        for id in child_id:
2688            # Students are not allowed to remove core tickets
2689            if id in self.context and \
2690                self.context[id].removable_by_student:
2691                del self.context[id]
2692                deleted.append(id)
2693        if len(deleted):
2694            self.flash(_('Successfully removed: ${a}',
2695                mapping = {'a':', '.join(deleted)}))
2696            self.context.writeLogMessage(
2697                self,'removed: %s at %s' %
2698                (', '.join(deleted), self.context.level))
2699        self.redirect(self.url(self.context, u'@@edit'))
2700        return
2701
2702    @jsaction(_('Remove selected tickets'))
2703    def delCourseTicket(self, **data):
2704        self._delCourseTicket(**data)
2705        return
2706
2707    def _registerCourses(self, **data):
2708        if self.context.student.is_postgrad and \
2709            not self.context.student.is_special_postgrad:
2710            self.flash(_(
2711                "You are a postgraduate student, "
2712                "your course list can't bee registered."))
2713            self.redirect(self.url(self.context))
2714            return
2715        students_utils = getUtility(IStudentsUtils)
2716        max_credits = students_utils.maxCredits(self.context)
2717        if self.context.total_credits > max_credits:
2718            self.flash(_('Maximum credits of ${a} exceeded.',
2719                mapping = {'a':max_credits}))
2720            return
2721        IWorkflowInfo(self.context.student).fireTransition(
2722            'register_courses')
2723        self.flash(_('Course list has been registered.'))
2724        self.redirect(self.url(self.context))
2725        return
2726
2727    @action(_('Register course list'))
2728    def registerCourses(self, **data):
2729        self._registerCourses(**data)
2730        return
2731
2732class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2733    """Add a course ticket by student.
2734    """
2735    grok.name('ctadd')
2736    grok.require('waeup.handleStudent')
2737    form_fields = grok.AutoFields(ICourseTicketAdd)
2738
2739    def update(self):
2740        if self.context.student.state != PAID or \
2741            not self.context.is_current_level:
2742            emit_lock_message(self)
2743            return
2744        super(CourseTicketAddFormPage2, self).update()
2745        return
2746
2747    @action(_('Add course ticket'))
2748    def addCourseTicket(self, **data):
2749        # Safety belt
2750        if self.context.student.state != PAID:
2751            return
2752        course = data['course']
2753        success = addCourseTicket(self, course)
2754        if success:
2755            self.redirect(self.url(self.context, u'@@edit'))
2756        return
2757
2758class SetPasswordPage(KofaPage):
2759    grok.context(IKofaObject)
2760    grok.name('setpassword')
2761    grok.require('waeup.Anonymous')
2762    grok.template('setpassword')
2763    label = _('Set password for first-time login')
2764    ac_prefix = 'PWD'
2765    pnav = 0
2766    set_button = _('Set')
2767
2768    def update(self, SUBMIT=None):
2769        self.reg_number = self.request.form.get('reg_number', None)
2770        self.ac_series = self.request.form.get('ac_series', None)
2771        self.ac_number = self.request.form.get('ac_number', None)
2772
2773        if SUBMIT is None:
2774            return
2775        hitlist = search(query=self.reg_number,
2776            searchtype='reg_number', view=self)
2777        if not hitlist:
2778            self.flash(_('No student found.'))
2779            return
2780        if len(hitlist) != 1:   # Cannot happen but anyway
2781            self.flash(_('More than one student found.'))
2782            return
2783        student = hitlist[0].context
2784        self.student_id = student.student_id
2785        student_pw = student.password
2786        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2787        code = get_access_code(pin)
2788        if not code:
2789            self.flash(_('Access code is invalid.'))
2790            return
2791        if student_pw and pin == student.adm_code:
2792            self.flash(_(
2793                'Password has already been set. Your Student Id is ${a}',
2794                mapping = {'a':self.student_id}))
2795            return
2796        elif student_pw:
2797            self.flash(
2798                _('Password has already been set. You are using the ' +
2799                'wrong Access Code.'))
2800            return
2801        # Mark pin as used (this also fires a pin related transition)
2802        # and set student password
2803        if code.state == USED:
2804            self.flash(_('Access code has already been used.'))
2805            return
2806        else:
2807            comment = _(u"invalidated")
2808            # Here we know that the ac is in state initialized so we do not
2809            # expect an exception
2810            invalidate_accesscode(pin,comment)
2811            IUserAccount(student).setPassword(self.ac_number)
2812            student.adm_code = pin
2813        self.flash(_('Password has been set. Your Student Id is ${a}',
2814            mapping = {'a':self.student_id}))
2815        return
2816
2817class StudentRequestPasswordPage(KofaAddFormPage):
2818    """Captcha'd registration page for applicants.
2819    """
2820    grok.name('requestpw')
2821    grok.require('waeup.Anonymous')
2822    grok.template('requestpw')
2823    form_fields = grok.AutoFields(IStudentRequestPW).select(
2824        'firstname','number','email')
2825    label = _('Request password for first-time login')
2826
2827    def update(self):
2828        # Handle captcha
2829        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2830        self.captcha_result = self.captcha.verify(self.request)
2831        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2832        return
2833
2834    def _redirect(self, email, password, student_id):
2835        # Forward only email to landing page in base package.
2836        self.redirect(self.url(self.context, 'requestpw_complete',
2837            data = dict(email=email)))
2838        return
2839
2840    def _pw_used(self):
2841        # XXX: False if password has not been used. We need an extra
2842        #      attribute which remembers if student logged in.
2843        return True
2844
2845    @action(_('Send login credentials to email address'), style='primary')
2846    def get_credentials(self, **data):
2847        if not self.captcha_result.is_valid:
2848            # Captcha will display error messages automatically.
2849            # No need to flash something.
2850            return
2851        number = data.get('number','')
2852        firstname = data.get('firstname','')
2853        cat = getUtility(ICatalog, name='students_catalog')
2854        results = list(
2855            cat.searchResults(reg_number=(number, number)))
2856        if not results:
2857            results = list(
2858                cat.searchResults(matric_number=(number, number)))
2859        if results:
2860            student = results[0]
2861            if getattr(student,'firstname',None) is None:
2862                self.flash(_('An error occurred.'))
2863                return
2864            elif student.firstname.lower() != firstname.lower():
2865                # Don't tell the truth here. Anonymous must not
2866                # know that a record was found and only the firstname
2867                # verification failed.
2868                self.flash(_('No student record found.'))
2869                return
2870            elif student.password is not None and self._pw_used:
2871                self.flash(_('Your password has already been set and used. '
2872                             'Please proceed to the login page.'))
2873                return
2874            # Store email address but nothing else.
2875            student.email = data['email']
2876            notify(grok.ObjectModifiedEvent(student))
2877        else:
2878            # No record found, this is the truth.
2879            self.flash(_('No student record found.'))
2880            return
2881
2882        kofa_utils = getUtility(IKofaUtils)
2883        password = kofa_utils.genPassword()
2884        mandate = PasswordMandate()
2885        mandate.params['password'] = password
2886        mandate.params['user'] = student
2887        site = grok.getSite()
2888        site['mandates'].addMandate(mandate)
2889        # Send email with credentials
2890        args = {'mandate_id':mandate.mandate_id}
2891        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2892        url_info = u'Confirmation link: %s' % mandate_url
2893        msg = _('You have successfully requested a password for the')
2894        if kofa_utils.sendCredentials(IUserAccount(student),
2895            password, url_info, msg):
2896            email_sent = student.email
2897        else:
2898            email_sent = None
2899        self._redirect(email=email_sent, password=password,
2900            student_id=student.student_id)
2901        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2902        self.context.logger.info(
2903            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2904        return
2905
2906class StudentRequestPasswordEmailSent(KofaPage):
2907    """Landing page after successful password request.
2908
2909    """
2910    grok.name('requestpw_complete')
2911    grok.require('waeup.Public')
2912    grok.template('requestpwmailsent')
2913    label = _('Your password request was successful.')
2914
2915    def update(self, email=None, student_id=None, password=None):
2916        self.email = email
2917        self.password = password
2918        self.student_id = student_id
2919        return
2920
2921class FilterStudentsInDepartmentPage(KofaPage):
2922    """Page that filters and lists students.
2923    """
2924    grok.context(IDepartment)
2925    grok.require('waeup.showStudents')
2926    grok.name('students')
2927    grok.template('filterstudentspage')
2928    pnav = 1
2929    session_label = _('Current Session')
2930    level_label = _('Current Level')
2931
2932    def label(self):
2933        return 'Students in %s' % self.context.longtitle()
2934
2935    def _set_session_values(self):
2936        vocab_terms = academic_sessions_vocab.by_value.values()
2937        self.sessions = sorted(
2938            [(x.title, x.token) for x in vocab_terms], reverse=True)
2939        self.sessions += [('All Sessions', 'all')]
2940        return
2941
2942    def _set_level_values(self):
2943        vocab_terms = course_levels.by_value.values()
2944        self.levels = sorted(
2945            [(x.title, x.token) for x in vocab_terms])
2946        self.levels += [('All Levels', 'all')]
2947        return
2948
2949    def _searchCatalog(self, session, level):
2950        if level not in (10, 999, None):
2951            start_level = 100 * (level // 100)
2952            end_level = start_level + 90
2953        else:
2954            start_level = end_level = level
2955        cat = queryUtility(ICatalog, name='students_catalog')
2956        students = cat.searchResults(
2957            current_session=(session, session),
2958            current_level=(start_level, end_level),
2959            depcode=(self.context.code, self.context.code)
2960            )
2961        hitlist = []
2962        for student in students:
2963            hitlist.append(StudentQueryResultItem(student, view=self))
2964        return hitlist
2965
2966    def update(self, SHOW=None, session=None, level=None):
2967        datatable.need()
2968        self.parent_url = self.url(self.context.__parent__)
2969        self._set_session_values()
2970        self._set_level_values()
2971        self.hitlist = []
2972        self.session_default = session
2973        self.level_default = level
2974        if SHOW is not None:
2975            if session != 'all':
2976                self.session = int(session)
2977                self.session_string = '%s %s/%s' % (
2978                    self.session_label, self.session, self.session+1)
2979            else:
2980                self.session = None
2981                self.session_string = _('in any session')
2982            if level != 'all':
2983                self.level = int(level)
2984                self.level_string = '%s %s' % (self.level_label, self.level)
2985            else:
2986                self.level = None
2987                self.level_string = _('at any level')
2988            self.hitlist = self._searchCatalog(self.session, self.level)
2989            if not self.hitlist:
2990                self.flash(_('No student found.'))
2991        return
2992
2993class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2994    """Page that filters and lists students.
2995    """
2996    grok.context(ICertificate)
2997
2998    def label(self):
2999        return 'Students studying %s' % self.context.longtitle()
3000
3001    def _searchCatalog(self, session, level):
3002        if level not in (10, 999, None):
3003            start_level = 100 * (level // 100)
3004            end_level = start_level + 90
3005        else:
3006            start_level = end_level = level
3007        cat = queryUtility(ICatalog, name='students_catalog')
3008        students = cat.searchResults(
3009            current_session=(session, session),
3010            current_level=(start_level, end_level),
3011            certcode=(self.context.code, self.context.code)
3012            )
3013        hitlist = []
3014        for student in students:
3015            hitlist.append(StudentQueryResultItem(student, view=self))
3016        return hitlist
3017
3018class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3019    """Page that filters and lists students.
3020    """
3021    grok.context(ICourse)
3022
3023    session_label = _('Session')
3024    level_label = _('Level')
3025
3026    def label(self):
3027        return 'Students registered for %s' % self.context.longtitle()
3028
3029    def _searchCatalog(self, session, level):
3030        if level not in (10, 999, None):
3031            start_level = 100 * (level // 100)
3032            end_level = start_level + 90
3033        else:
3034            start_level = end_level = level
3035        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3036        coursetickets = cat.searchResults(
3037            session=(session, session),
3038            level=(start_level, end_level),
3039            code=(self.context.code, self.context.code)
3040            )
3041        hitlist = []
3042        for ticket in coursetickets:
3043            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3044        return list(set(hitlist))
3045
3046class ExportJobContainerOverview(KofaPage):
3047    """Page that lists active student data export jobs and provides links
3048    to discard or download CSV files.
3049
3050    """
3051    grok.context(VirtualExportJobContainer)
3052    grok.require('waeup.showStudents')
3053    grok.name('index.html')
3054    grok.template('exportjobsindex')
3055    label = _('Student Data Exports')
3056    pnav = 1
3057
3058    def update(self, CREATE=None, DISCARD=None, job_id=None):
3059        if CREATE:
3060            self.redirect(self.url('@@exportconfig'))
3061            return
3062        if DISCARD and job_id:
3063            entry = self.context.entry_from_job_id(job_id)
3064            self.context.delete_export_entry(entry)
3065            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3066            self.context.logger.info(
3067                '%s - discarded: job_id=%s' % (ob_class, job_id))
3068            self.flash(_('Discarded export') + ' %s' % job_id)
3069        self.entries = doll_up(self, user=self.request.principal.id)
3070        return
3071
3072class ExportJobContainerJobConfig(KofaPage):
3073    """Page that configures a students export job.
3074
3075    This is a baseclass.
3076    """
3077    grok.baseclass()
3078    grok.name('exportconfig')
3079    grok.require('waeup.showStudents')
3080    grok.template('exportconfig')
3081    label = _('Configure student data export')
3082    pnav = 1
3083    redirect_target = ''
3084
3085    def _set_session_values(self):
3086        vocab_terms = academic_sessions_vocab.by_value.values()
3087        self.sessions = sorted(
3088            [(x.title, x.token) for x in vocab_terms], reverse=True)
3089        self.sessions += [(_('All Sessions'), 'all')]
3090        return
3091
3092    def _set_level_values(self):
3093        vocab_terms = course_levels.by_value.values()
3094        self.levels = sorted(
3095            [(x.title, x.token) for x in vocab_terms])
3096        self.levels += [(_('All Levels'), 'all')]
3097        return
3098
3099    def _set_mode_values(self):
3100        utils = getUtility(IKofaUtils)
3101        self.modes = sorted([(value, key) for key, value in
3102                      utils.STUDY_MODES_DICT.items()])
3103        self.modes +=[(_('All Modes'), 'all')]
3104        return
3105
3106    def _set_exporter_values(self):
3107        # We provide all student exporters, nothing else, yet.
3108        # Bursary or Department Officers don't have the general exportData
3109        # permission and are only allowed to export bursary or payments
3110        # overview data respectively. This is the only place where
3111        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3112        # are used.
3113        exporters = []
3114        if not checkPermission('waeup.exportData', self.context):
3115            if checkPermission('waeup.exportBursaryData', self.context):
3116                exporters += [('Bursary Data', 'bursary')]
3117            if checkPermission('waeup.exportPaymentsOverview', self.context):
3118                exporters += [('Student Payments Overview', 'paymentsoverview')]
3119            self.exporters = exporters
3120            return
3121        for name in EXPORTER_NAMES:
3122            util = getUtility(ICSVExporter, name=name)
3123            exporters.append((util.title, name),)
3124        self.exporters = exporters
3125        return
3126
3127    @property
3128    def depcode(self):
3129        return None
3130
3131    @property
3132    def certcode(self):
3133        return None
3134
3135    def update(self, START=None, session=None, level=None, mode=None,
3136               exporter=None):
3137        self._set_session_values()
3138        self._set_level_values()
3139        self._set_mode_values()
3140        self._set_exporter_values()
3141        if START is None:
3142            return
3143        if session == 'all':
3144            session=None
3145        if level == 'all':
3146            level = None
3147        if mode == 'all':
3148            mode = None
3149        if (mode, level, session,
3150            self.depcode, self.certcode) == (None, None, None, None, None):
3151            # Export all students including those without certificate
3152            job_id = self.context.start_export_job(exporter,
3153                                          self.request.principal.id)
3154        else:
3155            job_id = self.context.start_export_job(exporter,
3156                                          self.request.principal.id,
3157                                          current_session=session,
3158                                          current_level=level,
3159                                          current_mode=mode,
3160                                          depcode=self.depcode,
3161                                          certcode=self.certcode)
3162        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3163        self.context.logger.info(
3164            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
3165            % (ob_class, exporter, session, level, mode, self.depcode,
3166            self.certcode, job_id))
3167        self.flash(_('Export started for students with') +
3168                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3169                   session, level, mode))
3170        self.redirect(self.url(self.redirect_target))
3171        return
3172
3173class ExportJobContainerDownload(ExportCSVView):
3174    """Page that downloads a students export csv file.
3175
3176    """
3177    grok.context(VirtualExportJobContainer)
3178    grok.require('waeup.showStudents')
3179
3180class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3181    """Page that configures a students export job in datacenter.
3182
3183    """
3184    grok.context(IDataCenter)
3185    redirect_target = '@@export'
3186
3187class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3188    """Page that configures a students export job in facultiescontainer.
3189
3190    """
3191    grok.context(VirtualFacultiesExportJobContainer)
3192
3193class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3194    """Page that configures a students export job in departments.
3195
3196    """
3197    grok.context(VirtualDepartmentExportJobContainer)
3198
3199    @property
3200    def depcode(self):
3201        return self.context.__parent__.code
3202
3203class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3204    """Page that configures a students export job for certificates.
3205
3206    """
3207    grok.context(VirtualCertificateExportJobContainer)
3208    grok.template('exportconfig_certificate')
3209
3210    @property
3211    def certcode(self):
3212        return self.context.__parent__.code
3213
3214class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3215    """Page that configures a students export job for courses.
3216
3217    In contrast to department or certificate student data exports the
3218    coursetickets_catalog is searched here. Therefore the update
3219    method from the base class is customized.
3220    """
3221    grok.context(VirtualCourseExportJobContainer)
3222    grok.template('exportconfig_course')
3223
3224    def _set_exporter_values(self):
3225        # We provide only two exporters.
3226        exporters = []
3227        for name in ('students', 'coursetickets'):
3228            util = getUtility(ICSVExporter, name=name)
3229            exporters.append((util.title, name),)
3230        self.exporters = exporters
3231
3232    def update(self, START=None, session=None, level=None, mode=None,
3233               exporter=None):
3234        self._set_session_values()
3235        self._set_level_values()
3236        self._set_mode_values()
3237        self._set_exporter_values()
3238        if START is None:
3239            return
3240        if session == 'all':
3241            session = None
3242        if level == 'all':
3243            level = None
3244        job_id = self.context.start_export_job(exporter,
3245                                      self.request.principal.id,
3246                                      # Use a different catalog and
3247                                      # pass different keywords than
3248                                      # for the (default) students_catalog
3249                                      catalog='coursetickets',
3250                                      session=session,
3251                                      level=level,
3252                                      code=self.context.__parent__.code)
3253        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3254        self.context.logger.info(
3255            '%s - exported: %s (%s, %s, %s), job_id=%s'
3256            % (ob_class, exporter, session, level,
3257            self.context.__parent__.code, job_id))
3258        self.flash(_('Export started for course tickets with') +
3259                   ' level_session=%s, level=%s' % (
3260                   session, level))
3261        self.redirect(self.url(self.redirect_target))
3262        return
Note: See TracBrowser for help on using the repository browser.