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

Last change on this file since 15409 was 15405, checked in by Henrik Bettermann, 6 years ago

Remove transcript remark from course registration slip.

  • Property svn:keywords set to Id
File size: 145.8 KB
Line 
1## $Id: browser.py 15405 2019-05-08 12:59:19Z 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 csv
21import grok
22import pytz
23import sys
24from cStringIO import StringIO
25from datetime import datetime
26from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
27from urllib import urlencode
28from zope.catalog.interfaces import ICatalog
29from zope.component import queryUtility, getUtility, createObject
30from zope.event import notify
31from zope.formlib.textwidgets import BytesDisplayWidget
32from zope.i18n import translate
33from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
34from zope.security import checkPermission
35from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
36from waeup.kofa.accesscodes.workflow import USED
37from waeup.kofa.browser.breadcrumbs import Breadcrumb
38from waeup.kofa.browser.interfaces import ICaptchaManager
39from waeup.kofa.browser.layout import (
40    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
41    NullValidator, jsaction, action, UtilityView)
42from waeup.kofa.browser.pages import (
43    ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
44    LocalRoleAssignmentUtilityView)
45from waeup.kofa.hostels.hostel import NOT_OCCUPIED
46from waeup.kofa.interfaces import (
47    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
48    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
49    academic_sessions_vocab, IDataCenter, DOCLINK)
50from waeup.kofa.interfaces import MessageFactory as _
51from waeup.kofa.mandates.mandate import PasswordMandate
52from waeup.kofa.university.interfaces import (
53    IDepartment, ICertificate, ICourse)
54from waeup.kofa.university.certificate import (
55    VirtualCertificateExportJobContainer)
56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer)
58from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
59from waeup.kofa.university.facultiescontainer import (
60    VirtualFacultiesExportJobContainer)
61from waeup.kofa.university.course import (
62    VirtualCourseExportJobContainer,)
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.utils.batching import VirtualExportJobContainer
65from waeup.kofa.utils.helpers import get_current_principal, now
66from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
67from waeup.kofa.students.interfaces import (
68    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
69    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
70    IStudentStudyCourseTransfer,
71    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
72    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
73    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
74    )
75from waeup.kofa.students.catalog import search, StudentQueryResultItem
76from waeup.kofa.students.vocabularies import StudyLevelSource
77from waeup.kofa.students.workflow import (
78    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
79    VALIDATED, GRADUATED, TRANSREQ, TRANSVAL, TRANSREL, FORBIDDEN_POSTGRAD_TRANS
80    )
81
82
83grok.context(IKofaObject)  # Make IKofaObject the default context
84
85
86class TicketError(Exception):
87    """A course ticket could not be added
88    """
89    pass
90
91# Save function used for save methods in pages
92def msave(view, **data):
93    changed_fields = view.applyData(view.context, **data)
94    # Turn list of lists into single list
95    if changed_fields:
96        changed_fields = reduce(lambda x, y: x+y, changed_fields.values())
97    # Inform catalog if certificate has changed
98    # (applyData does this only for the context)
99    if 'certificate' in changed_fields:
100        notify(grok.ObjectModifiedEvent(view.context.student))
101    fields_string = ' + '.join(changed_fields)
102    view.flash(_('Form has been saved.'))
103    if fields_string:
104        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
105    return
106
107def emit_lock_message(view):
108    """Flash a lock message.
109    """
110    view.flash(_('The requested form is locked (read-only).'), type="warning")
111    view.redirect(view.url(view.context))
112    return
113
114def translated_values(view):
115    """Translate course ticket attribute values to be displayed on
116    studylevel pages.
117    """
118    lang = view.request.cookies.get('kofa.language')
119    for value in view.context.values():
120        # We have to unghostify (according to Tres Seaver) the __dict__
121        # by activating the object, otherwise value_dict will be empty
122        # when calling the first time.
123        value._p_activate()
124        value_dict = dict([i for i in value.__dict__.items()])
125        value_dict['url'] = view.url(value)
126        value_dict['removable_by_student'] = value.removable_by_student
127        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
128            target_language=lang)
129        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
130            target_language=lang)
131        value_dict['outstanding'] = translate(str(value.outstanding), 'zope',
132            target_language=lang)
133        value_dict['automatic'] = translate(str(value.automatic), 'zope',
134            target_language=lang)
135        value_dict['grade'] = value.grade
136        value_dict['weight'] = value.weight
137        value_dict['course_category'] = value.course_category
138        value_dict['total_score'] = value.total_score
139        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
140        value_dict['semester'] = semester_dict[
141            value.semester].replace('mester', 'm.')
142        yield value_dict
143
144def addCourseTicket(view, course=None):
145    students_utils = getUtility(IStudentsUtils)
146    ticket = createObject(u'waeup.CourseTicket')
147    ticket.automatic = False
148    ticket.carry_over = False
149    warning = students_utils.warnCreditsOOR(view.context, course)
150    if warning:
151        view.flash(warning, type="warning")
152        return False
153    try:
154        view.context.addCourseTicket(ticket, course)
155    except KeyError:
156        view.flash(_('The ticket exists.'), type="warning")
157        return False
158    except TicketError, error:
159        # Ticket errors are not being raised in the base package.
160        view.flash(error, type="warning")
161        return False
162    view.flash(_('Successfully added ${a}.',
163        mapping = {'a':ticket.code}))
164    view.context.writeLogMessage(
165        view,'added: %s|%s|%s' % (
166        ticket.code, ticket.level, ticket.level_session))
167    return True
168
169def level_dict(studycourse):
170    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
171    level_dict = {}
172    studylevelsource = StudyLevelSource().factory
173    for code in studylevelsource.getValues(studycourse):
174        title = translate(studylevelsource.getTitle(studycourse, code),
175            'waeup.kofa', target_language=portal_language)
176        level_dict[code] = title
177    return level_dict
178
179class StudentsBreadcrumb(Breadcrumb):
180    """A breadcrumb for the students container.
181    """
182    grok.context(IStudentsContainer)
183    title = _('Students')
184
185    @property
186    def target(self):
187        user = get_current_principal()
188        if getattr(user, 'user_type', None) == 'student':
189            return None
190        return self.viewname
191
192class StudentBreadcrumb(Breadcrumb):
193    """A breadcrumb for the student container.
194    """
195    grok.context(IStudent)
196
197    def title(self):
198        return self.context.display_fullname
199
200class SudyCourseBreadcrumb(Breadcrumb):
201    """A breadcrumb for the student study course.
202    """
203    grok.context(IStudentStudyCourse)
204
205    def title(self):
206        if self.context.is_current:
207            return _('Study Course')
208        else:
209            return _('Previous Study Course')
210
211class PaymentsBreadcrumb(Breadcrumb):
212    """A breadcrumb for the student payments folder.
213    """
214    grok.context(IStudentPaymentsContainer)
215    title = _('Payments')
216
217class OnlinePaymentBreadcrumb(Breadcrumb):
218    """A breadcrumb for payments.
219    """
220    grok.context(IStudentOnlinePayment)
221
222    @property
223    def title(self):
224        return self.context.p_id
225
226class AccommodationBreadcrumb(Breadcrumb):
227    """A breadcrumb for the student accommodation folder.
228    """
229    grok.context(IStudentAccommodation)
230    title = _('Accommodation')
231
232class BedTicketBreadcrumb(Breadcrumb):
233    """A breadcrumb for bed tickets.
234    """
235    grok.context(IBedTicket)
236
237    @property
238    def title(self):
239        return _('Bed Ticket ${a}',
240            mapping = {'a':self.context.getSessionString()})
241
242class StudyLevelBreadcrumb(Breadcrumb):
243    """A breadcrumb for course lists.
244    """
245    grok.context(IStudentStudyLevel)
246
247    @property
248    def title(self):
249        return self.context.level_title
250
251class StudentsContainerPage(KofaPage):
252    """The standard view for student containers.
253    """
254    grok.context(IStudentsContainer)
255    grok.name('index')
256    grok.require('waeup.viewStudentsContainer')
257    grok.template('containerpage')
258    label = _('Find students')
259    search_button = _('Find student(s)')
260    pnav = 4
261
262    def update(self, *args, **kw):
263        form = self.request.form
264        self.hitlist = []
265        if form.get('searchtype', None) in ('suspended', TRANSREQ, TRANSVAL):
266            self.searchtype = form['searchtype']
267            self.searchterm = None
268        elif 'searchterm' in form and form['searchterm']:
269            self.searchterm = form['searchterm']
270            self.searchtype = form['searchtype']
271        elif 'old_searchterm' in form:
272            self.searchterm = form['old_searchterm']
273            self.searchtype = form['old_searchtype']
274        else:
275            if 'search' in form:
276                self.flash(_('Empty search string'), type="warning")
277            return
278        if self.searchtype == 'current_session':
279            try:
280                self.searchterm = int(self.searchterm)
281            except ValueError:
282                self.flash(_('Only year dates allowed (e.g. 2011).'),
283                           type="danger")
284                return
285        self.hitlist = search(query=self.searchterm,
286            searchtype=self.searchtype, view=self)
287        if not self.hitlist:
288            self.flash(_('No student found.'), type="warning")
289        return
290
291class StudentsContainerManagePage(KofaPage):
292    """The manage page for student containers.
293    """
294    grok.context(IStudentsContainer)
295    grok.name('manage')
296    grok.require('waeup.manageStudent')
297    grok.template('containermanagepage')
298    pnav = 4
299    label = _('Manage students section')
300    search_button = _('Find student(s)')
301    remove_button = _('Remove selected')
302    doclink = DOCLINK + '/students.html'
303
304    def update(self, *args, **kw):
305        form = self.request.form
306        self.hitlist = []
307        if form.get('searchtype', None) in ('suspended', TRANSREQ, TRANSVAL):
308            self.searchtype = form['searchtype']
309            self.searchterm = None
310        elif 'searchterm' in form and form['searchterm']:
311            self.searchterm = form['searchterm']
312            self.searchtype = form['searchtype']
313        elif 'old_searchterm' in form:
314            self.searchterm = form['old_searchterm']
315            self.searchtype = form['old_searchtype']
316        else:
317            if 'search' in form:
318                self.flash(_('Empty search string'), type="warning")
319            return
320        if self.searchtype == 'current_session':
321            try:
322                self.searchterm = int(self.searchterm)
323            except ValueError:
324                self.flash(_('Only year dates allowed (e.g. 2011).'),
325                           type="danger")
326                return
327        if not 'entries' in form:
328            self.hitlist = search(query=self.searchterm,
329                searchtype=self.searchtype, view=self)
330            if not self.hitlist:
331                self.flash(_('No student found.'), type="warning")
332            if 'remove' in form:
333                self.flash(_('No item selected.'), type="warning")
334            return
335        entries = form['entries']
336        if isinstance(entries, basestring):
337            entries = [entries]
338        deleted = []
339        for entry in entries:
340            if 'remove' in form:
341                del self.context[entry]
342                deleted.append(entry)
343        self.hitlist = search(query=self.searchterm,
344            searchtype=self.searchtype, view=self)
345        if len(deleted):
346            self.flash(_('Successfully removed: ${a}',
347                mapping = {'a':', '.join(deleted)}))
348        return
349
350class StudentAddFormPage(KofaAddFormPage):
351    """Add-form to add a student.
352    """
353    grok.context(IStudentsContainer)
354    grok.require('waeup.manageStudent')
355    grok.name('addstudent')
356    form_fields = grok.AutoFields(IStudent).select(
357        'firstname', 'middlename', 'lastname', 'reg_number')
358    label = _('Add student')
359    pnav = 4
360
361    @action(_('Create student'), style='primary')
362    def addStudent(self, **data):
363        student = createObject(u'waeup.Student')
364        self.applyData(student, **data)
365        self.context.addStudent(student)
366        self.flash(_('Student record created.'))
367        self.redirect(self.url(self.context[student.student_id], 'index'))
368        return
369
370    @action(_('Create graduated student'), style='primary')
371    def addGraduatedStudent(self, **data):
372        student = createObject(u'waeup.Student')
373        self.applyData(student, **data)
374        self.context.addStudent(student)
375        IWorkflowState(student).setState(GRADUATED)
376        self.flash(_('Student record created.'))
377        self.redirect(self.url(self.context[student.student_id], 'index'))
378        return
379
380class LoginAsStudentStep1(KofaEditFormPage):
381    """ View to temporarily set a student password.
382    """
383    grok.context(IStudent)
384    grok.name('loginasstep1')
385    grok.require('waeup.loginAsStudent')
386    grok.template('loginasstep1')
387    pnav = 4
388
389    def label(self):
390        return _(u'Set temporary password for ${a}',
391            mapping = {'a':self.context.display_fullname})
392
393    @action('Set password now', style='primary')
394    def setPassword(self, *args, **data):
395        kofa_utils = getUtility(IKofaUtils)
396        password = kofa_utils.genPassword()
397        self.context.setTempPassword(self.request.principal.id, password)
398        self.context.writeLogMessage(
399            self, 'temp_password generated: %s' % password)
400        args = {'password':password}
401        self.redirect(self.url(self.context) +
402            '/loginasstep2?%s' % urlencode(args))
403        return
404
405class LoginAsStudentStep2(KofaPage):
406    """ View to temporarily login as student with a temporary password.
407    """
408    grok.context(IStudent)
409    grok.name('loginasstep2')
410    grok.require('waeup.Public')
411    grok.template('loginasstep2')
412    login_button = _('Login now')
413    pnav = 4
414
415    def label(self):
416        return _(u'Login as ${a}',
417            mapping = {'a':self.context.student_id})
418
419    def update(self, SUBMIT=None, password=None):
420        self.password = password
421        if SUBMIT is not None:
422            self.flash(_('You successfully logged in as student.'))
423            self.redirect(self.url(self.context))
424        return
425
426class StudentBaseDisplayFormPage(KofaDisplayFormPage):
427    """ Page to display student base data
428    """
429    grok.context(IStudent)
430    grok.name('index')
431    grok.require('waeup.viewStudent')
432    grok.template('basepage')
433    form_fields = grok.AutoFields(IStudentBase).omit(
434        'password', 'suspended', 'suspended_comment', 'flash_notice')
435    pnav = 4
436
437    @property
438    def label(self):
439        if self.context.suspended:
440            return _('${a}: Base Data (account deactivated)',
441                mapping = {'a':self.context.display_fullname})
442        return  _('${a}: Base Data',
443            mapping = {'a':self.context.display_fullname})
444
445    @property
446    def hasPassword(self):
447        if self.context.password:
448            return _('set')
449        return _('unset')
450
451    def update(self):
452        if self.context.flash_notice:
453            self.flash(self.context.flash_notice, type="warning")
454        super(StudentBaseDisplayFormPage, self).update()
455        return
456
457class StudentBasePDFFormPage(KofaDisplayFormPage):
458    """ Page to display student base data in pdf files.
459    """
460
461    def __init__(self, context, request, omit_fields=()):
462        self.omit_fields = omit_fields
463        super(StudentBasePDFFormPage, self).__init__(context, request)
464
465    @property
466    def form_fields(self):
467        form_fields = grok.AutoFields(IStudentBase)
468        for field in self.omit_fields:
469            form_fields = form_fields.omit(field)
470        return form_fields
471
472class ContactStudentFormPage(ContactAdminFormPage):
473    grok.context(IStudent)
474    grok.name('contactstudent')
475    grok.require('waeup.viewStudent')
476    pnav = 4
477    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
478
479    def update(self, subject=u'', body=u''):
480        super(ContactStudentFormPage, self).update()
481        self.form_fields.get('subject').field.default = subject
482        self.form_fields.get('body').field.default = body
483        return
484
485    def label(self):
486        return _(u'Send message to ${a}',
487            mapping = {'a':self.context.display_fullname})
488
489    @action('Send message now', style='primary')
490    def send(self, *args, **data):
491        try:
492            email = self.request.principal.email
493        except AttributeError:
494            email = self.config.email_admin
495        usertype = getattr(self.request.principal,
496                           'user_type', 'system').title()
497        kofa_utils = getUtility(IKofaUtils)
498        success = kofa_utils.sendContactForm(
499                self.request.principal.title,email,
500                self.context.display_fullname,self.context.email,
501                self.request.principal.id,usertype,
502                self.config.name,
503                data['body'],data['subject'])
504        if success:
505            self.flash(_('Your message has been sent.'))
506        else:
507            self.flash(_('An smtp server error occurred.'), type="danger")
508        return
509
510class ExportPDFAdmissionSlip(UtilityView, grok.View):
511    """Deliver a PDF Admission slip.
512    """
513    grok.context(IStudent)
514    grok.name('admission_slip.pdf')
515    grok.require('waeup.viewStudent')
516    prefix = 'form'
517
518    omit_fields = ('date_of_birth', 'current_level', 'flash_notice')
519
520    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
521
522    @property
523    def label(self):
524        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
525        return translate(_('Admission Letter of'),
526            'waeup.kofa', target_language=portal_language) \
527            + ' %s' % self.context.display_fullname
528
529    def render(self):
530        students_utils = getUtility(IStudentsUtils)
531        return students_utils.renderPDFAdmissionLetter(self,
532            self.context.student, omit_fields=self.omit_fields)
533
534class StudentBaseManageFormPage(KofaEditFormPage):
535    """ View to manage student base data
536    """
537    grok.context(IStudent)
538    grok.name('manage_base')
539    grok.require('waeup.manageStudent')
540    form_fields = grok.AutoFields(IStudentBase).omit(
541        'student_id', 'adm_code', 'suspended')
542    grok.template('basemanagepage')
543    label = _('Manage base data')
544    pnav = 4
545
546    def update(self):
547        super(StudentBaseManageFormPage, self).update()
548        self.wf_info = IWorkflowInfo(self.context)
549        return
550
551    @action(_('Save'), style='primary')
552    def save(self, **data):
553        form = self.request.form
554        password = form.get('password', None)
555        password_ctl = form.get('control_password', None)
556        if password:
557            validator = getUtility(IPasswordValidator)
558            errors = validator.validate_password(password, password_ctl)
559            if errors:
560                self.flash( ' '.join(errors), type="danger")
561                return
562        changed_fields = self.applyData(self.context, **data)
563        # Turn list of lists into single list
564        if changed_fields:
565            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
566        else:
567            changed_fields = []
568        if password:
569            # Now we know that the form has no errors and can set password
570            IUserAccount(self.context).setPassword(password)
571            changed_fields.append('password')
572        fields_string = ' + '.join(changed_fields)
573        self.flash(_('Form has been saved.'))
574        if fields_string:
575            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
576        return
577
578class StudentTriggerTransitionFormPage(KofaEditFormPage):
579    """ View to trigger student workflow transitions
580    """
581    grok.context(IStudent)
582    grok.name('trigtrans')
583    grok.require('waeup.triggerTransition')
584    grok.template('trigtrans')
585    label = _('Trigger registration transition')
586    pnav = 4
587
588    def getTransitions(self):
589        """Return a list of dicts of allowed transition ids and titles.
590
591        Each list entry provides keys ``name`` and ``title`` for
592        internal name and (human readable) title of a single
593        transition.
594        """
595        wf_info = IWorkflowInfo(self.context)
596        allowed_transitions = [t for t in wf_info.getManualTransitions()
597            if not t[0].startswith('pay')]
598        if self.context.is_postgrad and not self.context.is_special_postgrad:
599            allowed_transitions = [t for t in allowed_transitions
600                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
601        return [dict(name='', title=_('No transition'))] +[
602            dict(name=x, title=y) for x, y in allowed_transitions]
603
604    @action(_('Save'), style='primary')
605    def save(self, **data):
606        form = self.request.form
607        if 'transition' in form and form['transition']:
608            transition_id = form['transition']
609            wf_info = IWorkflowInfo(self.context)
610            wf_info.fireTransition(transition_id)
611        return
612
613class StudentActivateView(UtilityView, grok.View):
614    """ Activate student account
615    """
616    grok.context(IStudent)
617    grok.name('activate')
618    grok.require('waeup.manageStudent')
619
620    def update(self):
621        self.context.suspended = False
622        self.context.writeLogMessage(self, 'account activated')
623        history = IObjectHistory(self.context)
624        history.addMessage('Student account activated')
625        self.flash(_('Student account has been activated.'))
626        self.redirect(self.url(self.context))
627        return
628
629    def render(self):
630        return
631
632class StudentDeactivateView(UtilityView, grok.View):
633    """ Deactivate student account
634    """
635    grok.context(IStudent)
636    grok.name('deactivate')
637    grok.require('waeup.manageStudent')
638
639    def update(self):
640        self.context.suspended = True
641        self.context.writeLogMessage(self, 'account deactivated')
642        history = IObjectHistory(self.context)
643        history.addMessage('Student account deactivated')
644        self.flash(_('Student account has been deactivated.'))
645        self.redirect(self.url(self.context))
646        return
647
648    def render(self):
649        return
650
651class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
652    """ Page to display student clearance data
653    """
654    grok.context(IStudent)
655    grok.name('view_clearance')
656    grok.require('waeup.viewStudent')
657    pnav = 4
658
659    @property
660    def separators(self):
661        return getUtility(IStudentsUtils).SEPARATORS_DICT
662
663    @property
664    def form_fields(self):
665        if self.context.is_postgrad:
666            form_fields = grok.AutoFields(IPGStudentClearance)
667        else:
668            form_fields = grok.AutoFields(IUGStudentClearance)
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 ExportPDFClearanceSlip(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        'suspended', 'phone',
689        'adm_code', 'suspended_comment',
690        'date_of_birth', 'current_level',
691        'flash_notice')
692
693    @property
694    def form_fields(self):
695        if self.context.is_postgrad:
696            form_fields = grok.AutoFields(IPGStudentClearance)
697        else:
698            form_fields = grok.AutoFields(IUGStudentClearance)
699        if not getattr(self.context, 'officer_comment'):
700            form_fields = form_fields.omit('officer_comment')
701        return form_fields
702
703    @property
704    def title(self):
705        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
706        return translate(_('Clearance Data'), 'waeup.kofa',
707            target_language=portal_language)
708
709    @property
710    def label(self):
711        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
712        return translate(_('Clearance Slip of'),
713            'waeup.kofa', target_language=portal_language) \
714            + ' %s' % self.context.display_fullname
715
716    # XXX: not used in waeup.kofa and thus not tested
717    def _signatures(self):
718        isStudent = getattr(
719            self.request.principal, 'user_type', None) == 'student'
720        if not isStudent and self.context.state in (CLEARED, ):
721            return ([_('Student Signature')],
722                    [_('Clearance Officer Signature')])
723        return
724
725    def _sigsInFooter(self):
726        isStudent = getattr(
727            self.request.principal, 'user_type', None) == 'student'
728        if not isStudent and self.context.state in (CLEARED, ):
729            return (_('Date, Student Signature'),
730                    _('Date, Clearance Officer Signature'),
731                    )
732        return ()
733
734    def render(self):
735        studentview = StudentBasePDFFormPage(self.context.student,
736            self.request, self.omit_fields)
737        students_utils = getUtility(IStudentsUtils)
738        return students_utils.renderPDF(
739            self, 'clearance_slip.pdf',
740            self.context.student, studentview, signatures=self._signatures(),
741            sigs_in_footer=self._sigsInFooter(),
742            omit_fields=self.omit_fields)
743
744class StudentClearanceManageFormPage(KofaEditFormPage):
745    """ Page to manage student clearance data
746    """
747    grok.context(IStudent)
748    grok.name('manage_clearance')
749    grok.require('waeup.manageStudent')
750    grok.template('clearanceeditpage')
751    label = _('Manage clearance data')
752    deletion_warning = _('Are you sure?')
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    @action(_('Save'), style='primary')
768    def save(self, **data):
769        msave(self, **data)
770        return
771
772class StudentClearView(UtilityView, grok.View):
773    """ Clear student by clearance officer
774    """
775    grok.context(IStudent)
776    grok.name('clear')
777    grok.require('waeup.clearStudent')
778
779    def update(self):
780        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
781            self.context)
782        if cdm:
783            self.flash(cdm)
784            self.redirect(self.url(self.context,'view_clearance'))
785            return
786        if self.context.state == REQUESTED:
787            IWorkflowInfo(self.context).fireTransition('clear')
788            self.flash(_('Student has been cleared.'))
789        else:
790            self.flash(_('Student is in wrong state.'), type="warning")
791        self.redirect(self.url(self.context,'view_clearance'))
792        return
793
794    def render(self):
795        return
796
797class StudentRejectClearancePage(KofaEditFormPage):
798    """ Reject clearance by clearance officers.
799    """
800    grok.context(IStudent)
801    grok.name('reject_clearance')
802    label = _('Reject clearance')
803    grok.require('waeup.clearStudent')
804    form_fields = grok.AutoFields(
805        IUGStudentClearance).select('officer_comment')
806
807    def update(self):
808        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
809            self.context)
810        if cdm:
811            self.flash(cdm, type="warning")
812            self.redirect(self.url(self.context,'view_clearance'))
813            return
814        return super(StudentRejectClearancePage, self).update()
815
816    @action(_('Save comment and reject clearance now'), style='primary')
817    def reject(self, **data):
818        if self.context.state == CLEARED:
819            IWorkflowInfo(self.context).fireTransition('reset4')
820            message = _('Clearance has been annulled.')
821            self.flash(message, type="warning")
822        elif self.context.state == REQUESTED:
823            IWorkflowInfo(self.context).fireTransition('reset3')
824            message = _('Clearance request has been rejected.')
825            self.flash(message, type="warning")
826        else:
827            self.flash(_('Student is in wrong state.'), type="warning")
828            self.redirect(self.url(self.context,'view_clearance'))
829            return
830        self.applyData(self.context, **data)
831        comment = data['officer_comment']
832        if comment:
833            self.context.writeLogMessage(
834                self, 'comment: %s' % comment.replace('\n', '<br>'))
835            args = {'subject':message, 'body':comment}
836        else:
837            args = {'subject':message,}
838        self.redirect(self.url(self.context) +
839            '/contactstudent?%s' % urlencode(args))
840        return
841
842
843class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
844    """ Page to display student personal data
845    """
846    grok.context(IStudent)
847    grok.name('view_personal')
848    grok.require('waeup.viewStudent')
849    form_fields = grok.AutoFields(IStudentPersonal)
850    form_fields['perm_address'].custom_widget = BytesDisplayWidget
851    form_fields[
852        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
853    pnav = 4
854
855    @property
856    def label(self):
857        return _('${a}: Personal Data',
858            mapping = {'a':self.context.display_fullname})
859
860class StudentPersonalManageFormPage(KofaEditFormPage):
861    """ Page to manage personal data
862    """
863    grok.context(IStudent)
864    grok.name('manage_personal')
865    grok.require('waeup.manageStudent')
866    form_fields = grok.AutoFields(IStudentPersonal)
867    form_fields['personal_updated'].for_display = True
868    form_fields[
869        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
870    label = _('Manage personal data')
871    pnav = 4
872
873    @action(_('Save'), style='primary')
874    def save(self, **data):
875        msave(self, **data)
876        return
877
878class StudentPersonalEditFormPage(KofaEditFormPage):
879    """ Page to edit personal data
880    """
881    grok.context(IStudent)
882    grok.name('edit_personal')
883    grok.require('waeup.handleStudent')
884    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
885    label = _('Edit personal data')
886    pnav = 4
887
888    @action(_('Save/Confirm'), style='primary')
889    def save(self, **data):
890        msave(self, **data)
891        self.context.personal_updated = datetime.utcnow()
892        return
893
894class StudyCourseDisplayFormPage(KofaDisplayFormPage):
895    """ Page to display the student study course data
896    """
897    grok.context(IStudentStudyCourse)
898    grok.name('index')
899    grok.require('waeup.viewStudent')
900    grok.template('studycoursepage')
901    pnav = 4
902
903    @property
904    def form_fields(self):
905        if self.context.is_postgrad:
906            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
907                'previous_verdict')
908        else:
909            form_fields = grok.AutoFields(IStudentStudyCourse)
910        return form_fields
911
912    @property
913    def label(self):
914        if self.context.is_current:
915            return _('${a}: Study Course',
916                mapping = {'a':self.context.__parent__.display_fullname})
917        else:
918            return _('${a}: Previous Study Course',
919                mapping = {'a':self.context.__parent__.display_fullname})
920
921    @property
922    def current_mode(self):
923        if self.context.certificate is not None:
924            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
925            return studymodes_dict[self.context.certificate.study_mode]
926        return
927
928    @property
929    def department(self):
930        try:
931            if self.context.certificate is not None:
932                return self.context.certificate.__parent__.__parent__
933        except AttributeError:
934            # handle_certificate_removed does only clear
935            # studycourses with certificate code 'studycourse' but not
936            # 'studycourse_1' or 'studycourse_2'. These certificates do
937            # still exist but have no parents.
938            pass
939        return
940
941    @property
942    def faculty(self):
943        try:
944            if self.context.certificate is not None:
945                return self.context.certificate.__parent__.__parent__.__parent__
946        except AttributeError:
947            # handle_certificate_removed does only clear
948            # studycourses with certificate code 'studycourse' but not
949            # 'studycourse_1' or 'studycourse_2'. These certificates do
950            # still exist but have no parents.
951            pass
952        return
953
954    @property
955    def prev_studycourses(self):
956        if self.context.is_current:
957            if self.context.__parent__.get('studycourse_2', None) is not None:
958                return (
959                        {'href':self.url(self.context.student) + '/studycourse_1',
960                        'title':_('First Study Course, ')},
961                        {'href':self.url(self.context.student) + '/studycourse_2',
962                        'title':_('Second Study Course')}
963                        )
964            if self.context.__parent__.get('studycourse_1', None) is not None:
965                return (
966                        {'href':self.url(self.context.student) + '/studycourse_1',
967                        'title':_('First Study Course')},
968                        )
969        return
970
971class StudyCourseManageFormPage(KofaEditFormPage):
972    """ Page to edit the student study course data
973    """
974    grok.context(IStudentStudyCourse)
975    grok.name('manage')
976    grok.require('waeup.manageStudent')
977    grok.template('studycoursemanagepage')
978    label = _('Manage study course')
979    pnav = 4
980    taboneactions = [_('Save'),_('Cancel')]
981    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
982    tabthreeactions = [_('Add study level')]
983
984    @property
985    def form_fields(self):
986        if self.context.is_postgrad:
987            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
988                'previous_verdict')
989        else:
990            form_fields = grok.AutoFields(IStudentStudyCourse)
991        return form_fields
992
993    def update(self):
994        if not self.context.is_current \
995            or self.context.student.studycourse_locked:
996            emit_lock_message(self)
997            return
998        super(StudyCourseManageFormPage, self).update()
999        return
1000
1001    @action(_('Save'), style='primary')
1002    def save(self, **data):
1003        try:
1004            msave(self, **data)
1005        except ConstraintNotSatisfied:
1006            # The selected level might not exist in certificate
1007            self.flash(_('Current level not available for certificate.'),
1008                       type="warning")
1009            return
1010        notify(grok.ObjectModifiedEvent(self.context.__parent__))
1011        return
1012
1013    @property
1014    def level_dicts(self):
1015        studylevelsource = StudyLevelSource().factory
1016        for code in studylevelsource.getValues(self.context):
1017            title = studylevelsource.getTitle(self.context, code)
1018            yield(dict(code=code, title=title))
1019
1020    @property
1021    def session_dicts(self):
1022        yield(dict(code='', title='--'))
1023        for item in academic_sessions():
1024            code = item[1]
1025            title = item[0]
1026            yield(dict(code=code, title=title))
1027
1028    @action(_('Add study level'), style='primary')
1029    def addStudyLevel(self, **data):
1030        level_code = self.request.form.get('addlevel', None)
1031        level_session = self.request.form.get('level_session', None)
1032        if not level_session and not level_code == '0':
1033            self.flash(_('You must select a session for the level.'),
1034                       type="warning")
1035            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1036            return
1037        if level_session and level_code == '0':
1038            self.flash(_('Level zero must not be assigned a session.'),
1039                       type="warning")
1040            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1041            return
1042        studylevel = createObject(u'waeup.StudentStudyLevel')
1043        studylevel.level = int(level_code)
1044        if level_code != '0':
1045            studylevel.level_session = int(level_session)
1046        try:
1047            self.context.addStudentStudyLevel(
1048                self.context.certificate,studylevel)
1049            self.flash(_('Study level has been added.'))
1050        except KeyError:
1051            self.flash(_('This level exists.'), type="warning")
1052        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1053        return
1054
1055    @jsaction(_('Remove selected levels'))
1056    def delStudyLevels(self, **data):
1057        form = self.request.form
1058        if 'val_id' in form:
1059            child_id = form['val_id']
1060        else:
1061            self.flash(_('No study level selected.'), type="warning")
1062            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1063            return
1064        if not isinstance(child_id, list):
1065            child_id = [child_id]
1066        deleted = []
1067        for id in child_id:
1068            del self.context[id]
1069            deleted.append(id)
1070        if len(deleted):
1071            self.flash(_('Successfully removed: ${a}',
1072                mapping = {'a':', '.join(deleted)}))
1073            self.context.writeLogMessage(
1074                self,'removed: %s' % ', '.join(deleted))
1075        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1076        return
1077
1078class StudentTranscriptRequestPage(KofaPage):
1079    """ Page to request transcript by student
1080    """
1081    grok.context(IStudent)
1082    grok.name('request_transcript')
1083    grok.require('waeup.handleStudent')
1084    grok.template('transcriptrequest')
1085    label = _('Request transcript')
1086    ac_prefix = 'TSC'
1087    notice = ''
1088    pnav = 4
1089    buttonname = _('Submit')
1090    with_ac = True
1091
1092    def update(self, SUBMIT=None):
1093        super(StudentTranscriptRequestPage, self).update()
1094        if not self.context.state == GRADUATED:
1095            self.flash(_("Wrong state"), type="danger")
1096            self.redirect(self.url(self.context))
1097            return
1098        if self.with_ac:
1099            self.ac_series = self.request.form.get('ac_series', None)
1100            self.ac_number = self.request.form.get('ac_number', None)
1101        if getattr(
1102            self.context['studycourse'], 'transcript_comment', None) is not None:
1103            self.correspondence = self.context[
1104                'studycourse'].transcript_comment.replace(
1105                    '\n', '<br>')
1106        else:
1107            self.correspondence = ''
1108        if SUBMIT is None:
1109            return
1110        if self.with_ac:
1111            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1112            code = get_access_code(pin)
1113            if not code:
1114                self.flash(_('Activation code is invalid.'), type="warning")
1115                return
1116            if code.state == USED:
1117                self.flash(_('Activation code has already been used.'),
1118                           type="warning")
1119                return
1120            # Mark pin as used (this also fires a pin related transition)
1121            # and fire transition request_transcript
1122            comment = _(u"invalidated")
1123            # Here we know that the ac is in state initialized so we do not
1124            # expect an exception, but the owner might be different
1125            if not invalidate_accesscode(pin, comment, self.context.student_id):
1126                self.flash(_('You are not the owner of this access code.'),
1127                           type="warning")
1128                return
1129            self.context.clr_code = pin
1130        IWorkflowInfo(self.context).fireTransition('request_transcript')
1131        comment = self.request.form.get('comment', '').replace('\r', '')
1132        address = self.request.form.get('address', '').replace('\r', '')
1133        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1134        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1135        old_transcript_comment = getattr(
1136            self.context['studycourse'], 'transcript_comment', None)
1137        if old_transcript_comment == None:
1138            old_transcript_comment = ''
1139        self.context['studycourse'].transcript_comment = '''On %s %s wrote:
1140
1141%s
1142
1143Dispatch Address:
1144%s
1145
1146%s''' % (today, self.request.principal.id, comment, address,
1147         old_transcript_comment)
1148        self.context.writeLogMessage(
1149            self, 'comment: %s' % comment.replace('\n', '<br>'))
1150        self.flash(_('Transcript processing has been started.'))
1151        self.redirect(self.url(self.context))
1152        return
1153
1154class StudentTranscriptSignView(UtilityView, grok.View):
1155    """ View to sign transcript
1156    """
1157    grok.context(IStudentStudyCourse)
1158    grok.name('sign_transcript')
1159    grok.require('waeup.signTranscript')
1160
1161    def update(self, SUBMIT=None):
1162        if self.context.student.state != TRANSVAL:
1163            self.flash(_('Student is in wrong state.'), type="warning")
1164            self.redirect(self.url(self.context))
1165            return
1166        prev_transcript_signees = getattr(
1167            self.context, 'transcript_signees', None)
1168        if prev_transcript_signees \
1169            and '(%s)' % self.request.principal.id in prev_transcript_signees:
1170            self.flash(_('You have already signed this transcript.'),
1171                type="warning")
1172            self.redirect(self.url(self.context) + '/transcript')
1173            return
1174        self.flash(_('Transcript signed.'))
1175        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1176        self.context.student.__parent__.logger.info(
1177            '%s - %s - Transcript signed'
1178            % (ob_class, self.context.student.student_id))
1179        self.context.student.history.addMessage('Transcript signed')
1180        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1181        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1182        if prev_transcript_signees == None:
1183            prev_transcript_signees = ''
1184        self.context.transcript_signees = (
1185            u"Electronically signed by %s (%s) on %s\n%s"
1186            % (self.request.principal.title, self.request.principal.id, today,
1187            prev_transcript_signees))
1188        self.redirect(self.url(self.context) + '/transcript')
1189        return
1190
1191    def render(self):
1192        return
1193
1194class StudentTranscriptValidateFormPage(KofaEditFormPage):
1195    """ Page to validate transcript
1196    """
1197    grok.context(IStudentStudyCourse)
1198    grok.name('validate_transcript')
1199    grok.require('waeup.processTranscript')
1200    grok.template('transcriptprocess')
1201    label = _('Validate transcript')
1202    buttonname = _('Save comment and validate transcript')
1203    pnav = 4
1204
1205    @property
1206    def remarks(self):
1207        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1208        levelremarks = ''
1209        studylevelsource = StudyLevelSource().factory
1210        for studylevel in self.context.values():
1211            leveltitle = studylevelsource.getTitle(
1212                self.context, studylevel.level)
1213            url = self.url(self.context) + '/%s/remark' % studylevel.level
1214            button_title = translate(
1215                _('Edit'), 'waeup.kofa', target_language=portal_language)
1216            levelremarks += (
1217                '<tr>'
1218                '<td>%s:</td>'
1219                '<td>%s</td> '
1220                '<td><a class="btn btn-primary btn-xs" href="%s">%s</a></td>'
1221                '</tr>'
1222                ) % (
1223                leveltitle, studylevel.transcript_remark, url, button_title)
1224        return levelremarks
1225
1226    def update(self, SUBMIT=None):
1227        super(StudentTranscriptValidateFormPage, self).update()
1228        if self.context.student.state != TRANSREQ:
1229            self.flash(_('Student is in wrong state.'), type="warning")
1230            self.redirect(self.url(self.context))
1231            return
1232        if getattr(self.context, 'transcript_comment', None) is not None:
1233            self.correspondence = self.context.transcript_comment.replace(
1234                '\n', '<br>')
1235        else:
1236            self.correspondence = ''
1237        if getattr(self.context, 'transcript_signees', None) is not None:
1238            self.signees = self.context.transcript_signees.replace(
1239                '\n', '<br><br>')
1240        else:
1241            self.signees = ''
1242        if SUBMIT is None:
1243            return
1244        # Fire transition
1245        IWorkflowInfo(self.context.student).fireTransition('validate_transcript')
1246        self.flash(_('Transcript validated.'))
1247        comment = self.request.form.get('comment', '').replace('\r', '')
1248        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1249        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1250        old_transcript_comment = getattr(
1251            self.context, 'transcript_comment', None)
1252        if old_transcript_comment == None:
1253            old_transcript_comment = ''
1254        self.context.transcript_comment = '''On %s %s wrote:
1255
1256%s
1257
1258%s''' % (today, self.request.principal.id, comment,
1259         old_transcript_comment)
1260        self.context.writeLogMessage(
1261            self, 'comment: %s' % comment.replace('\n', '<br>'))
1262        self.redirect(self.url(self.context) + '/transcript')
1263        return
1264
1265class StudentTranscriptReleaseFormPage(KofaEditFormPage):
1266    """ Page to release transcript
1267    """
1268    grok.context(IStudentStudyCourse)
1269    grok.name('release_transcript')
1270    grok.require('waeup.processTranscript')
1271    grok.template('transcriptprocess')
1272    label = _('Release transcript')
1273    buttonname = _('Save comment and release transcript')
1274    pnav = 4
1275
1276    @property
1277    def remarks(self):
1278        levelremarks = ''
1279        studylevelsource = StudyLevelSource().factory
1280        for studylevel in self.context.values():
1281            leveltitle = studylevelsource.getTitle(
1282                self.context, studylevel.level)
1283            levelremarks += "%s: %s <br><br>" % (
1284                leveltitle, studylevel.transcript_remark)
1285        return levelremarks
1286
1287    def update(self, SUBMIT=None):
1288        super(StudentTranscriptReleaseFormPage, self).update()
1289        if self.context.student.state != TRANSVAL:
1290            self.flash(_('Student is in wrong state.'), type="warning")
1291            self.redirect(self.url(self.context))
1292            return
1293        if getattr(self.context, 'transcript_comment', None) is not None:
1294            self.correspondence = self.context.transcript_comment.replace(
1295                '\n', '<br>')
1296        else:
1297            self.correspondence = ''
1298        if getattr(self.context, 'transcript_signees', None) is not None:
1299            self.signees = self.context.transcript_signees.replace(
1300                '\n', '<br><br>')
1301        else:
1302            self.signees = ''
1303        if SUBMIT is None:
1304            return
1305        # Fire transition
1306        IWorkflowInfo(self.context.student).fireTransition('release_transcript')
1307        self.flash(_('Transcript released and final transcript file saved.'))
1308        comment = self.request.form.get('comment', '').replace('\r', '')
1309        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1310        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1311        old_transcript_comment = getattr(
1312            self.context, 'transcript_comment', None)
1313        if old_transcript_comment == None:
1314            old_transcript_comment = ''
1315        self.context.transcript_comment = '''On %s %s wrote:
1316
1317%s
1318
1319%s''' % (today, self.request.principal.id, comment,
1320         old_transcript_comment)
1321        self.context.writeLogMessage(
1322            self, 'comment: %s' % comment.replace('\n', '<br>'))
1323        # Produce transcript file
1324        self.redirect(self.url(self.context) + '/transcript.pdf')
1325        return
1326
1327class StudyCourseTranscriptPage(KofaDisplayFormPage):
1328    """ Page to display the student's transcript.
1329    """
1330    grok.context(IStudentStudyCourse)
1331    grok.name('transcript')
1332    grok.require('waeup.viewTranscript')
1333    grok.template('transcript')
1334    pnav = 4
1335
1336    def update(self):
1337        final_slip = getUtility(IExtFileStore).getFileByContext(
1338            self.context.student, attr='final_transcript')
1339        if not self.context.student.transcript_enabled or final_slip:
1340            self.flash(_('Forbidden!'), type="warning")
1341            self.redirect(self.url(self.context))
1342            return
1343        super(StudyCourseTranscriptPage, self).update()
1344        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1345        self.level_dict = level_dict(self.context)
1346        self.session_dict = dict([(None, 'None'),] +
1347            [(item[1], item[0]) for item in academic_sessions()])
1348        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1349        return
1350
1351    @property
1352    def label(self):
1353        # Here we know that the cookie has been set
1354        return _('${a}: Transcript Data', mapping = {
1355            'a':self.context.student.display_fullname})
1356
1357class ExportPDFTranscriptSlip(UtilityView, grok.View):
1358    """Deliver a PDF slip of the context.
1359    """
1360    grok.context(IStudentStudyCourse)
1361    grok.name('transcript.pdf')
1362    grok.require('waeup.viewTranscript')
1363    prefix = 'form'
1364    omit_fields = (
1365        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1366        'password', 'suspended', 'phone', 'email',
1367        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1368
1369    def update(self):
1370        final_slip = getUtility(IExtFileStore).getFileByContext(
1371            self.context.student, attr='final_transcript')
1372        if not self.context.student.transcript_enabled \
1373            or final_slip:
1374            self.flash(_('Forbidden!'), type="warning")
1375            self.redirect(self.url(self.context))
1376            return
1377        super(ExportPDFTranscriptSlip, self).update()
1378        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1379        self.level_dict = level_dict(self.context)
1380        self.session_dict = dict([(None, 'None'),] +
1381            [(item[1], item[0]) for item in academic_sessions()])
1382        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1383        return
1384
1385    @property
1386    def label(self):
1387        # Here we know that the cookie has been set
1388        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1389        return translate(_('Academic Transcript'),
1390            'waeup.kofa', target_language=portal_language)
1391
1392    def _sigsInFooter(self):
1393        if getattr(
1394            self.context.student['studycourse'], 'transcript_signees', None):
1395            return ()
1396        return (_('CERTIFIED TRUE COPY'),)
1397
1398    def _signatures(self):
1399        return ()
1400
1401    def _digital_sigs(self):
1402        if getattr(
1403            self.context.student['studycourse'], 'transcript_signees', None):
1404            return self.context.student['studycourse'].transcript_signees
1405        return ()
1406
1407    def _save_file(self):
1408        if self.context.student.state == TRANSREL:
1409            return True
1410        return False
1411
1412    def render(self):
1413        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1414        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1415        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1416        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1417        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1418        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1419        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1420        studentview = StudentBasePDFFormPage(self.context.student,
1421            self.request, self.omit_fields)
1422        students_utils = getUtility(IStudentsUtils)
1423
1424        tableheader = [(Code,'code', 2.5),
1425                         (Title,'title', 7),
1426                         (Term, 'semester', 1.5),
1427                         (Cred, 'credits', 1.5),
1428                         (Score, 'total_score', 1.5),
1429                         (Grade, 'grade', 1.5),
1430                         ]
1431
1432        pdfstream = students_utils.renderPDFTranscript(
1433            self, 'transcript.pdf',
1434            self.context.student, studentview,
1435            omit_fields=self.omit_fields,
1436            tableheader=tableheader,
1437            signatures=self._signatures(),
1438            sigs_in_footer=self._sigsInFooter(),
1439            digital_sigs=self._digital_sigs(),
1440            save_file=self._save_file(),
1441            )
1442        if not pdfstream:
1443            self.redirect(self.url(self.context.student))
1444            return
1445        return pdfstream
1446
1447class StudentTransferFormPage(KofaAddFormPage):
1448    """Page to transfer the student.
1449    """
1450    grok.context(IStudent)
1451    grok.name('transfer')
1452    grok.require('waeup.manageStudent')
1453    label = _('Transfer student')
1454    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1455        'entry_mode', 'entry_session')
1456    pnav = 4
1457
1458    @jsaction(_('Transfer'))
1459    def transferStudent(self, **data):
1460        error = self.context.transfer(**data)
1461        if error == -1:
1462            self.flash(_('Current level does not match certificate levels.'),
1463                       type="warning")
1464        elif error == -2:
1465            self.flash(_('Former study course record incomplete.'),
1466                       type="warning")
1467        elif error == -3:
1468            self.flash(_('Maximum number of transfers exceeded.'),
1469                       type="warning")
1470        else:
1471            self.flash(_('Successfully transferred.'))
1472        return
1473
1474class RevertTransferFormPage(KofaEditFormPage):
1475    """View that reverts the previous transfer.
1476    """
1477    grok.context(IStudent)
1478    grok.name('revert_transfer')
1479    grok.require('waeup.manageStudent')
1480    grok.template('reverttransfer')
1481    label = _('Revert previous transfer')
1482
1483    def update(self):
1484        if not self.context.has_key('studycourse_1'):
1485            self.flash(_('No previous transfer.'), type="warning")
1486            self.redirect(self.url(self.context))
1487            return
1488        return
1489
1490    @jsaction(_('Revert now'))
1491    def transferStudent(self, **data):
1492        self.context.revert_transfer()
1493        self.flash(_('Previous transfer reverted.'))
1494        self.redirect(self.url(self.context, 'studycourse'))
1495        return
1496
1497class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1498    """ Page to display student study levels
1499    """
1500    grok.context(IStudentStudyLevel)
1501    grok.name('index')
1502    grok.require('waeup.viewStudent')
1503    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1504    form_fields[
1505        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1506    grok.template('studylevelpage')
1507    pnav = 4
1508
1509    def update(self):
1510        super(StudyLevelDisplayFormPage, self).update()
1511        if self.context.level == 0:
1512            self.form_fields = self.form_fields.omit('gpa')
1513        return
1514
1515    @property
1516    def translated_values(self):
1517        return translated_values(self)
1518
1519    @property
1520    def label(self):
1521        # Here we know that the cookie has been set
1522        lang = self.request.cookies.get('kofa.language')
1523        level_title = translate(self.context.level_title, 'waeup.kofa',
1524            target_language=lang)
1525        return _('${a}: ${b}', mapping = {
1526            'a':self.context.student.display_fullname,
1527            'b':level_title})
1528
1529class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1530    """Deliver a PDF slip of the context.
1531    """
1532    grok.context(IStudentStudyLevel)
1533    grok.name('course_registration_slip.pdf')
1534    grok.require('waeup.viewStudent')
1535    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1536        'level', 'gpa', 'transcript_remark')
1537    form_fields[
1538        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1539    prefix = 'form'
1540    omit_fields = (
1541        'password', 'suspended', 'phone', 'date_of_birth',
1542        'adm_code', 'sex', 'suspended_comment', 'current_level',
1543        'flash_notice')
1544
1545    @property
1546    def title(self):
1547        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1548        return translate(_('Level Data'), 'waeup.kofa',
1549            target_language=portal_language)
1550
1551    @property
1552    def label(self):
1553        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1554        lang = self.request.cookies.get('kofa.language', portal_language)
1555        level_title = translate(self.context.level_title, 'waeup.kofa',
1556            target_language=lang)
1557        return translate(_('Course Registration Slip'),
1558            'waeup.kofa', target_language=portal_language) \
1559            + ' %s' % level_title
1560
1561    @property
1562    def tabletitle(self):
1563        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1564        tabletitle = []
1565        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1566            target_language=portal_language))
1567        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1568            target_language=portal_language))
1569        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1570            target_language=portal_language))
1571        return tabletitle
1572
1573    def render(self):
1574        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1575        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1576        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1577        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1578        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1579        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1580        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1581        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1582        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1583        studentview = StudentBasePDFFormPage(self.context.student,
1584            self.request, self.omit_fields)
1585        students_utils = getUtility(IStudentsUtils)
1586
1587        tabledata = []
1588        tableheader = []
1589        for i in range(1,7):
1590            tabledata.append(sorted(
1591                [value for value in self.context.values() if value.semester == i],
1592                key=lambda value: str(value.semester) + value.code))
1593            tableheader.append([(Code,'code', 2.5),
1594                             (Title,'title', 5),
1595                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1596                             (Cred, 'credits', 1.5),
1597                             #(Mand, 'mandatory', 1.5),
1598                             (Score, 'score', 1.5),
1599                             (Grade, 'grade', 1.5),
1600                             #('Auto', 'automatic', 1.5)
1601                             ])
1602        return students_utils.renderPDF(
1603            self, 'course_registration_slip.pdf',
1604            self.context.student, studentview,
1605            tableheader=tableheader,
1606            tabledata=tabledata,
1607            omit_fields=self.omit_fields
1608            )
1609
1610class StudyLevelManageFormPage(KofaEditFormPage):
1611    """ Page to edit the student study level data
1612    """
1613    grok.context(IStudentStudyLevel)
1614    grok.name('manage')
1615    grok.require('waeup.manageStudent')
1616    grok.template('studylevelmanagepage')
1617    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1618        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1619    pnav = 4
1620    taboneactions = [_('Save'),_('Cancel')]
1621    tabtwoactions = [_('Add course ticket'),
1622        _('Remove selected tickets'),_('Cancel')]
1623    placeholder = _('Enter valid course code')
1624
1625    def update(self, ADD=None, course=None):
1626        if not self.context.__parent__.is_current \
1627            or self.context.student.studycourse_locked:
1628            emit_lock_message(self)
1629            return
1630        super(StudyLevelManageFormPage, self).update()
1631        if ADD is not None:
1632            if not course:
1633                self.flash(_('No valid course code entered.'), type="warning")
1634                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1635                return
1636            cat = queryUtility(ICatalog, name='courses_catalog')
1637            result = cat.searchResults(code=(course, course))
1638            if len(result) != 1:
1639                self.flash(_('Course not found.'), type="warning")
1640            else:
1641                course = list(result)[0]
1642                addCourseTicket(self, course)
1643            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1644        return
1645
1646    @property
1647    def translated_values(self):
1648        return translated_values(self)
1649
1650    @property
1651    def label(self):
1652        # Here we know that the cookie has been set
1653        lang = self.request.cookies.get('kofa.language')
1654        level_title = translate(self.context.level_title, 'waeup.kofa',
1655            target_language=lang)
1656        return _('Manage ${a}',
1657            mapping = {'a':level_title})
1658
1659    @action(_('Save'), style='primary')
1660    def save(self, **data):
1661        msave(self, **data)
1662        return
1663
1664    @jsaction(_('Remove selected tickets'))
1665    def delCourseTicket(self, **data):
1666        form = self.request.form
1667        if 'val_id' in form:
1668            child_id = form['val_id']
1669        else:
1670            self.flash(_('No ticket selected.'), type="warning")
1671            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1672            return
1673        if not isinstance(child_id, list):
1674            child_id = [child_id]
1675        deleted = []
1676        for id in child_id:
1677            del self.context[id]
1678            deleted.append(id)
1679        if len(deleted):
1680            self.flash(_('Successfully removed: ${a}',
1681                mapping = {'a':', '.join(deleted)}))
1682            self.context.writeLogMessage(
1683                self,'removed: %s at %s' %
1684                (', '.join(deleted), self.context.level))
1685        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1686        return
1687
1688class StudyLevelRemarkFormPage(KofaEditFormPage):
1689    """ Page to edit the student study level transcript remark only
1690    """
1691    grok.context(IStudentStudyLevel)
1692    grok.name('remark')
1693    grok.require('waeup.processTranscript')
1694    grok.template('studylevelremarkpage')
1695    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1696    form_fields['level_session'].for_display = True
1697    form_fields['level_verdict'].for_display = True
1698    form_fields['validation_date'].for_display = True
1699    form_fields['validated_by'].for_display = True
1700
1701    def update(self, ADD=None, course=None):
1702        if self.context.student.studycourse_locked:
1703            emit_lock_message(self)
1704            return
1705        super(StudyLevelRemarkFormPage, self).update()
1706
1707    @property
1708    def label(self):
1709        lang = self.request.cookies.get('kofa.language')
1710        level_title = translate(self.context.level_title, 'waeup.kofa',
1711            target_language=lang)
1712        return _(
1713            'Edit transcript remark of level ${a}', mapping = {'a':level_title})
1714
1715    @property
1716    def translated_values(self):
1717        return translated_values(self)
1718
1719    @action(_('Save remark and go and back to transcript validation page'),
1720        style='primary')
1721    def save(self, **data):
1722        msave(self, **data)
1723        self.redirect(self.url(self.context.student)
1724            + '/studycourse/validate_transcript#tab4')
1725        return
1726
1727class ValidateCoursesView(UtilityView, grok.View):
1728    """ Validate course list by course adviser
1729    """
1730    grok.context(IStudentStudyLevel)
1731    grok.name('validate_courses')
1732    grok.require('waeup.validateStudent')
1733
1734    def update(self):
1735        if not self.context.__parent__.is_current:
1736            emit_lock_message(self)
1737            return
1738        if str(self.context.student.current_level) != self.context.__name__:
1739            self.flash(_('This is not the student\'s current level.'),
1740                       type="danger")
1741        elif self.context.student.state == REGISTERED:
1742            IWorkflowInfo(self.context.student).fireTransition(
1743                'validate_courses')
1744            self.flash(_('Course list has been validated.'))
1745        else:
1746            self.flash(_('Student is in the wrong state.'), type="warning")
1747        self.redirect(self.url(self.context))
1748        return
1749
1750    def render(self):
1751        return
1752
1753class RejectCoursesView(UtilityView, grok.View):
1754    """ Reject course list by course adviser
1755    """
1756    grok.context(IStudentStudyLevel)
1757    grok.name('reject_courses')
1758    grok.require('waeup.validateStudent')
1759
1760    def update(self):
1761        if not self.context.__parent__.is_current:
1762            emit_lock_message(self)
1763            return
1764        if str(self.context.__parent__.current_level) != self.context.__name__:
1765            self.flash(_('This is not the student\'s current level.'),
1766                       type="danger")
1767            self.redirect(self.url(self.context))
1768            return
1769        elif self.context.student.state == VALIDATED:
1770            IWorkflowInfo(self.context.student).fireTransition('reset8')
1771            message = _('Course list request has been annulled.')
1772            self.flash(message)
1773        elif self.context.student.state == REGISTERED:
1774            IWorkflowInfo(self.context.student).fireTransition('reset7')
1775            message = _('Course list has been unregistered.')
1776            self.flash(message)
1777        else:
1778            self.flash(_('Student is in the wrong state.'), type="warning")
1779            self.redirect(self.url(self.context))
1780            return
1781        args = {'subject':message}
1782        self.redirect(self.url(self.context.student) +
1783            '/contactstudent?%s' % urlencode(args))
1784        return
1785
1786    def render(self):
1787        return
1788
1789class UnregisterCoursesView(UtilityView, grok.View):
1790    """Unregister course list by student
1791    """
1792    grok.context(IStudentStudyLevel)
1793    grok.name('unregister_courses')
1794    grok.require('waeup.handleStudent')
1795
1796    def update(self):
1797        if not self.context.__parent__.is_current:
1798            emit_lock_message(self)
1799            return
1800        try:
1801            deadline = grok.getSite()['configuration'][
1802                str(self.context.level_session)].coursereg_deadline
1803        except (TypeError, KeyError):
1804            deadline = None
1805        if deadline and deadline < datetime.now(pytz.utc):
1806            self.flash(_(
1807                "Course registration has ended. "
1808                "Unregistration is disabled."), type="warning")
1809        elif str(self.context.__parent__.current_level) != self.context.__name__:
1810            self.flash(_('This is not your current level.'), type="danger")
1811        elif self.context.student.state == REGISTERED:
1812            IWorkflowInfo(self.context.student).fireTransition('reset7')
1813            message = _('Course list has been unregistered.')
1814            self.flash(message)
1815        else:
1816            self.flash(_('You are in the wrong state.'), type="warning")
1817        self.redirect(self.url(self.context))
1818        return
1819
1820    def render(self):
1821        return
1822
1823class CourseTicketAddFormPage(KofaAddFormPage):
1824    """Add a course ticket.
1825    """
1826    grok.context(IStudentStudyLevel)
1827    grok.name('add')
1828    grok.require('waeup.manageStudent')
1829    label = _('Add course ticket')
1830    form_fields = grok.AutoFields(ICourseTicketAdd)
1831    pnav = 4
1832
1833    def update(self):
1834        if not self.context.__parent__.is_current \
1835            or self.context.student.studycourse_locked:
1836            emit_lock_message(self)
1837            return
1838        super(CourseTicketAddFormPage, self).update()
1839        return
1840
1841    @action(_('Add course ticket'), style='primary')
1842    def addCourseTicket(self, **data):
1843        course = data['course']
1844        success = addCourseTicket(self, course)
1845        if success:
1846            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1847        return
1848
1849    @action(_('Cancel'), validator=NullValidator)
1850    def cancel(self, **data):
1851        self.redirect(self.url(self.context))
1852
1853class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1854    """ Page to display course tickets
1855    """
1856    grok.context(ICourseTicket)
1857    grok.name('index')
1858    grok.require('waeup.viewStudent')
1859    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
1860        'ticket_session')
1861    grok.template('courseticketpage')
1862    pnav = 4
1863
1864    @property
1865    def label(self):
1866        return _('${a}: Course Ticket ${b}', mapping = {
1867            'a':self.context.student.display_fullname,
1868            'b':self.context.code})
1869
1870class CourseTicketManageFormPage(KofaEditFormPage):
1871    """ Page to manage course tickets
1872    """
1873    grok.context(ICourseTicket)
1874    grok.name('manage')
1875    grok.require('waeup.manageStudent')
1876    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1877    form_fields['title'].for_display = True
1878    form_fields['fcode'].for_display = True
1879    form_fields['dcode'].for_display = True
1880    form_fields['semester'].for_display = True
1881    form_fields['passmark'].for_display = True
1882    form_fields['credits'].for_display = True
1883    form_fields['mandatory'].for_display = False
1884    form_fields['automatic'].for_display = True
1885    form_fields['carry_over'].for_display = True
1886    form_fields['ticket_session'].for_display = True
1887    pnav = 4
1888    grok.template('courseticketmanagepage')
1889
1890    def update(self):
1891        if not self.context.__parent__.__parent__.is_current \
1892            or self.context.student.studycourse_locked:
1893            emit_lock_message(self)
1894            return
1895        super(CourseTicketManageFormPage, self).update()
1896        return
1897
1898    @property
1899    def label(self):
1900        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1901
1902    @action('Save', style='primary')
1903    def save(self, **data):
1904        msave(self, **data)
1905        return
1906
1907class PaymentsManageFormPage(KofaEditFormPage):
1908    """ Page to manage the student payments
1909
1910    This manage form page is for both students and students officers.
1911    """
1912    grok.context(IStudentPaymentsContainer)
1913    grok.name('index')
1914    grok.require('waeup.viewStudent')
1915    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1916    grok.template('paymentsmanagepage')
1917    pnav = 4
1918
1919    @property
1920    def manage_payments_allowed(self):
1921        return checkPermission('waeup.payStudent', self.context)
1922
1923    def unremovable(self, ticket):
1924        usertype = getattr(self.request.principal, 'user_type', None)
1925        if not usertype:
1926            return False
1927        if not self.manage_payments_allowed:
1928            return True
1929        return (self.request.principal.user_type == 'student' and ticket.r_code)
1930
1931    @property
1932    def label(self):
1933        return _('${a}: Payments',
1934            mapping = {'a':self.context.__parent__.display_fullname})
1935
1936    @jsaction(_('Remove selected tickets'))
1937    def delPaymentTicket(self, **data):
1938        form = self.request.form
1939        if 'val_id' in form:
1940            child_id = form['val_id']
1941        else:
1942            self.flash(_('No payment selected.'), type="warning")
1943            self.redirect(self.url(self.context))
1944            return
1945        if not isinstance(child_id, list):
1946            child_id = [child_id]
1947        deleted = []
1948        for id in child_id:
1949            # Students are not allowed to remove used payment tickets
1950            ticket = self.context.get(id, None)
1951            if ticket is not None and not self.unremovable(ticket):
1952                del self.context[id]
1953                deleted.append(id)
1954        if len(deleted):
1955            self.flash(_('Successfully removed: ${a}',
1956                mapping = {'a': ', '.join(deleted)}))
1957            self.context.writeLogMessage(
1958                self,'removed: %s' % ', '.join(deleted))
1959        self.redirect(self.url(self.context))
1960        return
1961
1962    #@action(_('Add online payment ticket'))
1963    #def addPaymentTicket(self, **data):
1964    #    self.redirect(self.url(self.context, '@@addop'))
1965
1966class OnlinePaymentAddFormPage(KofaAddFormPage):
1967    """ Page to add an online payment ticket
1968    """
1969    grok.context(IStudentPaymentsContainer)
1970    grok.name('addop')
1971    grok.template('onlinepaymentaddform')
1972    grok.require('waeup.payStudent')
1973    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1974        'p_category')
1975    label = _('Add online payment')
1976    pnav = 4
1977
1978    @property
1979    def selectable_categories(self):
1980        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1981        return sorted(categories.items(), key=lambda value: value[1])
1982
1983    @action(_('Create ticket'), style='primary')
1984    def createTicket(self, **data):
1985        p_category = data['p_category']
1986        previous_session = data.get('p_session', None)
1987        previous_level = data.get('p_level', None)
1988        student = self.context.__parent__
1989        # The hostel_application payment category is temporarily used
1990        # by Uniben.
1991        if p_category in ('bed_allocation', 'hostel_application') and student[
1992            'studycourse'].current_session != grok.getSite()[
1993            'hostels'].accommodation_session:
1994                self.flash(
1995                    _('Your current session does not match ' + \
1996                    'accommodation session.'), type="danger")
1997                return
1998        if 'maintenance' in p_category:
1999            current_session = str(student['studycourse'].current_session)
2000            if not current_session in student['accommodation']:
2001                self.flash(_('You have not yet booked accommodation.'),
2002                           type="warning")
2003                return
2004        students_utils = getUtility(IStudentsUtils)
2005        error, payment = students_utils.setPaymentDetails(
2006            p_category, student, previous_session, previous_level)
2007        if error is not None:
2008            self.flash(error, type="danger")
2009            return
2010        if p_category == 'transfer':
2011            payment.p_item = self.request.form['new_programme']
2012        self.context[payment.p_id] = payment
2013        self.flash(_('Payment ticket created.'))
2014        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2015        self.redirect(self.url(self.context))
2016        return
2017
2018    @action(_('Cancel'), validator=NullValidator)
2019    def cancel(self, **data):
2020        self.redirect(self.url(self.context))
2021
2022class PreviousPaymentAddFormPage(KofaAddFormPage):
2023    """ Page to add an online payment ticket for previous sessions.
2024    """
2025    grok.context(IStudentPaymentsContainer)
2026    grok.name('addpp')
2027    grok.require('waeup.payStudent')
2028    form_fields = grok.AutoFields(IStudentPreviousPayment)
2029    label = _('Add previous session online payment')
2030    pnav = 4
2031
2032    def update(self):
2033        if self.context.student.before_payment:
2034            self.flash(_("No previous payment to be made."), type="warning")
2035            self.redirect(self.url(self.context))
2036        super(PreviousPaymentAddFormPage, self).update()
2037        return
2038
2039    @action(_('Create ticket'), style='primary')
2040    def createTicket(self, **data):
2041        p_category = data['p_category']
2042        previous_session = data.get('p_session', None)
2043        previous_level = data.get('p_level', None)
2044        student = self.context.__parent__
2045        students_utils = getUtility(IStudentsUtils)
2046        error, payment = students_utils.setPaymentDetails(
2047            p_category, student, previous_session, previous_level)
2048        if error is not None:
2049            self.flash(error, type="danger")
2050            return
2051        self.context[payment.p_id] = payment
2052        self.flash(_('Payment ticket created.'))
2053        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2054        self.redirect(self.url(self.context))
2055        return
2056
2057    @action(_('Cancel'), validator=NullValidator)
2058    def cancel(self, **data):
2059        self.redirect(self.url(self.context))
2060
2061class BalancePaymentAddFormPage(KofaAddFormPage):
2062    """ Page to add an online payment which can balance s previous session
2063    payment.
2064    """
2065    grok.context(IStudentPaymentsContainer)
2066    grok.name('addbp')
2067    grok.require('waeup.manageStudent')
2068    form_fields = grok.AutoFields(IStudentBalancePayment)
2069    label = _('Add balance')
2070    pnav = 4
2071
2072    @action(_('Create ticket'), style='primary')
2073    def createTicket(self, **data):
2074        p_category = data['p_category']
2075        balance_session = data.get('balance_session', None)
2076        balance_level = data.get('balance_level', None)
2077        balance_amount = data.get('balance_amount', None)
2078        student = self.context.__parent__
2079        students_utils = getUtility(IStudentsUtils)
2080        error, payment = students_utils.setBalanceDetails(
2081            p_category, student, balance_session,
2082            balance_level, balance_amount)
2083        if error is not None:
2084            self.flash(error, type="danger")
2085            return
2086        self.context[payment.p_id] = payment
2087        self.flash(_('Payment ticket created.'))
2088        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2089        self.redirect(self.url(self.context))
2090        return
2091
2092    @action(_('Cancel'), validator=NullValidator)
2093    def cancel(self, **data):
2094        self.redirect(self.url(self.context))
2095
2096class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2097    """ Page to view an online payment ticket
2098    """
2099    grok.context(IStudentOnlinePayment)
2100    grok.name('index')
2101    grok.require('waeup.viewStudent')
2102    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2103    form_fields[
2104        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2105    form_fields[
2106        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2107    pnav = 4
2108
2109    @property
2110    def label(self):
2111        return _('${a}: Online Payment Ticket ${b}', mapping = {
2112            'a':self.context.student.display_fullname,
2113            'b':self.context.p_id})
2114
2115class OnlinePaymentApproveView(UtilityView, grok.View):
2116    """ Callback view
2117    """
2118    grok.context(IStudentOnlinePayment)
2119    grok.name('approve')
2120    grok.require('waeup.managePortal')
2121
2122    def update(self):
2123        flashtype, msg, log = self.context.approveStudentPayment()
2124        if log is not None:
2125            # Add log message to students.log
2126            self.context.writeLogMessage(self,log)
2127            # Add log message to payments.log
2128            self.context.logger.info(
2129                '%s,%s,%s,%s,%s,,,,,,' % (
2130                self.context.student.student_id,
2131                self.context.p_id, self.context.p_category,
2132                self.context.amount_auth, self.context.r_code))
2133        self.flash(msg, type=flashtype)
2134        return
2135
2136    def render(self):
2137        self.redirect(self.url(self.context, '@@index'))
2138        return
2139
2140class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2141    """ Approval view for students.
2142
2143    This view is used for browser tests only and
2144    must be neutralized in custom pages!
2145    """
2146    grok.name('fake_approve')
2147    grok.require('waeup.payStudent')
2148
2149class ExportPDFPaymentSlip(UtilityView, grok.View):
2150    """Deliver a PDF slip of the context.
2151    """
2152    grok.context(IStudentOnlinePayment)
2153    grok.name('payment_slip.pdf')
2154    grok.require('waeup.viewStudent')
2155    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2156    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2157    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2158    prefix = 'form'
2159    note = None
2160    omit_fields = (
2161        'password', 'suspended', 'phone', 'date_of_birth',
2162        'adm_code', 'sex', 'suspended_comment', 'current_level',
2163        'flash_notice')
2164
2165    @property
2166    def title(self):
2167        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2168        return translate(_('Payment Data'), 'waeup.kofa',
2169            target_language=portal_language)
2170
2171    @property
2172    def label(self):
2173        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2174        return translate(_('Online Payment Slip'),
2175            'waeup.kofa', target_language=portal_language) \
2176            + ' %s' % self.context.p_id
2177
2178    def render(self):
2179        #if self.context.p_state != 'paid':
2180        #    self.flash('Ticket not yet paid.')
2181        #    self.redirect(self.url(self.context))
2182        #    return
2183        studentview = StudentBasePDFFormPage(self.context.student,
2184            self.request, self.omit_fields)
2185        students_utils = getUtility(IStudentsUtils)
2186        return students_utils.renderPDF(self, 'payment_slip.pdf',
2187            self.context.student, studentview, note=self.note,
2188            omit_fields=self.omit_fields)
2189
2190
2191class AccommodationManageFormPage(KofaEditFormPage):
2192    """ Page to manage bed tickets.
2193
2194    This manage form page is for both students and students officers.
2195    """
2196    grok.context(IStudentAccommodation)
2197    grok.name('index')
2198    grok.require('waeup.handleAccommodation')
2199    form_fields = grok.AutoFields(IStudentAccommodation)
2200    grok.template('accommodationmanagepage')
2201    pnav = 4
2202    with_hostel_selection = True
2203
2204    @property
2205    def booking_allowed(self):
2206        students_utils = getUtility(IStudentsUtils)
2207        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2208        error_message = students_utils.checkAccommodationRequirements(
2209            self.context.student, acc_details)
2210        if error_message:
2211            return False
2212        return True
2213
2214    @property
2215    def actionsgroup1(self):
2216        if not self.booking_allowed:
2217            return []
2218        if not self.with_hostel_selection:
2219            return []
2220        return [_('Save')]
2221
2222    @property
2223    def actionsgroup2(self):
2224        if getattr(self.request.principal, 'user_type', None) == 'student':
2225            ## Book button can be disabled in custom packages by
2226            ## uncommenting the following lines.
2227            #if not self.booking_allowed:
2228            #    return []
2229            return [_('Book accommodation')]
2230        return [_('Book accommodation'), _('Remove selected')]
2231
2232    @property
2233    def label(self):
2234        return _('${a}: Accommodation',
2235            mapping = {'a':self.context.__parent__.display_fullname})
2236
2237    @property
2238    def desired_hostel(self):
2239        if self.context.desired_hostel == 'no':
2240            return _('No favoured hostel')
2241        if self.context.desired_hostel:
2242            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2243            if hostel is not None:
2244                return hostel.hostel_name
2245        return
2246
2247    def getHostels(self):
2248        """Get a list of all stored hostels.
2249        """
2250        yield(dict(name=None, title='--', selected=''))
2251        selected = ''
2252        if self.context.desired_hostel == 'no':
2253          selected = 'selected'
2254        yield(dict(name='no', title=_('No favoured hostel'), selected=selected))
2255        for val in grok.getSite()['hostels'].values():
2256            selected = ''
2257            if val.hostel_id == self.context.desired_hostel:
2258                selected = 'selected'
2259            yield(dict(name=val.hostel_id, title=val.hostel_name,
2260                       selected=selected))
2261
2262    @action(_('Save'), style='primary')
2263    def save(self):
2264        hostel = self.request.form.get('hostel', None)
2265        self.context.desired_hostel = hostel
2266        self.flash(_('Your selection has been saved.'))
2267        return
2268
2269    @action(_('Book accommodation'), style='primary')
2270    def bookAccommodation(self, **data):
2271        self.redirect(self.url(self.context, 'add'))
2272        return
2273
2274    @jsaction(_('Remove selected'))
2275    def delBedTickets(self, **data):
2276        if getattr(self.request.principal, 'user_type', None) == 'student':
2277            self.flash(_('You are not allowed to remove bed tickets.'),
2278                       type="warning")
2279            self.redirect(self.url(self.context))
2280            return
2281        form = self.request.form
2282        if 'val_id' in form:
2283            child_id = form['val_id']
2284        else:
2285            self.flash(_('No bed ticket selected.'), type="warning")
2286            self.redirect(self.url(self.context))
2287            return
2288        if not isinstance(child_id, list):
2289            child_id = [child_id]
2290        deleted = []
2291        for id in child_id:
2292            del self.context[id]
2293            deleted.append(id)
2294        if len(deleted):
2295            self.flash(_('Successfully removed: ${a}',
2296                mapping = {'a':', '.join(deleted)}))
2297            self.context.writeLogMessage(
2298                self,'removed: % s' % ', '.join(deleted))
2299        self.redirect(self.url(self.context))
2300        return
2301
2302class BedTicketAddPage(KofaPage):
2303    """ Page to add a bed ticket
2304    """
2305    grok.context(IStudentAccommodation)
2306    grok.name('add')
2307    grok.require('waeup.handleAccommodation')
2308    grok.template('enterpin')
2309    ac_prefix = 'HOS'
2310    label = _('Add bed ticket')
2311    pnav = 4
2312    buttonname = _('Create bed ticket')
2313    notice = ''
2314    with_ac = True
2315
2316    def update(self, SUBMIT=None):
2317        student = self.context.student
2318        students_utils = getUtility(IStudentsUtils)
2319        acc_details  = students_utils.getAccommodationDetails(student)
2320        error_message = students_utils.checkAccommodationRequirements(
2321            student, acc_details)
2322        if error_message:
2323            self.flash(error_message, type="warning")
2324            self.redirect(self.url(self.context))
2325            return
2326        if self.with_ac:
2327            self.ac_series = self.request.form.get('ac_series', None)
2328            self.ac_number = self.request.form.get('ac_number', None)
2329        if SUBMIT is None:
2330            return
2331        if self.with_ac:
2332            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2333            code = get_access_code(pin)
2334            if not code:
2335                self.flash(_('Activation code is invalid.'), type="warning")
2336                return
2337        # Search and book bed
2338        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2339        entries = cat.searchResults(
2340            owner=(student.student_id,student.student_id))
2341        if len(entries):
2342            # If bed space has been manually allocated use this bed
2343            manual = True
2344            bed = [entry for entry in entries][0]
2345            # Safety belt for paranoids: Does this bed really exist on portal?
2346            # XXX: Can be remove if nobody complains.
2347            if bed.__parent__.__parent__ is None:
2348                self.flash(_('System error: Please contact the adminsitrator.'),
2349                           type="danger")
2350                self.context.writeLogMessage(
2351                    self, 'fatal error: %s' % bed.bed_id)
2352                return
2353        else:
2354            # else search for other available beds
2355            manual = False
2356            entries = cat.searchResults(
2357                bed_type=(acc_details['bt'],acc_details['bt']))
2358            available_beds = [
2359                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2360            if available_beds:
2361                students_utils = getUtility(IStudentsUtils)
2362                bed = students_utils.selectBed(
2363                    available_beds, self.context.desired_hostel)
2364                if bed is None:
2365                    self.flash(_(
2366                        'There is no free bed in your desired hostel. '
2367                        'Please try another hostel.'),
2368                        type="warning")
2369                    self.redirect(self.url(self.context))
2370                    return
2371                # Safety belt for paranoids: Does this bed really exist
2372                # in portal?
2373                # XXX: Can be remove if nobody complains.
2374                if bed.__parent__.__parent__ is None:
2375                    self.flash(_(
2376                        'System error: Please contact the administrator.'),
2377                        type="warning")
2378                    self.context.writeLogMessage(
2379                        self, 'fatal error: %s' % bed.bed_id)
2380                    return
2381                bed.bookBed(student.student_id)
2382            else:
2383                self.flash(_('There is no free bed in your category ${a}.',
2384                    mapping = {'a':acc_details['bt']}), type="warning")
2385                self.redirect(self.url(self.context))
2386                return
2387        if self.with_ac:
2388            # Mark pin as used (this also fires a pin related transition)
2389            if code.state == USED:
2390                self.flash(_('Activation code has already been used.'),
2391                           type="warning")
2392                if not manual:
2393                    # Release the previously booked bed
2394                    bed.owner = NOT_OCCUPIED
2395                    # Catalog must be informed
2396                    notify(grok.ObjectModifiedEvent(bed))
2397                return
2398            else:
2399                comment = _(u'invalidated')
2400                # Here we know that the ac is in state initialized so we do not
2401                # expect an exception, but the owner might be different
2402                success = invalidate_accesscode(
2403                    pin, comment, self.context.student.student_id)
2404                if not success:
2405                    self.flash(_('You are not the owner of this access code.'),
2406                               type="warning")
2407                    if not manual:
2408                        # Release the previously booked bed
2409                        bed.owner = NOT_OCCUPIED
2410                        # Catalog must be informed
2411                        notify(grok.ObjectModifiedEvent(bed))
2412                    return
2413        # Create bed ticket
2414        bedticket = createObject(u'waeup.BedTicket')
2415        if self.with_ac:
2416            bedticket.booking_code = pin
2417        bedticket.booking_session = acc_details['booking_session']
2418        bedticket.bed_type = acc_details['bt']
2419        bedticket.bed = bed
2420        hall_title = bed.__parent__.hostel_name
2421        coordinates = bed.coordinates[1:]
2422        block, room_nr, bed_nr = coordinates
2423        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2424            'a':hall_title, 'b':block,
2425            'c':room_nr, 'd':bed_nr,
2426            'e':bed.bed_type})
2427        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2428        bedticket.bed_coordinates = translate(
2429            bc, 'waeup.kofa',target_language=portal_language)
2430        self.context.addBedTicket(bedticket)
2431        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2432        self.flash(_('Bed ticket created and bed booked: ${a}',
2433            mapping = {'a':bedticket.display_coordinates}))
2434        self.redirect(self.url(self.context))
2435        return
2436
2437class BedTicketDisplayFormPage(KofaDisplayFormPage):
2438    """ Page to display bed tickets
2439    """
2440    grok.context(IBedTicket)
2441    grok.name('index')
2442    grok.require('waeup.handleAccommodation')
2443    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2444    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2445    pnav = 4
2446
2447    @property
2448    def label(self):
2449        return _('Bed Ticket for Session ${a}',
2450            mapping = {'a':self.context.getSessionString()})
2451
2452class ExportPDFBedTicketSlip(UtilityView, grok.View):
2453    """Deliver a PDF slip of the context.
2454    """
2455    grok.context(IBedTicket)
2456    grok.name('bed_allocation_slip.pdf')
2457    grok.require('waeup.handleAccommodation')
2458    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2459    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2460    prefix = 'form'
2461    omit_fields = (
2462        'password', 'suspended', 'phone', 'adm_code',
2463        'suspended_comment', 'date_of_birth', 'current_level',
2464        'flash_notice')
2465
2466    @property
2467    def title(self):
2468        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2469        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2470            target_language=portal_language)
2471
2472    @property
2473    def label(self):
2474        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2475        #return translate(_('Bed Allocation: '),
2476        #    'waeup.kofa', target_language=portal_language) \
2477        #    + ' %s' % self.context.bed_coordinates
2478        return translate(_('Bed Allocation Slip'),
2479            'waeup.kofa', target_language=portal_language) \
2480            + ' %s' % self.context.getSessionString()
2481
2482    def render(self):
2483        studentview = StudentBasePDFFormPage(self.context.student,
2484            self.request, self.omit_fields)
2485        students_utils = getUtility(IStudentsUtils)
2486        note = None
2487        n = grok.getSite()['hostels'].allocation_expiration
2488        if n:
2489            note = _("""
2490<br /><br /><br /><br /><br /><font size="12">
2491Please endeavour to pay your hostel maintenance charge within ${a} days
2492 of being allocated a space or else you are deemed to have
2493 voluntarily forfeited it and it goes back into circulation to be
2494 available for booking afresh!</font>)
2495""")
2496            note = _(note, mapping={'a': n})
2497            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2498            note = translate(
2499                note, 'waeup.kofa', target_language=portal_language)
2500        return students_utils.renderPDF(
2501            self, 'bed_allocation_slip.pdf',
2502            self.context.student, studentview,
2503            omit_fields=self.omit_fields,
2504            note=note)
2505
2506class BedTicketRelocationView(UtilityView, grok.View):
2507    """ Callback view
2508    """
2509    grok.context(IBedTicket)
2510    grok.name('relocate')
2511    grok.require('waeup.manageHostels')
2512
2513    # Relocate student if student parameters have changed or the bed_type
2514    # of the bed has changed
2515    def update(self):
2516        success, msg = self.context.relocateStudent()
2517        if not success:
2518            self.flash(msg, type="warning")
2519        else:
2520            self.flash(msg)
2521        self.redirect(self.url(self.context))
2522        return
2523
2524    def render(self):
2525        return
2526
2527class StudentHistoryPage(KofaPage):
2528    """ Page to display student history
2529    """
2530    grok.context(IStudent)
2531    grok.name('history')
2532    grok.require('waeup.viewStudent')
2533    grok.template('studenthistory')
2534    pnav = 4
2535
2536    @property
2537    def label(self):
2538        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2539
2540# Pages for students only
2541
2542class StudentBaseEditFormPage(KofaEditFormPage):
2543    """ View to edit student base data
2544    """
2545    grok.context(IStudent)
2546    grok.name('edit_base')
2547    grok.require('waeup.handleStudent')
2548    form_fields = grok.AutoFields(IStudentBase).select(
2549        'email', 'phone')
2550    label = _('Edit base data')
2551    pnav = 4
2552
2553    @action(_('Save'), style='primary')
2554    def save(self, **data):
2555        msave(self, **data)
2556        return
2557
2558class StudentChangePasswordPage(KofaEditFormPage):
2559    """ View to edit student passwords
2560    """
2561    grok.context(IStudent)
2562    grok.name('change_password')
2563    grok.require('waeup.handleStudent')
2564    grok.template('change_password')
2565    label = _('Change password')
2566    pnav = 4
2567
2568    @action(_('Save'), style='primary')
2569    def save(self, **data):
2570        form = self.request.form
2571        password = form.get('change_password', None)
2572        password_ctl = form.get('change_password_repeat', None)
2573        if password:
2574            validator = getUtility(IPasswordValidator)
2575            errors = validator.validate_password(password, password_ctl)
2576            if not errors:
2577                IUserAccount(self.context).setPassword(password)
2578                # Unset temporary password
2579                self.context.temp_password = None
2580                self.context.writeLogMessage(self, 'saved: password')
2581                self.flash(_('Password changed.'))
2582            else:
2583                self.flash( ' '.join(errors), type="warning")
2584        return
2585
2586class StudentFilesUploadPage(KofaPage):
2587    """ View to upload files by student
2588    """
2589    grok.context(IStudent)
2590    grok.name('change_portrait')
2591    grok.require('waeup.uploadStudentFile')
2592    grok.template('filesuploadpage')
2593    label = _('Upload portrait')
2594    pnav = 4
2595
2596    def update(self):
2597        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2598        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2599            emit_lock_message(self)
2600            return
2601        super(StudentFilesUploadPage, self).update()
2602        return
2603
2604class StartClearancePage(KofaPage):
2605    grok.context(IStudent)
2606    grok.name('start_clearance')
2607    grok.require('waeup.handleStudent')
2608    grok.template('enterpin')
2609    label = _('Start clearance')
2610    ac_prefix = 'CLR'
2611    notice = ''
2612    pnav = 4
2613    buttonname = _('Start clearance now')
2614    with_ac = True
2615
2616    @property
2617    def all_required_fields_filled(self):
2618        if not self.context.email:
2619            return _("Email address is missing."), 'edit_base'
2620        if not self.context.phone:
2621            return _("Phone number is missing."), 'edit_base'
2622        return
2623
2624    @property
2625    def portrait_uploaded(self):
2626        store = getUtility(IExtFileStore)
2627        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2628            return True
2629        return False
2630
2631    def update(self, SUBMIT=None):
2632        if not self.context.state == ADMITTED:
2633            self.flash(_("Wrong state"), type="warning")
2634            self.redirect(self.url(self.context))
2635            return
2636        if not self.portrait_uploaded:
2637            self.flash(_("No portrait uploaded."), type="warning")
2638            self.redirect(self.url(self.context, 'change_portrait'))
2639            return
2640        if self.all_required_fields_filled:
2641            arf_warning = self.all_required_fields_filled[0]
2642            arf_redirect = self.all_required_fields_filled[1]
2643            self.flash(arf_warning, type="warning")
2644            self.redirect(self.url(self.context, arf_redirect))
2645            return
2646        if self.with_ac:
2647            self.ac_series = self.request.form.get('ac_series', None)
2648            self.ac_number = self.request.form.get('ac_number', None)
2649        if SUBMIT is None:
2650            return
2651        if self.with_ac:
2652            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2653            code = get_access_code(pin)
2654            if not code:
2655                self.flash(_('Activation code is invalid.'), type="warning")
2656                return
2657            if code.state == USED:
2658                self.flash(_('Activation code has already been used.'),
2659                           type="warning")
2660                return
2661            # Mark pin as used (this also fires a pin related transition)
2662            # and fire transition start_clearance
2663            comment = _(u"invalidated")
2664            # Here we know that the ac is in state initialized so we do not
2665            # expect an exception, but the owner might be different
2666            if not invalidate_accesscode(pin, comment, self.context.student_id):
2667                self.flash(_('You are not the owner of this access code.'),
2668                           type="warning")
2669                return
2670            self.context.clr_code = pin
2671        IWorkflowInfo(self.context).fireTransition('start_clearance')
2672        self.flash(_('Clearance process has been started.'))
2673        self.redirect(self.url(self.context,'cedit'))
2674        return
2675
2676class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2677    """ View to edit student clearance data by student
2678    """
2679    grok.context(IStudent)
2680    grok.name('cedit')
2681    grok.require('waeup.handleStudent')
2682    label = _('Edit clearance data')
2683
2684    @property
2685    def form_fields(self):
2686        if self.context.is_postgrad:
2687            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2688                'clr_code', 'officer_comment')
2689        else:
2690            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2691                'clr_code', 'officer_comment')
2692        return form_fields
2693
2694    def update(self):
2695        if self.context.clearance_locked:
2696            emit_lock_message(self)
2697            return
2698        return super(StudentClearanceEditFormPage, self).update()
2699
2700    @action(_('Save'), style='primary')
2701    def save(self, **data):
2702        self.applyData(self.context, **data)
2703        self.flash(_('Clearance form has been saved.'))
2704        return
2705
2706    def dataNotComplete(self):
2707        """To be implemented in the customization package.
2708        """
2709        return False
2710
2711    @action(_('Save and request clearance'), style='primary',
2712            warning=_('You can not edit your data after '
2713            'requesting clearance. You really want to request clearance now?'))
2714    def requestClearance(self, **data):
2715        self.applyData(self.context, **data)
2716        if self.dataNotComplete():
2717            self.flash(self.dataNotComplete(), type="warning")
2718            return
2719        self.flash(_('Clearance form has been saved.'))
2720        if self.context.clr_code:
2721            self.redirect(self.url(self.context, 'request_clearance'))
2722        else:
2723            # We bypass the request_clearance page if student
2724            # has been imported in state 'clearance started' and
2725            # no clr_code was entered before.
2726            state = IWorkflowState(self.context).getState()
2727            if state != CLEARANCE:
2728                # This shouldn't happen, but the application officer
2729                # might have forgotten to lock the form after changing the state
2730                self.flash(_('This form cannot be submitted. Wrong state!'),
2731                           type="danger")
2732                return
2733            IWorkflowInfo(self.context).fireTransition('request_clearance')
2734            self.flash(_('Clearance has been requested.'))
2735            self.redirect(self.url(self.context))
2736        return
2737
2738class RequestClearancePage(KofaPage):
2739    grok.context(IStudent)
2740    grok.name('request_clearance')
2741    grok.require('waeup.handleStudent')
2742    grok.template('enterpin')
2743    label = _('Request clearance')
2744    notice = _('Enter the CLR access code used for starting clearance.')
2745    ac_prefix = 'CLR'
2746    pnav = 4
2747    buttonname = _('Request clearance now')
2748    with_ac = True
2749
2750    def update(self, SUBMIT=None):
2751        if self.with_ac:
2752            self.ac_series = self.request.form.get('ac_series', None)
2753            self.ac_number = self.request.form.get('ac_number', None)
2754        if SUBMIT is None:
2755            return
2756        if self.with_ac:
2757            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2758            if self.context.clr_code and self.context.clr_code != pin:
2759                self.flash(_("This isn't your CLR access code."), type="danger")
2760                return
2761        state = IWorkflowState(self.context).getState()
2762        if state != CLEARANCE:
2763            # This shouldn't happen, but the application officer
2764            # might have forgotten to lock the form after changing the state
2765            self.flash(_('This form cannot be submitted. Wrong state!'),
2766                       type="danger")
2767            return
2768        IWorkflowInfo(self.context).fireTransition('request_clearance')
2769        self.flash(_('Clearance has been requested.'))
2770        self.redirect(self.url(self.context))
2771        return
2772
2773class StartSessionPage(KofaPage):
2774    grok.context(IStudentStudyCourse)
2775    grok.name('start_session')
2776    grok.require('waeup.handleStudent')
2777    grok.template('enterpin')
2778    label = _('Start session')
2779    ac_prefix = 'SFE'
2780    notice = ''
2781    pnav = 4
2782    buttonname = _('Start now')
2783    with_ac = True
2784
2785    def update(self, SUBMIT=None):
2786        if not self.context.is_current:
2787            emit_lock_message(self)
2788            return
2789        super(StartSessionPage, self).update()
2790        if not self.context.next_session_allowed:
2791            self.flash(_("You are not entitled to start session."),
2792                       type="warning")
2793            self.redirect(self.url(self.context))
2794            return
2795        if self.with_ac:
2796            self.ac_series = self.request.form.get('ac_series', None)
2797            self.ac_number = self.request.form.get('ac_number', None)
2798        if SUBMIT is None:
2799            return
2800        if self.with_ac:
2801            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2802            code = get_access_code(pin)
2803            if not code:
2804                self.flash(_('Activation code is invalid.'), type="warning")
2805                return
2806            # Mark pin as used (this also fires a pin related transition)
2807            if code.state == USED:
2808                self.flash(_('Activation code has already been used.'),
2809                           type="warning")
2810                return
2811            else:
2812                comment = _(u"invalidated")
2813                # Here we know that the ac is in state initialized so we do not
2814                # expect an error, but the owner might be different
2815                if not invalidate_accesscode(
2816                    pin,comment,self.context.student.student_id):
2817                    self.flash(_('You are not the owner of this access code.'),
2818                               type="warning")
2819                    return
2820        try:
2821            if self.context.student.state == CLEARED:
2822                IWorkflowInfo(self.context.student).fireTransition(
2823                    'pay_first_school_fee')
2824            elif self.context.student.state == RETURNING:
2825                IWorkflowInfo(self.context.student).fireTransition(
2826                    'pay_school_fee')
2827            elif self.context.student.state == PAID:
2828                IWorkflowInfo(self.context.student).fireTransition(
2829                    'pay_pg_fee')
2830        except ConstraintNotSatisfied:
2831            self.flash(_('An error occurred, please contact the system administrator.'),
2832                       type="danger")
2833            return
2834        self.flash(_('Session started.'))
2835        self.redirect(self.url(self.context))
2836        return
2837
2838class AddStudyLevelFormPage(KofaEditFormPage):
2839    """ Page for students to add current study levels
2840    """
2841    grok.context(IStudentStudyCourse)
2842    grok.name('add')
2843    grok.require('waeup.handleStudent')
2844    grok.template('studyleveladdpage')
2845    form_fields = grok.AutoFields(IStudentStudyCourse)
2846    pnav = 4
2847
2848    @property
2849    def label(self):
2850        studylevelsource = StudyLevelSource().factory
2851        code = self.context.current_level
2852        title = studylevelsource.getTitle(self.context, code)
2853        return _('Add current level ${a}', mapping = {'a':title})
2854
2855    def update(self):
2856        if not self.context.is_current \
2857            or self.context.student.studycourse_locked:
2858            emit_lock_message(self)
2859            return
2860        if self.context.student.state != PAID:
2861            emit_lock_message(self)
2862            return
2863        code = self.context.current_level
2864        if code is None:
2865            self.flash(_('Your data are incomplete'), type="danger")
2866            self.redirect(self.url(self.context))
2867            return
2868        super(AddStudyLevelFormPage, self).update()
2869        return
2870
2871    @action(_('Create course list now'), style='primary')
2872    def addStudyLevel(self, **data):
2873        studylevel = createObject(u'waeup.StudentStudyLevel')
2874        studylevel.level = self.context.current_level
2875        studylevel.level_session = self.context.current_session
2876        try:
2877            self.context.addStudentStudyLevel(
2878                self.context.certificate,studylevel)
2879        except KeyError:
2880            self.flash(_('This level exists.'), type="warning")
2881            self.redirect(self.url(self.context))
2882            return
2883        except RequiredMissing:
2884            self.flash(_('Your data are incomplete.'), type="danger")
2885            self.redirect(self.url(self.context))
2886            return
2887        self.flash(_('You successfully created a new course list.'))
2888        self.redirect(self.url(self.context, str(studylevel.level)))
2889        return
2890
2891class StudyLevelEditFormPage(KofaEditFormPage):
2892    """ Page to edit the student study level data by students
2893    """
2894    grok.context(IStudentStudyLevel)
2895    grok.name('edit')
2896    grok.require('waeup.editStudyLevel')
2897    grok.template('studyleveleditpage')
2898    pnav = 4
2899    placeholder = _('Enter valid course code')
2900
2901    def update(self, ADD=None, course=None):
2902        if not self.context.__parent__.is_current:
2903            emit_lock_message(self)
2904            return
2905        if self.context.student.state != PAID or \
2906            not self.context.is_current_level:
2907            emit_lock_message(self)
2908            return
2909        super(StudyLevelEditFormPage, self).update()
2910        if ADD is not None:
2911            if not course:
2912                self.flash(_('No valid course code entered.'), type="warning")
2913                return
2914            cat = queryUtility(ICatalog, name='courses_catalog')
2915            result = cat.searchResults(code=(course, course))
2916            if len(result) != 1:
2917                self.flash(_('Course not found.'), type="warning")
2918                return
2919            course = list(result)[0]
2920            addCourseTicket(self, course)
2921        return
2922
2923    @property
2924    def label(self):
2925        # Here we know that the cookie has been set
2926        lang = self.request.cookies.get('kofa.language')
2927        level_title = translate(self.context.level_title, 'waeup.kofa',
2928            target_language=lang)
2929        return _('Edit course list of ${a}',
2930            mapping = {'a':level_title})
2931
2932    @property
2933    def translated_values(self):
2934        return translated_values(self)
2935
2936    def _delCourseTicket(self, **data):
2937        form = self.request.form
2938        if 'val_id' in form:
2939            child_id = form['val_id']
2940        else:
2941            self.flash(_('No ticket selected.'), type="warning")
2942            self.redirect(self.url(self.context, '@@edit'))
2943            return
2944        if not isinstance(child_id, list):
2945            child_id = [child_id]
2946        deleted = []
2947        for id in child_id:
2948            # Students are not allowed to remove core tickets
2949            if id in self.context and \
2950                self.context[id].removable_by_student:
2951                del self.context[id]
2952                deleted.append(id)
2953        if len(deleted):
2954            self.flash(_('Successfully removed: ${a}',
2955                mapping = {'a':', '.join(deleted)}))
2956            self.context.writeLogMessage(
2957                self,'removed: %s at %s' %
2958                (', '.join(deleted), self.context.level))
2959        self.redirect(self.url(self.context, u'@@edit'))
2960        return
2961
2962    @jsaction(_('Remove selected tickets'))
2963    def delCourseTicket(self, **data):
2964        self._delCourseTicket(**data)
2965        return
2966
2967    def _updateTickets(self, **data):
2968        cat = queryUtility(ICatalog, name='courses_catalog')
2969        invalidated = list()
2970        for value in self.context.values():
2971            result = cat.searchResults(code=(value.code, value.code))
2972            if len(result) != 1:
2973                course = None
2974            else:
2975                course = list(result)[0]
2976            invalid = self.context.updateCourseTicket(value, course)
2977            if invalid:
2978                invalidated.append(invalid)
2979        if invalidated:
2980            invalidated_string = ', '.join(invalidated)
2981            self.context.writeLogMessage(
2982                self, 'course tickets invalidated: %s' % invalidated_string)
2983        self.flash(_('All course tickets updated.'))
2984        return
2985
2986    @action(_('Update all tickets'),
2987        tooltip=_('Update all course parameters including course titles.'))
2988    def updateTickets(self, **data):
2989        self._updateTickets(**data)
2990        return
2991
2992    def _registerCourses(self, **data):
2993        if self.context.student.is_postgrad and \
2994            not self.context.student.is_special_postgrad:
2995            self.flash(_(
2996                "You are a postgraduate student, "
2997                "your course list can't bee registered."), type="warning")
2998            self.redirect(self.url(self.context))
2999            return
3000        students_utils = getUtility(IStudentsUtils)
3001        warning = students_utils.warnCreditsOOR(self.context)
3002        if warning:
3003            self.flash(warning, type="warning")
3004            return
3005        msg = self.context.course_registration_forbidden
3006        if msg:
3007            self.flash(msg, type="warning")
3008            return
3009        IWorkflowInfo(self.context.student).fireTransition(
3010            'register_courses')
3011        self.flash(_('Course list has been registered.'))
3012        self.redirect(self.url(self.context))
3013        return
3014
3015    @action(_('Register course list'), style='primary',
3016        warning=_('You can not edit your course list after registration.'
3017            ' You really want to register?'))
3018    def registerCourses(self, **data):
3019        self._registerCourses(**data)
3020        return
3021
3022class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3023    """Add a course ticket by student.
3024    """
3025    grok.name('ctadd')
3026    grok.require('waeup.handleStudent')
3027    form_fields = grok.AutoFields(ICourseTicketAdd)
3028
3029    def update(self):
3030        if self.context.student.state != PAID or \
3031            not self.context.is_current_level:
3032            emit_lock_message(self)
3033            return
3034        super(CourseTicketAddFormPage2, self).update()
3035        return
3036
3037    @action(_('Add course ticket'))
3038    def addCourseTicket(self, **data):
3039        # Safety belt
3040        if self.context.student.state != PAID:
3041            return
3042        course = data['course']
3043        success = addCourseTicket(self, course)
3044        if success:
3045            self.redirect(self.url(self.context, u'@@edit'))
3046        return
3047
3048class SetPasswordPage(KofaPage):
3049    grok.context(IKofaObject)
3050    grok.name('setpassword')
3051    grok.require('waeup.Anonymous')
3052    grok.template('setpassword')
3053    label = _('Set password for first-time login')
3054    ac_prefix = 'PWD'
3055    pnav = 0
3056    set_button = _('Set')
3057
3058    def update(self, SUBMIT=None):
3059        self.reg_number = self.request.form.get('reg_number', None)
3060        self.ac_series = self.request.form.get('ac_series', None)
3061        self.ac_number = self.request.form.get('ac_number', None)
3062
3063        if SUBMIT is None:
3064            return
3065        hitlist = search(query=self.reg_number,
3066            searchtype='reg_number', view=self)
3067        if not hitlist:
3068            self.flash(_('No student found.'), type="warning")
3069            return
3070        if len(hitlist) != 1:   # Cannot happen but anyway
3071            self.flash(_('More than one student found.'), type="warning")
3072            return
3073        student = hitlist[0].context
3074        self.student_id = student.student_id
3075        student_pw = student.password
3076        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3077        code = get_access_code(pin)
3078        if not code:
3079            self.flash(_('Access code is invalid.'), type="warning")
3080            return
3081        if student_pw and pin == student.adm_code:
3082            self.flash(_(
3083                'Password has already been set. Your Student Id is ${a}',
3084                mapping = {'a':self.student_id}))
3085            return
3086        elif student_pw:
3087            self.flash(
3088                _('Password has already been set. You are using the ' +
3089                'wrong Access Code.'), type="warning")
3090            return
3091        # Mark pin as used (this also fires a pin related transition)
3092        # and set student password
3093        if code.state == USED:
3094            self.flash(_('Access code has already been used.'), type="warning")
3095            return
3096        else:
3097            comment = _(u"invalidated")
3098            # Here we know that the ac is in state initialized so we do not
3099            # expect an exception
3100            invalidate_accesscode(pin,comment)
3101            IUserAccount(student).setPassword(self.ac_number)
3102            student.adm_code = pin
3103        self.flash(_('Password has been set. Your Student Id is ${a}',
3104            mapping = {'a':self.student_id}))
3105        return
3106
3107class StudentRequestPasswordPage(KofaAddFormPage):
3108    """Captcha'd request password page for students.
3109    """
3110    grok.name('requestpw')
3111    grok.require('waeup.Anonymous')
3112    grok.template('requestpw')
3113    form_fields = grok.AutoFields(IStudentRequestPW).select(
3114        'lastname','number','email')
3115    label = _('Request password for first-time login')
3116
3117    def update(self):
3118        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3119        if blocker:
3120            self.flash(_('The portal is in maintenance mode. '
3121                        'Password request forms are temporarily disabled.'),
3122                       type='warning')
3123            self.redirect(self.url(self.context))
3124            return
3125        # Handle captcha
3126        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3127        self.captcha_result = self.captcha.verify(self.request)
3128        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3129        return
3130
3131    def _redirect(self, email, password, student_id):
3132        # Forward only email to landing page in base package.
3133        self.redirect(self.url(self.context, 'requestpw_complete',
3134            data = dict(email=email)))
3135        return
3136
3137    def _redirect_no_student(self):
3138        # No record found, this is the truth. We do not redirect here.
3139        # We are using this method in custom packages
3140        # for redirecting alumni to the application section.
3141        self.flash(_('No student record found.'), type="warning")
3142        return
3143
3144    def _pw_used(self):
3145        # XXX: False if password has not been used. We need an extra
3146        #      attribute which remembers if student logged in.
3147        return True
3148
3149    @action(_('Send login credentials to email address'), style='primary')
3150    def get_credentials(self, **data):
3151        if not self.captcha_result.is_valid:
3152            # Captcha will display error messages automatically.
3153            # No need to flash something.
3154            return
3155        number = data.get('number','')
3156        lastname = data.get('lastname','')
3157        cat = getUtility(ICatalog, name='students_catalog')
3158        results = list(
3159            cat.searchResults(reg_number=(number, number)))
3160        if not results:
3161            results = list(
3162                cat.searchResults(matric_number=(number, number)))
3163        if results:
3164            student = results[0]
3165            if getattr(student,'lastname',None) is None:
3166                self.flash(_('An error occurred.'), type="danger")
3167                return
3168            elif student.lastname.lower() != lastname.lower():
3169                # Don't tell the truth here. Anonymous must not
3170                # know that a record was found and only the lastname
3171                # verification failed.
3172                self.flash(_('No student record found.'), type="warning")
3173                return
3174            elif student.password is not None and self._pw_used:
3175                self.flash(_('Your password has already been set and used. '
3176                             'Please proceed to the login page.'),
3177                           type="warning")
3178                return
3179            # Store email address but nothing else.
3180            student.email = data['email']
3181            notify(grok.ObjectModifiedEvent(student))
3182        else:
3183            self._redirect_no_student()
3184            return
3185
3186        kofa_utils = getUtility(IKofaUtils)
3187        password = kofa_utils.genPassword()
3188        mandate = PasswordMandate()
3189        mandate.params['password'] = password
3190        mandate.params['user'] = student
3191        site = grok.getSite()
3192        site['mandates'].addMandate(mandate)
3193        # Send email with credentials
3194        args = {'mandate_id':mandate.mandate_id}
3195        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3196        url_info = u'Confirmation link: %s' % mandate_url
3197        msg = _('You have successfully requested a password for the')
3198        if kofa_utils.sendCredentials(IUserAccount(student),
3199            password, url_info, msg):
3200            email_sent = student.email
3201        else:
3202            email_sent = None
3203        self._redirect(email=email_sent, password=password,
3204            student_id=student.student_id)
3205        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3206        self.context.logger.info(
3207            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3208        return
3209
3210class StudentRequestPasswordEmailSent(KofaPage):
3211    """Landing page after successful password request.
3212
3213    """
3214    grok.name('requestpw_complete')
3215    grok.require('waeup.Public')
3216    grok.template('requestpwmailsent')
3217    label = _('Your password request was successful.')
3218
3219    def update(self, email=None, student_id=None, password=None):
3220        self.email = email
3221        self.password = password
3222        self.student_id = student_id
3223        return
3224
3225class FilterStudentsInDepartmentPage(KofaPage):
3226    """Page that filters and lists students.
3227    """
3228    grok.context(IDepartment)
3229    grok.require('waeup.showStudents')
3230    grok.name('students')
3231    grok.template('filterstudentspage')
3232    pnav = 1
3233    session_label = _('Current Session')
3234    level_label = _('Current Level')
3235
3236    def label(self):
3237        return 'Students in %s' % self.context.longtitle
3238
3239    def _set_session_values(self):
3240        vocab_terms = academic_sessions_vocab.by_value.values()
3241        self.sessions = sorted(
3242            [(x.title, x.token) for x in vocab_terms], reverse=True)
3243        self.sessions += [('All Sessions', 'all')]
3244        return
3245
3246    def _set_level_values(self):
3247        vocab_terms = course_levels.by_value.values()
3248        self.levels = sorted(
3249            [(x.title, x.token) for x in vocab_terms])
3250        self.levels += [('All Levels', 'all')]
3251        return
3252
3253    def _searchCatalog(self, session, level):
3254        if level not in (10, 999, None):
3255            start_level = 100 * (level // 100)
3256            end_level = start_level + 90
3257        else:
3258            start_level = end_level = level
3259        cat = queryUtility(ICatalog, name='students_catalog')
3260        students = cat.searchResults(
3261            current_session=(session, session),
3262            current_level=(start_level, end_level),
3263            depcode=(self.context.code, self.context.code)
3264            )
3265        hitlist = []
3266        for student in students:
3267            hitlist.append(StudentQueryResultItem(student, view=self))
3268        return hitlist
3269
3270    def update(self, SHOW=None, session=None, level=None):
3271        self.parent_url = self.url(self.context.__parent__)
3272        self._set_session_values()
3273        self._set_level_values()
3274        self.hitlist = []
3275        self.session_default = session
3276        self.level_default = level
3277        if SHOW is not None:
3278            if session != 'all':
3279                self.session = int(session)
3280                self.session_string = '%s %s/%s' % (
3281                    self.session_label, self.session, self.session+1)
3282            else:
3283                self.session = None
3284                self.session_string = _('in any session')
3285            if level != 'all':
3286                self.level = int(level)
3287                self.level_string = '%s %s' % (self.level_label, self.level)
3288            else:
3289                self.level = None
3290                self.level_string = _('at any level')
3291            self.hitlist = self._searchCatalog(self.session, self.level)
3292            if not self.hitlist:
3293                self.flash(_('No student found.'), type="warning")
3294        return
3295
3296class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3297    """Page that filters and lists students.
3298    """
3299    grok.context(ICertificate)
3300
3301    def label(self):
3302        return 'Students studying %s' % self.context.longtitle
3303
3304    def _searchCatalog(self, session, level):
3305        if level not in (10, 999, None):
3306            start_level = 100 * (level // 100)
3307            end_level = start_level + 90
3308        else:
3309            start_level = end_level = level
3310        cat = queryUtility(ICatalog, name='students_catalog')
3311        students = cat.searchResults(
3312            current_session=(session, session),
3313            current_level=(start_level, end_level),
3314            certcode=(self.context.code, self.context.code)
3315            )
3316        hitlist = []
3317        for student in students:
3318            hitlist.append(StudentQueryResultItem(student, view=self))
3319        return hitlist
3320
3321class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3322    """Page that filters and lists students.
3323    """
3324    grok.context(ICourse)
3325    grok.require('waeup.viewStudent')
3326
3327    session_label = _('Session')
3328    level_label = _('Level')
3329
3330    def label(self):
3331        return 'Students registered for %s' % self.context.longtitle
3332
3333    def _searchCatalog(self, session, level):
3334        if level not in (10, 999, None):
3335            start_level = 100 * (level // 100)
3336            end_level = start_level + 90
3337        else:
3338            start_level = end_level = level
3339        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3340        coursetickets = cat.searchResults(
3341            session=(session, session),
3342            level=(start_level, end_level),
3343            code=(self.context.code, self.context.code)
3344            )
3345        hitlist = []
3346        for ticket in coursetickets:
3347            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3348        return list(set(hitlist))
3349
3350class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3351    """ Clear all students of a department in state 'clearance requested'.
3352    """
3353    grok.context(IDepartment)
3354    grok.name('clearallstudents')
3355    grok.require('waeup.clearAllStudents')
3356
3357    def update(self):
3358        cat = queryUtility(ICatalog, name='students_catalog')
3359        students = cat.searchResults(
3360            depcode=(self.context.code, self.context.code),
3361            state=(REQUESTED, REQUESTED)
3362            )
3363        num = 0
3364        for student in students:
3365            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3366                continue
3367            IWorkflowInfo(student).fireTransition('clear')
3368            num += 1
3369        self.flash(_('%d students have been cleared.' % num))
3370        self.redirect(self.url(self.context))
3371        return
3372
3373    def render(self):
3374        return
3375
3376
3377class EditScoresPage(KofaPage):
3378    """Page that allows to edit batches of scores.
3379    """
3380    grok.context(ICourse)
3381    grok.require('waeup.editScores')
3382    grok.name('edit_scores')
3383    grok.template('editscorespage')
3384    pnav = 1
3385    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3386
3387    def label(self):
3388        return '%s tickets in academic session %s' % (
3389            self.context.code, self.session_title)
3390
3391    def _searchCatalog(self, session):
3392        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3393        # Attention: Also tickets of previous studycourses are found
3394        coursetickets = cat.searchResults(
3395            session=(session, session),
3396            code=(self.context.code, self.context.code)
3397            )
3398        return list(coursetickets)
3399
3400    def _extract_uploadfile(self, uploadfile):
3401        """Get a mapping of student-ids to scores.
3402
3403        The mapping is constructed by reading contents from `uploadfile`.
3404
3405        We expect uploadfile to be a regular CSV file with columns
3406        ``student_id`` and ``score`` (other cols are ignored).
3407        """
3408        result = dict()
3409        data = StringIO(uploadfile.read())  # ensure we have something seekable
3410        reader = csv.DictReader(data)
3411        for row in reader:
3412            if not 'student_id' in row or not 'score' in row:
3413                continue
3414            result[row['student_id']] = row['score']
3415        return result
3416
3417    def _update_scores(self, form):
3418        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3419        error = ''
3420        if 'UPDATE_FILE' in form:
3421            if form['uploadfile']:
3422                try:
3423                    formvals = self._extract_uploadfile(form['uploadfile'])
3424                except:
3425                    self.flash(
3426                        _('Uploaded file contains illegal data. Ignored'),
3427                        type="danger")
3428                    return False
3429            else:
3430                self.flash(
3431                    _('No file provided.'), type="danger")
3432                return False
3433        else:
3434            formvals = dict(zip(form['sids'], form['scores']))
3435        for ticket in self.editable_tickets:
3436            score = ticket.score
3437            sid = ticket.student.student_id
3438            if sid not in formvals:
3439                continue
3440            if formvals[sid] == '':
3441                score = None
3442            else:
3443                try:
3444                    score = int(formvals[sid])
3445                except ValueError:
3446                    error += '%s, ' % ticket.student.display_fullname
3447            if ticket.score != score:
3448                ticket.score = score
3449                ticket.student.__parent__.logger.info(
3450                    '%s - %s %s/%s score updated (%s)' % (
3451                        ob_class, ticket.student.student_id,
3452                        ticket.level, ticket.code, score)
3453                    )
3454        if error:
3455            self.flash(
3456                _('Error: Score(s) of following students have not been '
3457                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3458                type="danger")
3459        return True
3460
3461    def update(self,  *args, **kw):
3462        form = self.request.form
3463        self.current_academic_session = grok.getSite()[
3464            'configuration'].current_academic_session
3465        if self.context.__parent__.__parent__.score_editing_disabled:
3466            self.flash(_('Score editing disabled.'), type="warning")
3467            self.redirect(self.url(self.context))
3468            return
3469        if not self.current_academic_session:
3470            self.flash(_('Current academic session not set.'), type="warning")
3471            self.redirect(self.url(self.context))
3472            return
3473        self.session_title = academic_sessions_vocab.getTerm(
3474            self.current_academic_session).title
3475        self.tickets = self._searchCatalog(self.current_academic_session)
3476        if not self.tickets:
3477            self.flash(_('No student found.'), type="warning")
3478            self.redirect(self.url(self.context))
3479            return
3480        self.editable_tickets = [
3481            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3482        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3483            return
3484        if not self.editable_tickets:
3485            return
3486        success = self._update_scores(form)
3487        if success:
3488            self.flash(_('You successfully updated course results.'))
3489        return
3490
3491
3492class DownloadScoresView(UtilityView, grok.View):
3493    """View that exports scores.
3494    """
3495    grok.context(ICourse)
3496    grok.require('waeup.editScores')
3497    grok.name('download_scores')
3498
3499    def update(self):
3500        self.current_academic_session = grok.getSite()[
3501            'configuration'].current_academic_session
3502        if self.context.__parent__.__parent__.score_editing_disabled:
3503            self.flash(_('Score editing disabled.'), type="warning")
3504            self.redirect(self.url(self.context))
3505            return
3506        if not self.current_academic_session:
3507            self.flash(_('Current academic session not set.'), type="warning")
3508            self.redirect(self.url(self.context))
3509            return
3510        site = grok.getSite()
3511        exporter = getUtility(ICSVExporter, name='lecturer')
3512        self.csv = exporter.export_filtered(site, filepath=None,
3513                                 catalog='coursetickets',
3514                                 session=self.current_academic_session,
3515                                 level=None,
3516                                 code=self.context.code)
3517        return
3518
3519    def render(self):
3520        filename = 'results_%s_%s.csv' % (
3521            self.context.code, self.current_academic_session)
3522        self.response.setHeader(
3523            'Content-Type', 'text/csv; charset=UTF-8')
3524        self.response.setHeader(
3525            'Content-Disposition:', 'attachment; filename="%s' % filename)
3526        return self.csv
3527
3528class ExportPDFScoresSlip(UtilityView, grok.View,
3529    LocalRoleAssignmentUtilityView):
3530    """Deliver a PDF slip of course tickets for a lecturer.
3531    """
3532    grok.context(ICourse)
3533    grok.name('coursetickets.pdf')
3534    grok.require('waeup.editScores')
3535
3536    @property
3537    def note(self):
3538        return
3539
3540    def data(self, session):
3541        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3542        # Attention: Also tickets of previous studycourses are found
3543        coursetickets = cat.searchResults(
3544            session=(session, session),
3545            code=(self.context.code, self.context.code)
3546            )
3547        header = [[_('Matric No.'),
3548                   _('Reg. No.'),
3549                   _('Fullname'),
3550                   _('Status'),
3551                   _('Course of Studies'),
3552                   _('Level'),
3553                   _('Score') ],]
3554        tickets = []
3555        for ticket in list(coursetickets):
3556            row = [ticket.student.matric_number,
3557                  ticket.student.reg_number,
3558                  ticket.student.display_fullname,
3559                  ticket.student.translated_state,
3560                  ticket.student.certcode,
3561                  ticket.level,
3562                  ticket.score]
3563            tickets.append(row)
3564        return header + sorted(tickets, key=lambda value: value[0]), None
3565
3566    def render(self):
3567        session = grok.getSite()['configuration'].current_academic_session
3568        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3569                     if i['local_role'] == 'waeup.local.Lecturer']
3570        lecturers =  ', '.join(lecturers)
3571        students_utils = getUtility(IStudentsUtils)
3572        return students_utils.renderPDFCourseticketsOverview(
3573            self, session, self.data(session), lecturers, 'landscape', 90,
3574            self.note)
3575
3576class ExportJobContainerOverview(KofaPage):
3577    """Page that lists active student data export jobs and provides links
3578    to discard or download CSV files.
3579
3580    """
3581    grok.context(VirtualExportJobContainer)
3582    grok.require('waeup.showStudents')
3583    grok.name('index.html')
3584    grok.template('exportjobsindex')
3585    label = _('Student Data Exports')
3586    pnav = 1
3587    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3588
3589    def update(self, CREATE=None, DISCARD=None, job_id=None):
3590        if CREATE:
3591            self.redirect(self.url('@@exportconfig'))
3592            return
3593        if DISCARD and job_id:
3594            entry = self.context.entry_from_job_id(job_id)
3595            self.context.delete_export_entry(entry)
3596            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3597            self.context.logger.info(
3598                '%s - discarded: job_id=%s' % (ob_class, job_id))
3599            self.flash(_('Discarded export') + ' %s' % job_id)
3600        self.entries = doll_up(self, user=self.request.principal.id)
3601        return
3602
3603class ExportJobContainerJobConfig(KofaPage):
3604    """Page that configures a students export job.
3605
3606    This is a baseclass.
3607    """
3608    grok.baseclass()
3609    grok.name('exportconfig')
3610    grok.require('waeup.showStudents')
3611    grok.template('exportconfig')
3612    label = _('Configure student data export')
3613    pnav = 1
3614    redirect_target = ''
3615    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3616
3617    def _set_session_values(self):
3618        vocab_terms = academic_sessions_vocab.by_value.values()
3619        self.sessions = [(_('All Sessions'), 'all')]
3620        self.sessions += sorted(
3621            [(x.title, x.token) for x in vocab_terms], reverse=True)
3622        return
3623
3624    def _set_level_values(self):
3625        vocab_terms = course_levels.by_value.values()
3626        self.levels = [(_('All Levels'), 'all')]
3627        self.levels += sorted(
3628            [(x.title, x.token) for x in vocab_terms])
3629        return
3630
3631    def _set_mode_values(self):
3632        utils = getUtility(IKofaUtils)
3633        self.modes =[(_('All Modes'), 'all')]
3634        self.modes += sorted([(value, key) for key, value in
3635                      utils.STUDY_MODES_DICT.items()])
3636        return
3637
3638    def _set_paycat_values(self):
3639        utils = getUtility(IKofaUtils)
3640        self.paycats =[(_('All Payment Categories'), 'all')]
3641        self.paycats += sorted([(value, key) for key, value in
3642                      utils.PAYMENT_CATEGORIES.items()])
3643        return
3644
3645    def _set_exporter_values(self):
3646        # We provide all student exporters, nothing else, yet.
3647        # Bursary, Department or Accommodation Officers don't
3648        # have the general exportData
3649        # permission and are only allowed to export bursary, payments
3650        # overview or accommodation data respectively.
3651        # This is the only place where waeup.exportAccommodationData,
3652        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3653        # are used.
3654        exporters = []
3655        if not checkPermission('waeup.exportData', self.context):
3656            if checkPermission('waeup.exportBursaryData', self.context):
3657                exporters += [('Bursary Data', 'bursary')]
3658            if checkPermission('waeup.exportPaymentsOverview', self.context):
3659                exporters += [('School Fee Payments Overview',
3660                               'sfpaymentsoverview'),
3661                              ('Session Payments Overview',
3662                               'sessionpaymentsoverview')]
3663            if checkPermission('waeup.exportAccommodationData', self.context):
3664                exporters += [('Bed Tickets', 'bedtickets'),
3665                              ('Accommodation Payments',
3666                               'accommodationpayments')]
3667            self.exporters = exporters
3668            return
3669        STUDENT_EXPORTER_NAMES = getUtility(
3670            IStudentsUtils).STUDENT_EXPORTER_NAMES
3671        for name in STUDENT_EXPORTER_NAMES:
3672            util = getUtility(ICSVExporter, name=name)
3673            exporters.append((util.title, name),)
3674        self.exporters = exporters
3675        return
3676
3677    @property
3678    def faccode(self):
3679        return None
3680
3681    @property
3682    def depcode(self):
3683        return None
3684
3685    @property
3686    def certcode(self):
3687        return None
3688
3689    def update(self, START=None, session=None, level=None, mode=None,
3690               payments_start=None, payments_end=None, ct_level=None,
3691               ct_session=None, paycat=None, paysession=None, exporter=None):
3692        self._set_session_values()
3693        self._set_level_values()
3694        self._set_mode_values()
3695        self._set_paycat_values()
3696        self._set_exporter_values()
3697        if START is None:
3698            return
3699        ena = exports_not_allowed(self)
3700        if ena:
3701            self.flash(ena, type='danger')
3702            return
3703        if payments_start or payments_end:
3704            date_format = '%d/%m/%Y'
3705            try:
3706                datetime.strptime(payments_start, date_format)
3707                datetime.strptime(payments_end, date_format)
3708            except ValueError:
3709                self.flash(_('Payment dates do not match format d/m/Y.'),
3710                           type="danger")
3711                return
3712        if session == 'all':
3713            session=None
3714        if level == 'all':
3715            level = None
3716        if mode == 'all':
3717            mode = None
3718        if (mode,
3719            level,
3720            session,
3721            self.faccode,
3722            self.depcode,
3723            self.certcode) == (None, None, None, None, None, None):
3724            # Export all students including those without certificate
3725            job_id = self.context.start_export_job(exporter,
3726                                          self.request.principal.id,
3727                                          payments_start = payments_start,
3728                                          payments_end = payments_end,
3729                                          paycat=paycat,
3730                                          paysession=paysession,
3731                                          ct_level = ct_level,
3732                                          ct_session = ct_session,
3733                                          )
3734        else:
3735            job_id = self.context.start_export_job(exporter,
3736                                          self.request.principal.id,
3737                                          current_session=session,
3738                                          current_level=level,
3739                                          current_mode=mode,
3740                                          faccode=self.faccode,
3741                                          depcode=self.depcode,
3742                                          certcode=self.certcode,
3743                                          payments_start = payments_start,
3744                                          payments_end = payments_end,
3745                                          paycat=paycat,
3746                                          paysession=paysession,
3747                                          ct_level = ct_level,
3748                                          ct_session = ct_session,)
3749        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3750        self.context.logger.info(
3751            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3752            % (ob_class, exporter, session, level, mode, self.faccode,
3753            self.depcode, self.certcode, payments_start, payments_end,
3754            ct_level, ct_session, paycat, paysession, job_id))
3755        self.flash(_('Export started for students with') +
3756                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3757                   session, level, mode))
3758        self.redirect(self.url(self.redirect_target))
3759        return
3760
3761class ExportJobContainerDownload(ExportCSVView):
3762    """Page that downloads a students export csv file.
3763
3764    """
3765    grok.context(VirtualExportJobContainer)
3766    grok.require('waeup.showStudents')
3767
3768class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3769    """Page that configures a students export job in datacenter.
3770
3771    """
3772    grok.context(IDataCenter)
3773    redirect_target = '@@export'
3774
3775class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3776    """Page that configures a students export job in datacenter.
3777
3778    """
3779    grok.name('exportselected')
3780    grok.context(IDataCenter)
3781    redirect_target = '@@export'
3782    grok.template('exportselected')
3783    label = _('Configure student data export')
3784
3785    def update(self, START=None, students=None, exporter=None):
3786        self._set_exporter_values()
3787        if START is None:
3788            return
3789        ena = exports_not_allowed(self)
3790        if ena:
3791            self.flash(ena, type='danger')
3792            return
3793        try:
3794            ids = students.replace(',', ' ').split()
3795        except:
3796            self.flash(sys.exc_info()[1])
3797            self.redirect(self.url(self.redirect_target))
3798            return
3799        job_id = self.context.start_export_job(
3800            exporter, self.request.principal.id, selected=ids)
3801        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3802        self.context.logger.info(
3803            '%s - selected students exported: %s, job_id=%s' %
3804            (ob_class, exporter, job_id))
3805        self.flash(_('Export of selected students started.'))
3806        self.redirect(self.url(self.redirect_target))
3807        return
3808
3809class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3810    """Page that configures a students export job in facultiescontainer.
3811
3812    """
3813    grok.context(VirtualFacultiesExportJobContainer)
3814
3815
3816class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3817    """Page that configures a students export job in faculties.
3818
3819    """
3820    grok.context(VirtualFacultyExportJobContainer)
3821
3822    @property
3823    def faccode(self):
3824        return self.context.__parent__.code
3825
3826class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3827    """Page that configures a students export job in departments.
3828
3829    """
3830    grok.context(VirtualDepartmentExportJobContainer)
3831
3832    @property
3833    def depcode(self):
3834        return self.context.__parent__.code
3835
3836class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3837    """Page that configures a students export job for certificates.
3838
3839    """
3840    grok.context(VirtualCertificateExportJobContainer)
3841    grok.template('exportconfig_certificate')
3842
3843    @property
3844    def certcode(self):
3845        return self.context.__parent__.code
3846
3847class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3848    """Page that configures a students export job for courses.
3849
3850    In contrast to department or certificate student data exports the
3851    coursetickets_catalog is searched here. Therefore the update
3852    method from the base class is customized.
3853    """
3854    grok.context(VirtualCourseExportJobContainer)
3855    grok.template('exportconfig_course')
3856
3857    def _set_exporter_values(self):
3858        # We provide only the 'coursetickets' and 'lecturer' exporter
3859        # but can add more.
3860        exporters = []
3861        for name in ('coursetickets', 'lecturer'):
3862            util = getUtility(ICSVExporter, name=name)
3863            exporters.append((util.title, name),)
3864        self.exporters = exporters
3865
3866    def _set_session_values(self):
3867        # We allow only current academic session
3868        academic_session = grok.getSite()['configuration'].current_academic_session
3869        if not academic_session:
3870            self.sessions = []
3871            return
3872        x = academic_sessions_vocab.getTerm(academic_session)
3873        self.sessions = [(x.title, x.token)]
3874        return
3875
3876    def update(self, START=None, session=None, level=None, mode=None,
3877               exporter=None):
3878        self._set_session_values()
3879        self._set_level_values()
3880        self._set_mode_values()
3881        self._set_exporter_values()
3882        if not self.sessions:
3883            self.flash(
3884                _('Academic session not set. '
3885                  'Please contact the administrator.'),
3886                type='danger')
3887            self.redirect(self.url(self.context))
3888            return
3889        if START is None:
3890            return
3891        ena = exports_not_allowed(self)
3892        if ena:
3893            self.flash(ena, type='danger')
3894            return
3895        if session == 'all':
3896            session = None
3897        if level == 'all':
3898            level = None
3899        job_id = self.context.start_export_job(exporter,
3900                                      self.request.principal.id,
3901                                      # Use a different catalog and
3902                                      # pass different keywords than
3903                                      # for the (default) students_catalog
3904                                      catalog='coursetickets',
3905                                      session=session,
3906                                      level=level,
3907                                      code=self.context.__parent__.code)
3908        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3909        self.context.logger.info(
3910            '%s - exported: %s (%s, %s, %s), job_id=%s'
3911            % (ob_class, exporter, session, level,
3912            self.context.__parent__.code, job_id))
3913        self.flash(_('Export started for course tickets with') +
3914                   ' level_session=%s, level=%s' % (
3915                   session, level))
3916        self.redirect(self.url(self.redirect_target))
3917        return
Note: See TracBrowser for help on using the repository browser.