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

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

Level Zero must not be included in transcripts.

  • Property svn:keywords set to Id
File size: 141.7 KB
Line 
1## $Id: browser.py 15213 2018-11-01 08:17:29Z 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    def update(self, SUBMIT=None):
1206        super(StudentTranscriptValidateFormPage, self).update()
1207        if self.context.student.state != TRANSREQ:
1208            self.flash(_('Student is in wrong state.'), type="warning")
1209            self.redirect(self.url(self.context))
1210            return
1211        if getattr(self.context, 'transcript_comment', None) is not None:
1212            self.correspondence = self.context.transcript_comment.replace(
1213                '\n', '<br>')
1214        else:
1215            self.correspondence = ''
1216        if getattr(self.context, 'transcript_signees', None) is not None:
1217            self.signees = self.context.transcript_signees.replace(
1218                '\n', '<br><br>')
1219        else:
1220            self.signees = ''
1221        if SUBMIT is None:
1222            return
1223        # Fire transition
1224        IWorkflowInfo(self.context.student).fireTransition('validate_transcript')
1225        self.flash(_('Transcript validated.'))
1226        comment = self.request.form.get('comment', '').replace('\r', '')
1227        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1228        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1229        old_transcript_comment = getattr(
1230            self.context, 'transcript_comment', None)
1231        if old_transcript_comment == None:
1232            old_transcript_comment = ''
1233        self.context.transcript_comment = '''On %s %s wrote:
1234
1235%s
1236
1237%s''' % (today, self.request.principal.id, comment,
1238         old_transcript_comment)
1239        self.context.writeLogMessage(
1240            self, 'comment: %s' % comment.replace('\n', '<br>'))
1241        self.redirect(self.url(self.context) + '/transcript')
1242        return
1243
1244class StudentTranscriptReleaseFormPage(KofaEditFormPage):
1245    """ Page to release transcript
1246    """
1247    grok.context(IStudentStudyCourse)
1248    grok.name('release_transcript')
1249    grok.require('waeup.processTranscript')
1250    grok.template('transcriptprocess')
1251    label = _('Release transcript')
1252    buttonname = _('Save comment and release transcript')
1253    pnav = 4
1254
1255    def update(self, SUBMIT=None):
1256        super(StudentTranscriptReleaseFormPage, self).update()
1257        if self.context.student.state != TRANSVAL:
1258            self.flash(_('Student is in wrong state.'), type="warning")
1259            self.redirect(self.url(self.context))
1260            return
1261        if getattr(self.context, 'transcript_comment', None) is not None:
1262            self.correspondence = self.context.transcript_comment.replace(
1263                '\n', '<br>')
1264        else:
1265            self.correspondence = ''
1266        if getattr(self.context, 'transcript_signees', None) is not None:
1267            self.signees = self.context.transcript_signees.replace(
1268                '\n', '<br><br>')
1269        else:
1270            self.signees = ''
1271        if SUBMIT is None:
1272            return
1273        # Fire transition
1274        IWorkflowInfo(self.context.student).fireTransition('release_transcript')
1275        self.flash(_('Transcript released and final transcript file saved.'))
1276        comment = self.request.form.get('comment', '').replace('\r', '')
1277        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1278        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1279        old_transcript_comment = getattr(
1280            self.context, 'transcript_comment', None)
1281        if old_transcript_comment == None:
1282            old_transcript_comment = ''
1283        self.context.transcript_comment = '''On %s %s wrote:
1284
1285%s
1286
1287%s''' % (today, self.request.principal.id, comment,
1288         old_transcript_comment)
1289        self.context.writeLogMessage(
1290            self, 'comment: %s' % comment.replace('\n', '<br>'))
1291        # Produce transcript file
1292        self.redirect(self.url(self.context) + '/transcript.pdf')
1293        return
1294
1295class StudyCourseTranscriptPage(KofaDisplayFormPage):
1296    """ Page to display the student's transcript.
1297    """
1298    grok.context(IStudentStudyCourse)
1299    grok.name('transcript')
1300    grok.require('waeup.viewTranscript')
1301    grok.template('transcript')
1302    pnav = 4
1303
1304    def update(self):
1305        final_slip = getUtility(IExtFileStore).getFileByContext(
1306            self.context.student, attr='final_transcript')
1307        if not self.context.student.transcript_enabled or final_slip:
1308            self.flash(_('Forbidden!'), type="warning")
1309            self.redirect(self.url(self.context))
1310            return
1311        super(StudyCourseTranscriptPage, self).update()
1312        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1313        self.level_dict = level_dict(self.context)
1314        self.session_dict = dict([(None, 'None'),] +
1315            [(item[1], item[0]) for item in academic_sessions()])
1316        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1317        return
1318
1319    @property
1320    def label(self):
1321        # Here we know that the cookie has been set
1322        return _('${a}: Transcript Data', mapping = {
1323            'a':self.context.student.display_fullname})
1324
1325class ExportPDFTranscriptSlip(UtilityView, grok.View):
1326    """Deliver a PDF slip of the context.
1327    """
1328    grok.context(IStudentStudyCourse)
1329    grok.name('transcript.pdf')
1330    grok.require('waeup.viewTranscript')
1331    prefix = 'form'
1332    omit_fields = (
1333        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1334        'password', 'suspended', 'phone', 'email',
1335        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1336
1337    def update(self):
1338        final_slip = getUtility(IExtFileStore).getFileByContext(
1339            self.context.student, attr='final_transcript')
1340        if not self.context.student.transcript_enabled \
1341            or final_slip:
1342            self.flash(_('Forbidden!'), type="warning")
1343            self.redirect(self.url(self.context))
1344            return
1345        super(ExportPDFTranscriptSlip, self).update()
1346        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1347        self.level_dict = level_dict(self.context)
1348        self.session_dict = dict([(None, 'None'),] +
1349            [(item[1], item[0]) for item in academic_sessions()])
1350        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1351        return
1352
1353    @property
1354    def label(self):
1355        # Here we know that the cookie has been set
1356        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1357        return translate(_('Academic Transcript'),
1358            'waeup.kofa', target_language=portal_language)
1359
1360    def _sigsInFooter(self):
1361        if getattr(
1362            self.context.student['studycourse'], 'transcript_signees', None):
1363            return ()
1364        return (_('CERTIFIED TRUE COPY'),)
1365
1366    def _signatures(self):
1367        return ()
1368
1369    def _digital_sigs(self):
1370        if getattr(
1371            self.context.student['studycourse'], 'transcript_signees', None):
1372            return self.context.student['studycourse'].transcript_signees
1373        return ()
1374
1375    def _save_file(self):
1376        if self.context.student.state == TRANSREL:
1377            return True
1378        return False
1379
1380    def render(self):
1381        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1382        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1383        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1384        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1385        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1386        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1387        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1388        studentview = StudentBasePDFFormPage(self.context.student,
1389            self.request, self.omit_fields)
1390        students_utils = getUtility(IStudentsUtils)
1391
1392        tableheader = [(Code,'code', 2.5),
1393                         (Title,'title', 7),
1394                         (Term, 'semester', 1.5),
1395                         (Cred, 'credits', 1.5),
1396                         (Score, 'total_score', 1.5),
1397                         (Grade, 'grade', 1.5),
1398                         ]
1399
1400        pdfstream = students_utils.renderPDFTranscript(
1401            self, 'transcript.pdf',
1402            self.context.student, studentview,
1403            omit_fields=self.omit_fields,
1404            tableheader=tableheader,
1405            signatures=self._signatures(),
1406            sigs_in_footer=self._sigsInFooter(),
1407            digital_sigs=self._digital_sigs(),
1408            save_file=self._save_file(),
1409            )
1410        if not pdfstream:
1411            self.redirect(self.url(self.context.student))
1412            return
1413        return pdfstream
1414
1415class StudentTransferFormPage(KofaAddFormPage):
1416    """Page to transfer the student.
1417    """
1418    grok.context(IStudent)
1419    grok.name('transfer')
1420    grok.require('waeup.manageStudent')
1421    label = _('Transfer student')
1422    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1423        'entry_mode', 'entry_session')
1424    pnav = 4
1425
1426    @jsaction(_('Transfer'))
1427    def transferStudent(self, **data):
1428        error = self.context.transfer(**data)
1429        if error == -1:
1430            self.flash(_('Current level does not match certificate levels.'),
1431                       type="warning")
1432        elif error == -2:
1433            self.flash(_('Former study course record incomplete.'),
1434                       type="warning")
1435        elif error == -3:
1436            self.flash(_('Maximum number of transfers exceeded.'),
1437                       type="warning")
1438        else:
1439            self.flash(_('Successfully transferred.'))
1440        return
1441
1442class RevertTransferFormPage(KofaEditFormPage):
1443    """View that reverts the previous transfer.
1444    """
1445    grok.context(IStudent)
1446    grok.name('revert_transfer')
1447    grok.require('waeup.manageStudent')
1448    grok.template('reverttransfer')
1449    label = _('Revert previous transfer')
1450
1451    def update(self):
1452        if not self.context.has_key('studycourse_1'):
1453            self.flash(_('No previous transfer.'), type="warning")
1454            self.redirect(self.url(self.context))
1455            return
1456        return
1457
1458    @jsaction(_('Revert now'))
1459    def transferStudent(self, **data):
1460        self.context.revert_transfer()
1461        self.flash(_('Previous transfer reverted.'))
1462        self.redirect(self.url(self.context, 'studycourse'))
1463        return
1464
1465class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1466    """ Page to display student study levels
1467    """
1468    grok.context(IStudentStudyLevel)
1469    grok.name('index')
1470    grok.require('waeup.viewStudent')
1471    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1472    form_fields[
1473        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1474    grok.template('studylevelpage')
1475    pnav = 4
1476
1477    def update(self):
1478        super(StudyLevelDisplayFormPage, self).update()
1479        if self.context.level == 0:
1480            self.form_fields = self.form_fields.omit('gpa')
1481        return
1482
1483    @property
1484    def translated_values(self):
1485        return translated_values(self)
1486
1487    @property
1488    def label(self):
1489        # Here we know that the cookie has been set
1490        lang = self.request.cookies.get('kofa.language')
1491        level_title = translate(self.context.level_title, 'waeup.kofa',
1492            target_language=lang)
1493        return _('${a}: ${b}', mapping = {
1494            'a':self.context.student.display_fullname,
1495            'b':level_title})
1496
1497class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1498    """Deliver a PDF slip of the context.
1499    """
1500    grok.context(IStudentStudyLevel)
1501    grok.name('course_registration_slip.pdf')
1502    grok.require('waeup.viewStudent')
1503    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level', 'gpa')
1504    form_fields[
1505        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1506    prefix = 'form'
1507    omit_fields = (
1508        'password', 'suspended', 'phone', 'date_of_birth',
1509        'adm_code', 'sex', 'suspended_comment', 'current_level',
1510        'flash_notice')
1511
1512    @property
1513    def title(self):
1514        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1515        return translate(_('Level Data'), 'waeup.kofa',
1516            target_language=portal_language)
1517
1518    @property
1519    def label(self):
1520        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1521        lang = self.request.cookies.get('kofa.language', portal_language)
1522        level_title = translate(self.context.level_title, 'waeup.kofa',
1523            target_language=lang)
1524        return translate(_('Course Registration Slip'),
1525            'waeup.kofa', target_language=portal_language) \
1526            + ' %s' % level_title
1527
1528    @property
1529    def tabletitle(self):
1530        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1531        tabletitle = []
1532        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1533            target_language=portal_language))
1534        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1535            target_language=portal_language))
1536        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1537            target_language=portal_language))
1538        return tabletitle
1539
1540    def render(self):
1541        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1542        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1543        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1544        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1545        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1546        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1547        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1548        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1549        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1550        studentview = StudentBasePDFFormPage(self.context.student,
1551            self.request, self.omit_fields)
1552        students_utils = getUtility(IStudentsUtils)
1553
1554        tabledata = []
1555        tableheader = []
1556        for i in range(1,7):
1557            tabledata.append(sorted(
1558                [value for value in self.context.values() if value.semester == i],
1559                key=lambda value: str(value.semester) + value.code))
1560            tableheader.append([(Code,'code', 2.5),
1561                             (Title,'title', 5),
1562                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1563                             (Cred, 'credits', 1.5),
1564                             #(Mand, 'mandatory', 1.5),
1565                             (Score, 'score', 1.5),
1566                             (Grade, 'grade', 1.5),
1567                             #('Auto', 'automatic', 1.5)
1568                             ])
1569        return students_utils.renderPDF(
1570            self, 'course_registration_slip.pdf',
1571            self.context.student, studentview,
1572            tableheader=tableheader,
1573            tabledata=tabledata,
1574            omit_fields=self.omit_fields
1575            )
1576
1577class StudyLevelManageFormPage(KofaEditFormPage):
1578    """ Page to edit the student study level data
1579    """
1580    grok.context(IStudentStudyLevel)
1581    grok.name('manage')
1582    grok.require('waeup.manageStudent')
1583    grok.template('studylevelmanagepage')
1584    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1585        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1586    pnav = 4
1587    taboneactions = [_('Save'),_('Cancel')]
1588    tabtwoactions = [_('Add course ticket'),
1589        _('Remove selected tickets'),_('Cancel')]
1590    placeholder = _('Enter valid course code')
1591
1592    def update(self, ADD=None, course=None):
1593        if not self.context.__parent__.is_current \
1594            or self.context.student.studycourse_locked:
1595            emit_lock_message(self)
1596            return
1597        super(StudyLevelManageFormPage, self).update()
1598        if ADD is not None:
1599            if not course:
1600                self.flash(_('No valid course code entered.'), type="warning")
1601                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1602                return
1603            cat = queryUtility(ICatalog, name='courses_catalog')
1604            result = cat.searchResults(code=(course, course))
1605            if len(result) != 1:
1606                self.flash(_('Course not found.'), type="warning")
1607            else:
1608                course = list(result)[0]
1609                addCourseTicket(self, course)
1610            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1611        return
1612
1613    @property
1614    def translated_values(self):
1615        return translated_values(self)
1616
1617    @property
1618    def label(self):
1619        # Here we know that the cookie has been set
1620        lang = self.request.cookies.get('kofa.language')
1621        level_title = translate(self.context.level_title, 'waeup.kofa',
1622            target_language=lang)
1623        return _('Manage ${a}',
1624            mapping = {'a':level_title})
1625
1626    @action(_('Save'), style='primary')
1627    def save(self, **data):
1628        msave(self, **data)
1629        return
1630
1631    @jsaction(_('Remove selected tickets'))
1632    def delCourseTicket(self, **data):
1633        form = self.request.form
1634        if 'val_id' in form:
1635            child_id = form['val_id']
1636        else:
1637            self.flash(_('No ticket selected.'), type="warning")
1638            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1639            return
1640        if not isinstance(child_id, list):
1641            child_id = [child_id]
1642        deleted = []
1643        for id in child_id:
1644            del self.context[id]
1645            deleted.append(id)
1646        if len(deleted):
1647            self.flash(_('Successfully removed: ${a}',
1648                mapping = {'a':', '.join(deleted)}))
1649            self.context.writeLogMessage(
1650                self,'removed: %s at %s' %
1651                (', '.join(deleted), self.context.level))
1652        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1653        return
1654
1655class ValidateCoursesView(UtilityView, grok.View):
1656    """ Validate course list by course adviser
1657    """
1658    grok.context(IStudentStudyLevel)
1659    grok.name('validate_courses')
1660    grok.require('waeup.validateStudent')
1661
1662    def update(self):
1663        if not self.context.__parent__.is_current:
1664            emit_lock_message(self)
1665            return
1666        if str(self.context.student.current_level) != self.context.__name__:
1667            self.flash(_('This is not the student\'s current level.'),
1668                       type="danger")
1669        elif self.context.student.state == REGISTERED:
1670            IWorkflowInfo(self.context.student).fireTransition(
1671                'validate_courses')
1672            self.flash(_('Course list has been validated.'))
1673        else:
1674            self.flash(_('Student is in the wrong state.'), type="warning")
1675        self.redirect(self.url(self.context))
1676        return
1677
1678    def render(self):
1679        return
1680
1681class RejectCoursesView(UtilityView, grok.View):
1682    """ Reject course list by course adviser
1683    """
1684    grok.context(IStudentStudyLevel)
1685    grok.name('reject_courses')
1686    grok.require('waeup.validateStudent')
1687
1688    def update(self):
1689        if not self.context.__parent__.is_current:
1690            emit_lock_message(self)
1691            return
1692        if str(self.context.__parent__.current_level) != self.context.__name__:
1693            self.flash(_('This is not the student\'s current level.'),
1694                       type="danger")
1695            self.redirect(self.url(self.context))
1696            return
1697        elif self.context.student.state == VALIDATED:
1698            IWorkflowInfo(self.context.student).fireTransition('reset8')
1699            message = _('Course list request has been annulled.')
1700            self.flash(message)
1701        elif self.context.student.state == REGISTERED:
1702            IWorkflowInfo(self.context.student).fireTransition('reset7')
1703            message = _('Course list has been unregistered.')
1704            self.flash(message)
1705        else:
1706            self.flash(_('Student is in the wrong state.'), type="warning")
1707            self.redirect(self.url(self.context))
1708            return
1709        args = {'subject':message}
1710        self.redirect(self.url(self.context.student) +
1711            '/contactstudent?%s' % urlencode(args))
1712        return
1713
1714    def render(self):
1715        return
1716
1717class UnregisterCoursesView(UtilityView, grok.View):
1718    """Unregister course list by student
1719    """
1720    grok.context(IStudentStudyLevel)
1721    grok.name('unregister_courses')
1722    grok.require('waeup.handleStudent')
1723
1724    def update(self):
1725        if not self.context.__parent__.is_current:
1726            emit_lock_message(self)
1727            return
1728        try:
1729            deadline = grok.getSite()['configuration'][
1730                str(self.context.level_session)].coursereg_deadline
1731        except (TypeError, KeyError):
1732            deadline = None
1733        if deadline and deadline < datetime.now(pytz.utc):
1734            self.flash(_(
1735                "Course registration has ended. "
1736                "Unregistration is disabled."), type="warning")
1737        elif str(self.context.__parent__.current_level) != self.context.__name__:
1738            self.flash(_('This is not your current level.'), type="danger")
1739        elif self.context.student.state == REGISTERED:
1740            IWorkflowInfo(self.context.student).fireTransition('reset7')
1741            message = _('Course list has been unregistered.')
1742            self.flash(message)
1743        else:
1744            self.flash(_('You are in the wrong state.'), type="warning")
1745        self.redirect(self.url(self.context))
1746        return
1747
1748    def render(self):
1749        return
1750
1751class CourseTicketAddFormPage(KofaAddFormPage):
1752    """Add a course ticket.
1753    """
1754    grok.context(IStudentStudyLevel)
1755    grok.name('add')
1756    grok.require('waeup.manageStudent')
1757    label = _('Add course ticket')
1758    form_fields = grok.AutoFields(ICourseTicketAdd)
1759    pnav = 4
1760
1761    def update(self):
1762        if not self.context.__parent__.is_current \
1763            or self.context.student.studycourse_locked:
1764            emit_lock_message(self)
1765            return
1766        super(CourseTicketAddFormPage, self).update()
1767        return
1768
1769    @action(_('Add course ticket'), style='primary')
1770    def addCourseTicket(self, **data):
1771        course = data['course']
1772        success = addCourseTicket(self, course)
1773        if success:
1774            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1775        return
1776
1777    @action(_('Cancel'), validator=NullValidator)
1778    def cancel(self, **data):
1779        self.redirect(self.url(self.context))
1780
1781class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1782    """ Page to display course tickets
1783    """
1784    grok.context(ICourseTicket)
1785    grok.name('index')
1786    grok.require('waeup.viewStudent')
1787    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
1788        'ticket_session')
1789    grok.template('courseticketpage')
1790    pnav = 4
1791
1792    @property
1793    def label(self):
1794        return _('${a}: Course Ticket ${b}', mapping = {
1795            'a':self.context.student.display_fullname,
1796            'b':self.context.code})
1797
1798class CourseTicketManageFormPage(KofaEditFormPage):
1799    """ Page to manage course tickets
1800    """
1801    grok.context(ICourseTicket)
1802    grok.name('manage')
1803    grok.require('waeup.manageStudent')
1804    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1805    form_fields['title'].for_display = True
1806    form_fields['fcode'].for_display = True
1807    form_fields['dcode'].for_display = True
1808    form_fields['semester'].for_display = True
1809    form_fields['passmark'].for_display = True
1810    form_fields['credits'].for_display = True
1811    form_fields['mandatory'].for_display = False
1812    form_fields['automatic'].for_display = True
1813    form_fields['carry_over'].for_display = True
1814    form_fields['ticket_session'].for_display = True
1815    pnav = 4
1816    grok.template('courseticketmanagepage')
1817
1818    def update(self):
1819        if not self.context.__parent__.__parent__.is_current \
1820            or self.context.student.studycourse_locked:
1821            emit_lock_message(self)
1822            return
1823        super(CourseTicketManageFormPage, self).update()
1824        return
1825
1826    @property
1827    def label(self):
1828        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1829
1830    @action('Save', style='primary')
1831    def save(self, **data):
1832        msave(self, **data)
1833        return
1834
1835class PaymentsManageFormPage(KofaEditFormPage):
1836    """ Page to manage the student payments
1837
1838    This manage form page is for both students and students officers.
1839    """
1840    grok.context(IStudentPaymentsContainer)
1841    grok.name('index')
1842    grok.require('waeup.viewStudent')
1843    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1844    grok.template('paymentsmanagepage')
1845    pnav = 4
1846
1847    @property
1848    def manage_payments_allowed(self):
1849        return checkPermission('waeup.payStudent', self.context)
1850
1851    def unremovable(self, ticket):
1852        usertype = getattr(self.request.principal, 'user_type', None)
1853        if not usertype:
1854            return False
1855        if not self.manage_payments_allowed:
1856            return True
1857        return (self.request.principal.user_type == 'student' and ticket.r_code)
1858
1859    @property
1860    def label(self):
1861        return _('${a}: Payments',
1862            mapping = {'a':self.context.__parent__.display_fullname})
1863
1864    @jsaction(_('Remove selected tickets'))
1865    def delPaymentTicket(self, **data):
1866        form = self.request.form
1867        if 'val_id' in form:
1868            child_id = form['val_id']
1869        else:
1870            self.flash(_('No payment selected.'), type="warning")
1871            self.redirect(self.url(self.context))
1872            return
1873        if not isinstance(child_id, list):
1874            child_id = [child_id]
1875        deleted = []
1876        for id in child_id:
1877            # Students are not allowed to remove used payment tickets
1878            ticket = self.context.get(id, None)
1879            if ticket is not None and not self.unremovable(ticket):
1880                del self.context[id]
1881                deleted.append(id)
1882        if len(deleted):
1883            self.flash(_('Successfully removed: ${a}',
1884                mapping = {'a': ', '.join(deleted)}))
1885            self.context.writeLogMessage(
1886                self,'removed: %s' % ', '.join(deleted))
1887        self.redirect(self.url(self.context))
1888        return
1889
1890    #@action(_('Add online payment ticket'))
1891    #def addPaymentTicket(self, **data):
1892    #    self.redirect(self.url(self.context, '@@addop'))
1893
1894class OnlinePaymentAddFormPage(KofaAddFormPage):
1895    """ Page to add an online payment ticket
1896    """
1897    grok.context(IStudentPaymentsContainer)
1898    grok.name('addop')
1899    grok.template('onlinepaymentaddform')
1900    grok.require('waeup.payStudent')
1901    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1902        'p_category')
1903    label = _('Add online payment')
1904    pnav = 4
1905
1906    @property
1907    def selectable_categories(self):
1908        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1909        return sorted(categories.items(), key=lambda value: value[1])
1910
1911    @action(_('Create ticket'), style='primary')
1912    def createTicket(self, **data):
1913        p_category = data['p_category']
1914        previous_session = data.get('p_session', None)
1915        previous_level = data.get('p_level', None)
1916        student = self.context.__parent__
1917        # The hostel_application payment category is temporarily used
1918        # by Uniben.
1919        if p_category in ('bed_allocation', 'hostel_application') and student[
1920            'studycourse'].current_session != grok.getSite()[
1921            'hostels'].accommodation_session:
1922                self.flash(
1923                    _('Your current session does not match ' + \
1924                    'accommodation session.'), type="danger")
1925                return
1926        if 'maintenance' in p_category:
1927            current_session = str(student['studycourse'].current_session)
1928            if not current_session in student['accommodation']:
1929                self.flash(_('You have not yet booked accommodation.'),
1930                           type="warning")
1931                return
1932        students_utils = getUtility(IStudentsUtils)
1933        error, payment = students_utils.setPaymentDetails(
1934            p_category, student, previous_session, previous_level)
1935        if error is not None:
1936            self.flash(error, type="danger")
1937            return
1938        if p_category == 'transfer':
1939            payment.p_item = self.request.form['new_programme']
1940        self.context[payment.p_id] = payment
1941        self.flash(_('Payment ticket created.'))
1942        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1943        self.redirect(self.url(self.context))
1944        return
1945
1946    @action(_('Cancel'), validator=NullValidator)
1947    def cancel(self, **data):
1948        self.redirect(self.url(self.context))
1949
1950class PreviousPaymentAddFormPage(KofaAddFormPage):
1951    """ Page to add an online payment ticket for previous sessions.
1952    """
1953    grok.context(IStudentPaymentsContainer)
1954    grok.name('addpp')
1955    grok.require('waeup.payStudent')
1956    form_fields = grok.AutoFields(IStudentPreviousPayment)
1957    label = _('Add previous session online payment')
1958    pnav = 4
1959
1960    def update(self):
1961        if self.context.student.before_payment:
1962            self.flash(_("No previous payment to be made."), type="warning")
1963            self.redirect(self.url(self.context))
1964        super(PreviousPaymentAddFormPage, self).update()
1965        return
1966
1967    @action(_('Create ticket'), style='primary')
1968    def createTicket(self, **data):
1969        p_category = data['p_category']
1970        previous_session = data.get('p_session', None)
1971        previous_level = data.get('p_level', None)
1972        student = self.context.__parent__
1973        students_utils = getUtility(IStudentsUtils)
1974        error, payment = students_utils.setPaymentDetails(
1975            p_category, student, previous_session, previous_level)
1976        if error is not None:
1977            self.flash(error, type="danger")
1978            return
1979        self.context[payment.p_id] = payment
1980        self.flash(_('Payment ticket created.'))
1981        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1982        self.redirect(self.url(self.context))
1983        return
1984
1985    @action(_('Cancel'), validator=NullValidator)
1986    def cancel(self, **data):
1987        self.redirect(self.url(self.context))
1988
1989class BalancePaymentAddFormPage(KofaAddFormPage):
1990    """ Page to add an online payment which can balance s previous session
1991    payment.
1992    """
1993    grok.context(IStudentPaymentsContainer)
1994    grok.name('addbp')
1995    grok.require('waeup.manageStudent')
1996    form_fields = grok.AutoFields(IStudentBalancePayment)
1997    label = _('Add balance')
1998    pnav = 4
1999
2000    @action(_('Create ticket'), style='primary')
2001    def createTicket(self, **data):
2002        p_category = data['p_category']
2003        balance_session = data.get('balance_session', None)
2004        balance_level = data.get('balance_level', None)
2005        balance_amount = data.get('balance_amount', None)
2006        student = self.context.__parent__
2007        students_utils = getUtility(IStudentsUtils)
2008        error, payment = students_utils.setBalanceDetails(
2009            p_category, student, balance_session,
2010            balance_level, balance_amount)
2011        if error is not None:
2012            self.flash(error, type="danger")
2013            return
2014        self.context[payment.p_id] = payment
2015        self.flash(_('Payment ticket created.'))
2016        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2017        self.redirect(self.url(self.context))
2018        return
2019
2020    @action(_('Cancel'), validator=NullValidator)
2021    def cancel(self, **data):
2022        self.redirect(self.url(self.context))
2023
2024class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2025    """ Page to view an online payment ticket
2026    """
2027    grok.context(IStudentOnlinePayment)
2028    grok.name('index')
2029    grok.require('waeup.viewStudent')
2030    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2031    form_fields[
2032        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2033    form_fields[
2034        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2035    pnav = 4
2036
2037    @property
2038    def label(self):
2039        return _('${a}: Online Payment Ticket ${b}', mapping = {
2040            'a':self.context.student.display_fullname,
2041            'b':self.context.p_id})
2042
2043class OnlinePaymentApproveView(UtilityView, grok.View):
2044    """ Callback view
2045    """
2046    grok.context(IStudentOnlinePayment)
2047    grok.name('approve')
2048    grok.require('waeup.managePortal')
2049
2050    def update(self):
2051        flashtype, msg, log = self.context.approveStudentPayment()
2052        if log is not None:
2053            # Add log message to students.log
2054            self.context.writeLogMessage(self,log)
2055            # Add log message to payments.log
2056            self.context.logger.info(
2057                '%s,%s,%s,%s,%s,,,,,,' % (
2058                self.context.student.student_id,
2059                self.context.p_id, self.context.p_category,
2060                self.context.amount_auth, self.context.r_code))
2061        self.flash(msg, type=flashtype)
2062        return
2063
2064    def render(self):
2065        self.redirect(self.url(self.context, '@@index'))
2066        return
2067
2068class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2069    """ Approval view for students.
2070
2071    This view is used for browser tests only and
2072    must be neutralized in custom pages!
2073    """
2074    grok.name('fake_approve')
2075    grok.require('waeup.payStudent')
2076
2077class ExportPDFPaymentSlip(UtilityView, grok.View):
2078    """Deliver a PDF slip of the context.
2079    """
2080    grok.context(IStudentOnlinePayment)
2081    grok.name('payment_slip.pdf')
2082    grok.require('waeup.viewStudent')
2083    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
2084    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2085    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2086    prefix = 'form'
2087    note = None
2088    omit_fields = (
2089        'password', 'suspended', 'phone', 'date_of_birth',
2090        'adm_code', 'sex', 'suspended_comment', 'current_level',
2091        'flash_notice')
2092
2093    @property
2094    def title(self):
2095        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2096        return translate(_('Payment Data'), 'waeup.kofa',
2097            target_language=portal_language)
2098
2099    @property
2100    def label(self):
2101        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2102        return translate(_('Online Payment Slip'),
2103            'waeup.kofa', target_language=portal_language) \
2104            + ' %s' % self.context.p_id
2105
2106    def render(self):
2107        #if self.context.p_state != 'paid':
2108        #    self.flash('Ticket not yet paid.')
2109        #    self.redirect(self.url(self.context))
2110        #    return
2111        studentview = StudentBasePDFFormPage(self.context.student,
2112            self.request, self.omit_fields)
2113        students_utils = getUtility(IStudentsUtils)
2114        return students_utils.renderPDF(self, 'payment_slip.pdf',
2115            self.context.student, studentview, note=self.note,
2116            omit_fields=self.omit_fields)
2117
2118
2119class AccommodationManageFormPage(KofaEditFormPage):
2120    """ Page to manage bed tickets.
2121
2122    This manage form page is for both students and students officers.
2123    """
2124    grok.context(IStudentAccommodation)
2125    grok.name('index')
2126    grok.require('waeup.handleAccommodation')
2127    form_fields = grok.AutoFields(IStudentAccommodation)
2128    grok.template('accommodationmanagepage')
2129    pnav = 4
2130    with_hostel_selection = True
2131
2132    @property
2133    def booking_allowed(self):
2134        students_utils = getUtility(IStudentsUtils)
2135        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2136        error_message = students_utils.checkAccommodationRequirements(
2137            self.context.student, acc_details)
2138        if error_message:
2139            return False
2140        return True
2141
2142    @property
2143    def actionsgroup1(self):
2144        if not self.booking_allowed:
2145            return []
2146        if not self.with_hostel_selection:
2147            return []
2148        return [_('Save')]
2149
2150    @property
2151    def actionsgroup2(self):
2152        if getattr(self.request.principal, 'user_type', None) == 'student':
2153            ## Book button can be disabled in custom packages by
2154            ## uncommenting the following lines.
2155            #if not self.booking_allowed:
2156            #    return []
2157            return [_('Book accommodation')]
2158        return [_('Book accommodation'), _('Remove selected')]
2159
2160    @property
2161    def label(self):
2162        return _('${a}: Accommodation',
2163            mapping = {'a':self.context.__parent__.display_fullname})
2164
2165    @property
2166    def desired_hostel(self):
2167        if self.context.desired_hostel:
2168            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2169            if hostel is not None:
2170                return hostel.hostel_name
2171        return
2172
2173    def getHostels(self):
2174        """Get a list of all stored hostels.
2175        """
2176        yield(dict(name=None, title='--', selected=''))
2177        for val in grok.getSite()['hostels'].values():
2178            selected = ''
2179            if val.hostel_id == self.context.desired_hostel:
2180                selected = 'selected'
2181            yield(dict(name=val.hostel_id, title=val.hostel_name,
2182                       selected=selected))
2183
2184    @action(_('Save'), style='primary')
2185    def save(self):
2186        hostel = self.request.form.get('hostel', None)
2187        self.context.desired_hostel = hostel
2188        self.flash(_('Your selection has been saved.'))
2189        return
2190
2191    @action(_('Book accommodation'), style='primary')
2192    def bookAccommodation(self, **data):
2193        self.redirect(self.url(self.context, 'add'))
2194        return
2195
2196    @jsaction(_('Remove selected'))
2197    def delBedTickets(self, **data):
2198        if getattr(self.request.principal, 'user_type', None) == 'student':
2199            self.flash(_('You are not allowed to remove bed tickets.'),
2200                       type="warning")
2201            self.redirect(self.url(self.context))
2202            return
2203        form = self.request.form
2204        if 'val_id' in form:
2205            child_id = form['val_id']
2206        else:
2207            self.flash(_('No bed ticket selected.'), type="warning")
2208            self.redirect(self.url(self.context))
2209            return
2210        if not isinstance(child_id, list):
2211            child_id = [child_id]
2212        deleted = []
2213        for id in child_id:
2214            del self.context[id]
2215            deleted.append(id)
2216        if len(deleted):
2217            self.flash(_('Successfully removed: ${a}',
2218                mapping = {'a':', '.join(deleted)}))
2219            self.context.writeLogMessage(
2220                self,'removed: % s' % ', '.join(deleted))
2221        self.redirect(self.url(self.context))
2222        return
2223
2224class BedTicketAddPage(KofaPage):
2225    """ Page to add a bed ticket
2226    """
2227    grok.context(IStudentAccommodation)
2228    grok.name('add')
2229    grok.require('waeup.handleAccommodation')
2230    grok.template('enterpin')
2231    ac_prefix = 'HOS'
2232    label = _('Add bed ticket')
2233    pnav = 4
2234    buttonname = _('Create bed ticket')
2235    notice = ''
2236    with_ac = True
2237
2238    def update(self, SUBMIT=None):
2239        student = self.context.student
2240        students_utils = getUtility(IStudentsUtils)
2241        acc_details  = students_utils.getAccommodationDetails(student)
2242        error_message = students_utils.checkAccommodationRequirements(
2243            student, acc_details)
2244        if error_message:
2245            self.flash(error_message, type="warning")
2246            self.redirect(self.url(self.context))
2247            return
2248        if self.with_ac:
2249            self.ac_series = self.request.form.get('ac_series', None)
2250            self.ac_number = self.request.form.get('ac_number', None)
2251        if SUBMIT is None:
2252            return
2253        if self.with_ac:
2254            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2255            code = get_access_code(pin)
2256            if not code:
2257                self.flash(_('Activation code is invalid.'), type="warning")
2258                return
2259        # Search and book bed
2260        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2261        entries = cat.searchResults(
2262            owner=(student.student_id,student.student_id))
2263        if len(entries):
2264            # If bed space has been manually allocated use this bed
2265            manual = True
2266            bed = [entry for entry in entries][0]
2267            # Safety belt for paranoids: Does this bed really exist on portal?
2268            # XXX: Can be remove if nobody complains.
2269            if bed.__parent__.__parent__ is None:
2270                self.flash(_('System error: Please contact the adminsitrator.'),
2271                           type="danger")
2272                self.context.writeLogMessage(
2273                    self, 'fatal error: %s' % bed.bed_id)
2274                return
2275        else:
2276            # else search for other available beds
2277            manual = False
2278            entries = cat.searchResults(
2279                bed_type=(acc_details['bt'],acc_details['bt']))
2280            available_beds = [
2281                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2282            if available_beds:
2283                students_utils = getUtility(IStudentsUtils)
2284                bed = students_utils.selectBed(
2285                    available_beds, self.context.desired_hostel)
2286                if bed is None:
2287                    self.flash(_(
2288                        'There is no free bed in your desired hostel. '
2289                        'Please try another hostel.'),
2290                        type="warning")
2291                    self.redirect(self.url(self.context))
2292                    return
2293                # Safety belt for paranoids: Does this bed really exist
2294                # in portal?
2295                # XXX: Can be remove if nobody complains.
2296                if bed.__parent__.__parent__ is None:
2297                    self.flash(_(
2298                        'System error: Please contact the administrator.'),
2299                        type="warning")
2300                    self.context.writeLogMessage(
2301                        self, 'fatal error: %s' % bed.bed_id)
2302                    return
2303                bed.bookBed(student.student_id)
2304            else:
2305                self.flash(_('There is no free bed in your category ${a}.',
2306                    mapping = {'a':acc_details['bt']}), type="warning")
2307                self.redirect(self.url(self.context))
2308                return
2309        if self.with_ac:
2310            # Mark pin as used (this also fires a pin related transition)
2311            if code.state == USED:
2312                self.flash(_('Activation code has already been used.'),
2313                           type="warning")
2314                if not manual:
2315                    # Release the previously booked bed
2316                    bed.owner = NOT_OCCUPIED
2317                    # Catalog must be informed
2318                    notify(grok.ObjectModifiedEvent(bed))
2319                return
2320            else:
2321                comment = _(u'invalidated')
2322                # Here we know that the ac is in state initialized so we do not
2323                # expect an exception, but the owner might be different
2324                success = invalidate_accesscode(
2325                    pin, comment, self.context.student.student_id)
2326                if not success:
2327                    self.flash(_('You are not the owner of this access code.'),
2328                               type="warning")
2329                    if not manual:
2330                        # Release the previously booked bed
2331                        bed.owner = NOT_OCCUPIED
2332                        # Catalog must be informed
2333                        notify(grok.ObjectModifiedEvent(bed))
2334                    return
2335        # Create bed ticket
2336        bedticket = createObject(u'waeup.BedTicket')
2337        if self.with_ac:
2338            bedticket.booking_code = pin
2339        bedticket.booking_session = acc_details['booking_session']
2340        bedticket.bed_type = acc_details['bt']
2341        bedticket.bed = bed
2342        hall_title = bed.__parent__.hostel_name
2343        coordinates = bed.coordinates[1:]
2344        block, room_nr, bed_nr = coordinates
2345        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2346            'a':hall_title, 'b':block,
2347            'c':room_nr, 'd':bed_nr,
2348            'e':bed.bed_type})
2349        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2350        bedticket.bed_coordinates = translate(
2351            bc, 'waeup.kofa',target_language=portal_language)
2352        self.context.addBedTicket(bedticket)
2353        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2354        self.flash(_('Bed ticket created and bed booked: ${a}',
2355            mapping = {'a':bedticket.display_coordinates}))
2356        self.redirect(self.url(self.context))
2357        return
2358
2359class BedTicketDisplayFormPage(KofaDisplayFormPage):
2360    """ Page to display bed tickets
2361    """
2362    grok.context(IBedTicket)
2363    grok.name('index')
2364    grok.require('waeup.handleAccommodation')
2365    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2366    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2367    pnav = 4
2368
2369    @property
2370    def label(self):
2371        return _('Bed Ticket for Session ${a}',
2372            mapping = {'a':self.context.getSessionString()})
2373
2374class ExportPDFBedTicketSlip(UtilityView, grok.View):
2375    """Deliver a PDF slip of the context.
2376    """
2377    grok.context(IBedTicket)
2378    grok.name('bed_allocation_slip.pdf')
2379    grok.require('waeup.handleAccommodation')
2380    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2381    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2382    prefix = 'form'
2383    omit_fields = (
2384        'password', 'suspended', 'phone', 'adm_code',
2385        'suspended_comment', 'date_of_birth', 'current_level',
2386        'flash_notice')
2387
2388    @property
2389    def title(self):
2390        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2391        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2392            target_language=portal_language)
2393
2394    @property
2395    def label(self):
2396        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2397        #return translate(_('Bed Allocation: '),
2398        #    'waeup.kofa', target_language=portal_language) \
2399        #    + ' %s' % self.context.bed_coordinates
2400        return translate(_('Bed Allocation Slip'),
2401            'waeup.kofa', target_language=portal_language) \
2402            + ' %s' % self.context.getSessionString()
2403
2404    def render(self):
2405        studentview = StudentBasePDFFormPage(self.context.student,
2406            self.request, self.omit_fields)
2407        students_utils = getUtility(IStudentsUtils)
2408        return students_utils.renderPDF(
2409            self, 'bed_allocation_slip.pdf',
2410            self.context.student, studentview,
2411            omit_fields=self.omit_fields)
2412
2413class BedTicketRelocationView(UtilityView, grok.View):
2414    """ Callback view
2415    """
2416    grok.context(IBedTicket)
2417    grok.name('relocate')
2418    grok.require('waeup.manageHostels')
2419
2420    # Relocate student if student parameters have changed or the bed_type
2421    # of the bed has changed
2422    def update(self):
2423        success, msg = self.context.relocateStudent()
2424        if not success:
2425            self.flash(msg, type="warning")
2426        else:
2427            self.flash(msg)
2428        self.redirect(self.url(self.context))
2429        return
2430
2431    def render(self):
2432        return
2433
2434class StudentHistoryPage(KofaPage):
2435    """ Page to display student history
2436    """
2437    grok.context(IStudent)
2438    grok.name('history')
2439    grok.require('waeup.viewStudent')
2440    grok.template('studenthistory')
2441    pnav = 4
2442
2443    @property
2444    def label(self):
2445        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2446
2447# Pages for students only
2448
2449class StudentBaseEditFormPage(KofaEditFormPage):
2450    """ View to edit student base data
2451    """
2452    grok.context(IStudent)
2453    grok.name('edit_base')
2454    grok.require('waeup.handleStudent')
2455    form_fields = grok.AutoFields(IStudentBase).select(
2456        'email', 'phone')
2457    label = _('Edit base data')
2458    pnav = 4
2459
2460    @action(_('Save'), style='primary')
2461    def save(self, **data):
2462        msave(self, **data)
2463        return
2464
2465class StudentChangePasswordPage(KofaEditFormPage):
2466    """ View to edit student passwords
2467    """
2468    grok.context(IStudent)
2469    grok.name('change_password')
2470    grok.require('waeup.handleStudent')
2471    grok.template('change_password')
2472    label = _('Change password')
2473    pnav = 4
2474
2475    @action(_('Save'), style='primary')
2476    def save(self, **data):
2477        form = self.request.form
2478        password = form.get('change_password', None)
2479        password_ctl = form.get('change_password_repeat', None)
2480        if password:
2481            validator = getUtility(IPasswordValidator)
2482            errors = validator.validate_password(password, password_ctl)
2483            if not errors:
2484                IUserAccount(self.context).setPassword(password)
2485                # Unset temporary password
2486                self.context.temp_password = None
2487                self.context.writeLogMessage(self, 'saved: password')
2488                self.flash(_('Password changed.'))
2489            else:
2490                self.flash( ' '.join(errors), type="warning")
2491        return
2492
2493class StudentFilesUploadPage(KofaPage):
2494    """ View to upload files by student
2495    """
2496    grok.context(IStudent)
2497    grok.name('change_portrait')
2498    grok.require('waeup.uploadStudentFile')
2499    grok.template('filesuploadpage')
2500    label = _('Upload portrait')
2501    pnav = 4
2502
2503    def update(self):
2504        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2505        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2506            emit_lock_message(self)
2507            return
2508        super(StudentFilesUploadPage, self).update()
2509        return
2510
2511class StartClearancePage(KofaPage):
2512    grok.context(IStudent)
2513    grok.name('start_clearance')
2514    grok.require('waeup.handleStudent')
2515    grok.template('enterpin')
2516    label = _('Start clearance')
2517    ac_prefix = 'CLR'
2518    notice = ''
2519    pnav = 4
2520    buttonname = _('Start clearance now')
2521    with_ac = True
2522
2523    @property
2524    def all_required_fields_filled(self):
2525        if not self.context.email:
2526            return _("Email address is missing."), 'edit_base'
2527        if not self.context.phone:
2528            return _("Phone number is missing."), 'edit_base'
2529        return
2530
2531    @property
2532    def portrait_uploaded(self):
2533        store = getUtility(IExtFileStore)
2534        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2535            return True
2536        return False
2537
2538    def update(self, SUBMIT=None):
2539        if not self.context.state == ADMITTED:
2540            self.flash(_("Wrong state"), type="warning")
2541            self.redirect(self.url(self.context))
2542            return
2543        if not self.portrait_uploaded:
2544            self.flash(_("No portrait uploaded."), type="warning")
2545            self.redirect(self.url(self.context, 'change_portrait'))
2546            return
2547        if self.all_required_fields_filled:
2548            arf_warning = self.all_required_fields_filled[0]
2549            arf_redirect = self.all_required_fields_filled[1]
2550            self.flash(arf_warning, type="warning")
2551            self.redirect(self.url(self.context, arf_redirect))
2552            return
2553        if self.with_ac:
2554            self.ac_series = self.request.form.get('ac_series', None)
2555            self.ac_number = self.request.form.get('ac_number', None)
2556        if SUBMIT is None:
2557            return
2558        if self.with_ac:
2559            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2560            code = get_access_code(pin)
2561            if not code:
2562                self.flash(_('Activation code is invalid.'), type="warning")
2563                return
2564            if code.state == USED:
2565                self.flash(_('Activation code has already been used.'),
2566                           type="warning")
2567                return
2568            # Mark pin as used (this also fires a pin related transition)
2569            # and fire transition start_clearance
2570            comment = _(u"invalidated")
2571            # Here we know that the ac is in state initialized so we do not
2572            # expect an exception, but the owner might be different
2573            if not invalidate_accesscode(pin, comment, self.context.student_id):
2574                self.flash(_('You are not the owner of this access code.'),
2575                           type="warning")
2576                return
2577            self.context.clr_code = pin
2578        IWorkflowInfo(self.context).fireTransition('start_clearance')
2579        self.flash(_('Clearance process has been started.'))
2580        self.redirect(self.url(self.context,'cedit'))
2581        return
2582
2583class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2584    """ View to edit student clearance data by student
2585    """
2586    grok.context(IStudent)
2587    grok.name('cedit')
2588    grok.require('waeup.handleStudent')
2589    label = _('Edit clearance data')
2590
2591    @property
2592    def form_fields(self):
2593        if self.context.is_postgrad:
2594            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2595                'clr_code', 'officer_comment')
2596        else:
2597            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2598                'clr_code', 'officer_comment')
2599        return form_fields
2600
2601    def update(self):
2602        if self.context.clearance_locked:
2603            emit_lock_message(self)
2604            return
2605        return super(StudentClearanceEditFormPage, self).update()
2606
2607    @action(_('Save'), style='primary')
2608    def save(self, **data):
2609        self.applyData(self.context, **data)
2610        self.flash(_('Clearance form has been saved.'))
2611        return
2612
2613    def dataNotComplete(self):
2614        """To be implemented in the customization package.
2615        """
2616        return False
2617
2618    @action(_('Save and request clearance'), style='primary',
2619            warning=_('You can not edit your data after '
2620            'requesting clearance. You really want to request clearance now?'))
2621    def requestClearance(self, **data):
2622        self.applyData(self.context, **data)
2623        if self.dataNotComplete():
2624            self.flash(self.dataNotComplete(), type="warning")
2625            return
2626        self.flash(_('Clearance form has been saved.'))
2627        if self.context.clr_code:
2628            self.redirect(self.url(self.context, 'request_clearance'))
2629        else:
2630            # We bypass the request_clearance page if student
2631            # has been imported in state 'clearance started' and
2632            # no clr_code was entered before.
2633            state = IWorkflowState(self.context).getState()
2634            if state != CLEARANCE:
2635                # This shouldn't happen, but the application officer
2636                # might have forgotten to lock the form after changing the state
2637                self.flash(_('This form cannot be submitted. Wrong state!'),
2638                           type="danger")
2639                return
2640            IWorkflowInfo(self.context).fireTransition('request_clearance')
2641            self.flash(_('Clearance has been requested.'))
2642            self.redirect(self.url(self.context))
2643        return
2644
2645class RequestClearancePage(KofaPage):
2646    grok.context(IStudent)
2647    grok.name('request_clearance')
2648    grok.require('waeup.handleStudent')
2649    grok.template('enterpin')
2650    label = _('Request clearance')
2651    notice = _('Enter the CLR access code used for starting clearance.')
2652    ac_prefix = 'CLR'
2653    pnav = 4
2654    buttonname = _('Request clearance now')
2655    with_ac = True
2656
2657    def update(self, SUBMIT=None):
2658        if self.with_ac:
2659            self.ac_series = self.request.form.get('ac_series', None)
2660            self.ac_number = self.request.form.get('ac_number', None)
2661        if SUBMIT is None:
2662            return
2663        if self.with_ac:
2664            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2665            if self.context.clr_code and self.context.clr_code != pin:
2666                self.flash(_("This isn't your CLR access code."), type="danger")
2667                return
2668        state = IWorkflowState(self.context).getState()
2669        if state != CLEARANCE:
2670            # This shouldn't happen, but the application officer
2671            # might have forgotten to lock the form after changing the state
2672            self.flash(_('This form cannot be submitted. Wrong state!'),
2673                       type="danger")
2674            return
2675        IWorkflowInfo(self.context).fireTransition('request_clearance')
2676        self.flash(_('Clearance has been requested.'))
2677        self.redirect(self.url(self.context))
2678        return
2679
2680class StartSessionPage(KofaPage):
2681    grok.context(IStudentStudyCourse)
2682    grok.name('start_session')
2683    grok.require('waeup.handleStudent')
2684    grok.template('enterpin')
2685    label = _('Start session')
2686    ac_prefix = 'SFE'
2687    notice = ''
2688    pnav = 4
2689    buttonname = _('Start now')
2690    with_ac = True
2691
2692    def update(self, SUBMIT=None):
2693        if not self.context.is_current:
2694            emit_lock_message(self)
2695            return
2696        super(StartSessionPage, self).update()
2697        if not self.context.next_session_allowed:
2698            self.flash(_("You are not entitled to start session."),
2699                       type="warning")
2700            self.redirect(self.url(self.context))
2701            return
2702        if self.with_ac:
2703            self.ac_series = self.request.form.get('ac_series', None)
2704            self.ac_number = self.request.form.get('ac_number', None)
2705        if SUBMIT is None:
2706            return
2707        if self.with_ac:
2708            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2709            code = get_access_code(pin)
2710            if not code:
2711                self.flash(_('Activation code is invalid.'), type="warning")
2712                return
2713            # Mark pin as used (this also fires a pin related transition)
2714            if code.state == USED:
2715                self.flash(_('Activation code has already been used.'),
2716                           type="warning")
2717                return
2718            else:
2719                comment = _(u"invalidated")
2720                # Here we know that the ac is in state initialized so we do not
2721                # expect an error, but the owner might be different
2722                if not invalidate_accesscode(
2723                    pin,comment,self.context.student.student_id):
2724                    self.flash(_('You are not the owner of this access code.'),
2725                               type="warning")
2726                    return
2727        try:
2728            if self.context.student.state == CLEARED:
2729                IWorkflowInfo(self.context.student).fireTransition(
2730                    'pay_first_school_fee')
2731            elif self.context.student.state == RETURNING:
2732                IWorkflowInfo(self.context.student).fireTransition(
2733                    'pay_school_fee')
2734            elif self.context.student.state == PAID:
2735                IWorkflowInfo(self.context.student).fireTransition(
2736                    'pay_pg_fee')
2737        except ConstraintNotSatisfied:
2738            self.flash(_('An error occurred, please contact the system administrator.'),
2739                       type="danger")
2740            return
2741        self.flash(_('Session started.'))
2742        self.redirect(self.url(self.context))
2743        return
2744
2745class AddStudyLevelFormPage(KofaEditFormPage):
2746    """ Page for students to add current study levels
2747    """
2748    grok.context(IStudentStudyCourse)
2749    grok.name('add')
2750    grok.require('waeup.handleStudent')
2751    grok.template('studyleveladdpage')
2752    form_fields = grok.AutoFields(IStudentStudyCourse)
2753    pnav = 4
2754
2755    @property
2756    def label(self):
2757        studylevelsource = StudyLevelSource().factory
2758        code = self.context.current_level
2759        title = studylevelsource.getTitle(self.context, code)
2760        return _('Add current level ${a}', mapping = {'a':title})
2761
2762    def update(self):
2763        if not self.context.is_current \
2764            or self.context.student.studycourse_locked:
2765            emit_lock_message(self)
2766            return
2767        if self.context.student.state != PAID:
2768            emit_lock_message(self)
2769            return
2770        code = self.context.current_level
2771        if code is None:
2772            self.flash(_('Your data are incomplete'), type="danger")
2773            self.redirect(self.url(self.context))
2774            return
2775        super(AddStudyLevelFormPage, self).update()
2776        return
2777
2778    @action(_('Create course list now'), style='primary')
2779    def addStudyLevel(self, **data):
2780        studylevel = createObject(u'waeup.StudentStudyLevel')
2781        studylevel.level = self.context.current_level
2782        studylevel.level_session = self.context.current_session
2783        try:
2784            self.context.addStudentStudyLevel(
2785                self.context.certificate,studylevel)
2786        except KeyError:
2787            self.flash(_('This level exists.'), type="warning")
2788            self.redirect(self.url(self.context))
2789            return
2790        except RequiredMissing:
2791            self.flash(_('Your data are incomplete.'), type="danger")
2792            self.redirect(self.url(self.context))
2793            return
2794        self.flash(_('You successfully created a new course list.'))
2795        self.redirect(self.url(self.context, str(studylevel.level)))
2796        return
2797
2798class StudyLevelEditFormPage(KofaEditFormPage):
2799    """ Page to edit the student study level data by students
2800    """
2801    grok.context(IStudentStudyLevel)
2802    grok.name('edit')
2803    grok.require('waeup.editStudyLevel')
2804    grok.template('studyleveleditpage')
2805    pnav = 4
2806    placeholder = _('Enter valid course code')
2807
2808    def update(self, ADD=None, course=None):
2809        if not self.context.__parent__.is_current:
2810            emit_lock_message(self)
2811            return
2812        if self.context.student.state != PAID or \
2813            not self.context.is_current_level:
2814            emit_lock_message(self)
2815            return
2816        super(StudyLevelEditFormPage, self).update()
2817        if ADD is not None:
2818            if not course:
2819                self.flash(_('No valid course code entered.'), type="warning")
2820                return
2821            cat = queryUtility(ICatalog, name='courses_catalog')
2822            result = cat.searchResults(code=(course, course))
2823            if len(result) != 1:
2824                self.flash(_('Course not found.'), type="warning")
2825                return
2826            course = list(result)[0]
2827            addCourseTicket(self, course)
2828        return
2829
2830    @property
2831    def label(self):
2832        # Here we know that the cookie has been set
2833        lang = self.request.cookies.get('kofa.language')
2834        level_title = translate(self.context.level_title, 'waeup.kofa',
2835            target_language=lang)
2836        return _('Edit course list of ${a}',
2837            mapping = {'a':level_title})
2838
2839    @property
2840    def translated_values(self):
2841        return translated_values(self)
2842
2843    def _delCourseTicket(self, **data):
2844        form = self.request.form
2845        if 'val_id' in form:
2846            child_id = form['val_id']
2847        else:
2848            self.flash(_('No ticket selected.'), type="warning")
2849            self.redirect(self.url(self.context, '@@edit'))
2850            return
2851        if not isinstance(child_id, list):
2852            child_id = [child_id]
2853        deleted = []
2854        for id in child_id:
2855            # Students are not allowed to remove core tickets
2856            if id in self.context and \
2857                self.context[id].removable_by_student:
2858                del self.context[id]
2859                deleted.append(id)
2860        if len(deleted):
2861            self.flash(_('Successfully removed: ${a}',
2862                mapping = {'a':', '.join(deleted)}))
2863            self.context.writeLogMessage(
2864                self,'removed: %s at %s' %
2865                (', '.join(deleted), self.context.level))
2866        self.redirect(self.url(self.context, u'@@edit'))
2867        return
2868
2869    @jsaction(_('Remove selected tickets'))
2870    def delCourseTicket(self, **data):
2871        self._delCourseTicket(**data)
2872        return
2873
2874    def _updateTickets(self, **data):
2875        cat = queryUtility(ICatalog, name='courses_catalog')
2876        invalidated = list()
2877        for value in self.context.values():
2878            result = cat.searchResults(code=(value.code, value.code))
2879            if len(result) != 1:
2880                course = None
2881            else:
2882                course = list(result)[0]
2883            invalid = self.context.updateCourseTicket(value, course)
2884            if invalid:
2885                invalidated.append(invalid)
2886        if invalidated:
2887            invalidated_string = ', '.join(invalidated)
2888            self.context.writeLogMessage(
2889                self, 'course tickets invalidated: %s' % invalidated_string)
2890        self.flash(_('All course tickets updated.'))
2891        return
2892
2893    @action(_('Update all tickets'),
2894        tooltip=_('Update all course parameters including course titles.'))
2895    def updateTickets(self, **data):
2896        self._updateTickets(**data)
2897        return
2898
2899    def _registerCourses(self, **data):
2900        if self.context.student.is_postgrad and \
2901            not self.context.student.is_special_postgrad:
2902            self.flash(_(
2903                "You are a postgraduate student, "
2904                "your course list can't bee registered."), type="warning")
2905            self.redirect(self.url(self.context))
2906            return
2907        students_utils = getUtility(IStudentsUtils)
2908        warning = students_utils.warnCreditsOOR(self.context)
2909        if warning:
2910            self.flash(warning, type="warning")
2911            return
2912        msg = self.context.course_registration_forbidden
2913        if msg:
2914            self.flash(msg, type="warning")
2915            return
2916        IWorkflowInfo(self.context.student).fireTransition(
2917            'register_courses')
2918        self.flash(_('Course list has been registered.'))
2919        self.redirect(self.url(self.context))
2920        return
2921
2922    @action(_('Register course list'), style='primary',
2923        warning=_('You can not edit your course list after registration.'
2924            ' You really want to register?'))
2925    def registerCourses(self, **data):
2926        self._registerCourses(**data)
2927        return
2928
2929class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2930    """Add a course ticket by student.
2931    """
2932    grok.name('ctadd')
2933    grok.require('waeup.handleStudent')
2934    form_fields = grok.AutoFields(ICourseTicketAdd)
2935
2936    def update(self):
2937        if self.context.student.state != PAID or \
2938            not self.context.is_current_level:
2939            emit_lock_message(self)
2940            return
2941        super(CourseTicketAddFormPage2, self).update()
2942        return
2943
2944    @action(_('Add course ticket'))
2945    def addCourseTicket(self, **data):
2946        # Safety belt
2947        if self.context.student.state != PAID:
2948            return
2949        course = data['course']
2950        success = addCourseTicket(self, course)
2951        if success:
2952            self.redirect(self.url(self.context, u'@@edit'))
2953        return
2954
2955class SetPasswordPage(KofaPage):
2956    grok.context(IKofaObject)
2957    grok.name('setpassword')
2958    grok.require('waeup.Anonymous')
2959    grok.template('setpassword')
2960    label = _('Set password for first-time login')
2961    ac_prefix = 'PWD'
2962    pnav = 0
2963    set_button = _('Set')
2964
2965    def update(self, SUBMIT=None):
2966        self.reg_number = self.request.form.get('reg_number', None)
2967        self.ac_series = self.request.form.get('ac_series', None)
2968        self.ac_number = self.request.form.get('ac_number', None)
2969
2970        if SUBMIT is None:
2971            return
2972        hitlist = search(query=self.reg_number,
2973            searchtype='reg_number', view=self)
2974        if not hitlist:
2975            self.flash(_('No student found.'), type="warning")
2976            return
2977        if len(hitlist) != 1:   # Cannot happen but anyway
2978            self.flash(_('More than one student found.'), type="warning")
2979            return
2980        student = hitlist[0].context
2981        self.student_id = student.student_id
2982        student_pw = student.password
2983        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2984        code = get_access_code(pin)
2985        if not code:
2986            self.flash(_('Access code is invalid.'), type="warning")
2987            return
2988        if student_pw and pin == student.adm_code:
2989            self.flash(_(
2990                'Password has already been set. Your Student Id is ${a}',
2991                mapping = {'a':self.student_id}))
2992            return
2993        elif student_pw:
2994            self.flash(
2995                _('Password has already been set. You are using the ' +
2996                'wrong Access Code.'), type="warning")
2997            return
2998        # Mark pin as used (this also fires a pin related transition)
2999        # and set student password
3000        if code.state == USED:
3001            self.flash(_('Access code has already been used.'), type="warning")
3002            return
3003        else:
3004            comment = _(u"invalidated")
3005            # Here we know that the ac is in state initialized so we do not
3006            # expect an exception
3007            invalidate_accesscode(pin,comment)
3008            IUserAccount(student).setPassword(self.ac_number)
3009            student.adm_code = pin
3010        self.flash(_('Password has been set. Your Student Id is ${a}',
3011            mapping = {'a':self.student_id}))
3012        return
3013
3014class StudentRequestPasswordPage(KofaAddFormPage):
3015    """Captcha'd request password page for students.
3016    """
3017    grok.name('requestpw')
3018    grok.require('waeup.Anonymous')
3019    grok.template('requestpw')
3020    form_fields = grok.AutoFields(IStudentRequestPW).select(
3021        'lastname','number','email')
3022    label = _('Request password for first-time login')
3023
3024    def update(self):
3025        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3026        if blocker:
3027            self.flash(_('The portal is in maintenance mode. '
3028                        'Password request forms are temporarily disabled.'),
3029                       type='warning')
3030            self.redirect(self.url(self.context))
3031            return
3032        # Handle captcha
3033        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3034        self.captcha_result = self.captcha.verify(self.request)
3035        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3036        return
3037
3038    def _redirect(self, email, password, student_id):
3039        # Forward only email to landing page in base package.
3040        self.redirect(self.url(self.context, 'requestpw_complete',
3041            data = dict(email=email)))
3042        return
3043
3044    def _redirect_no_student(self):
3045        # No record found, this is the truth. We do not redirect here.
3046        # We are using this method in custom packages
3047        # for redirecting alumni to the application section.
3048        self.flash(_('No student record found.'), type="warning")
3049        return
3050
3051    def _pw_used(self):
3052        # XXX: False if password has not been used. We need an extra
3053        #      attribute which remembers if student logged in.
3054        return True
3055
3056    @action(_('Send login credentials to email address'), style='primary')
3057    def get_credentials(self, **data):
3058        if not self.captcha_result.is_valid:
3059            # Captcha will display error messages automatically.
3060            # No need to flash something.
3061            return
3062        number = data.get('number','')
3063        lastname = data.get('lastname','')
3064        cat = getUtility(ICatalog, name='students_catalog')
3065        results = list(
3066            cat.searchResults(reg_number=(number, number)))
3067        if not results:
3068            results = list(
3069                cat.searchResults(matric_number=(number, number)))
3070        if results:
3071            student = results[0]
3072            if getattr(student,'lastname',None) is None:
3073                self.flash(_('An error occurred.'), type="danger")
3074                return
3075            elif student.lastname.lower() != lastname.lower():
3076                # Don't tell the truth here. Anonymous must not
3077                # know that a record was found and only the lastname
3078                # verification failed.
3079                self.flash(_('No student record found.'), type="warning")
3080                return
3081            elif student.password is not None and self._pw_used:
3082                self.flash(_('Your password has already been set and used. '
3083                             'Please proceed to the login page.'),
3084                           type="warning")
3085                return
3086            # Store email address but nothing else.
3087            student.email = data['email']
3088            notify(grok.ObjectModifiedEvent(student))
3089        else:
3090            self._redirect_no_student()
3091            return
3092
3093        kofa_utils = getUtility(IKofaUtils)
3094        password = kofa_utils.genPassword()
3095        mandate = PasswordMandate()
3096        mandate.params['password'] = password
3097        mandate.params['user'] = student
3098        site = grok.getSite()
3099        site['mandates'].addMandate(mandate)
3100        # Send email with credentials
3101        args = {'mandate_id':mandate.mandate_id}
3102        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3103        url_info = u'Confirmation link: %s' % mandate_url
3104        msg = _('You have successfully requested a password for the')
3105        if kofa_utils.sendCredentials(IUserAccount(student),
3106            password, url_info, msg):
3107            email_sent = student.email
3108        else:
3109            email_sent = None
3110        self._redirect(email=email_sent, password=password,
3111            student_id=student.student_id)
3112        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3113        self.context.logger.info(
3114            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3115        return
3116
3117class StudentRequestPasswordEmailSent(KofaPage):
3118    """Landing page after successful password request.
3119
3120    """
3121    grok.name('requestpw_complete')
3122    grok.require('waeup.Public')
3123    grok.template('requestpwmailsent')
3124    label = _('Your password request was successful.')
3125
3126    def update(self, email=None, student_id=None, password=None):
3127        self.email = email
3128        self.password = password
3129        self.student_id = student_id
3130        return
3131
3132class FilterStudentsInDepartmentPage(KofaPage):
3133    """Page that filters and lists students.
3134    """
3135    grok.context(IDepartment)
3136    grok.require('waeup.showStudents')
3137    grok.name('students')
3138    grok.template('filterstudentspage')
3139    pnav = 1
3140    session_label = _('Current Session')
3141    level_label = _('Current Level')
3142
3143    def label(self):
3144        return 'Students in %s' % self.context.longtitle
3145
3146    def _set_session_values(self):
3147        vocab_terms = academic_sessions_vocab.by_value.values()
3148        self.sessions = sorted(
3149            [(x.title, x.token) for x in vocab_terms], reverse=True)
3150        self.sessions += [('All Sessions', 'all')]
3151        return
3152
3153    def _set_level_values(self):
3154        vocab_terms = course_levels.by_value.values()
3155        self.levels = sorted(
3156            [(x.title, x.token) for x in vocab_terms])
3157        self.levels += [('All Levels', 'all')]
3158        return
3159
3160    def _searchCatalog(self, session, level):
3161        if level not in (10, 999, None):
3162            start_level = 100 * (level // 100)
3163            end_level = start_level + 90
3164        else:
3165            start_level = end_level = level
3166        cat = queryUtility(ICatalog, name='students_catalog')
3167        students = cat.searchResults(
3168            current_session=(session, session),
3169            current_level=(start_level, end_level),
3170            depcode=(self.context.code, self.context.code)
3171            )
3172        hitlist = []
3173        for student in students:
3174            hitlist.append(StudentQueryResultItem(student, view=self))
3175        return hitlist
3176
3177    def update(self, SHOW=None, session=None, level=None):
3178        self.parent_url = self.url(self.context.__parent__)
3179        self._set_session_values()
3180        self._set_level_values()
3181        self.hitlist = []
3182        self.session_default = session
3183        self.level_default = level
3184        if SHOW is not None:
3185            if session != 'all':
3186                self.session = int(session)
3187                self.session_string = '%s %s/%s' % (
3188                    self.session_label, self.session, self.session+1)
3189            else:
3190                self.session = None
3191                self.session_string = _('in any session')
3192            if level != 'all':
3193                self.level = int(level)
3194                self.level_string = '%s %s' % (self.level_label, self.level)
3195            else:
3196                self.level = None
3197                self.level_string = _('at any level')
3198            self.hitlist = self._searchCatalog(self.session, self.level)
3199            if not self.hitlist:
3200                self.flash(_('No student found.'), type="warning")
3201        return
3202
3203class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3204    """Page that filters and lists students.
3205    """
3206    grok.context(ICertificate)
3207
3208    def label(self):
3209        return 'Students studying %s' % self.context.longtitle
3210
3211    def _searchCatalog(self, session, level):
3212        if level not in (10, 999, None):
3213            start_level = 100 * (level // 100)
3214            end_level = start_level + 90
3215        else:
3216            start_level = end_level = level
3217        cat = queryUtility(ICatalog, name='students_catalog')
3218        students = cat.searchResults(
3219            current_session=(session, session),
3220            current_level=(start_level, end_level),
3221            certcode=(self.context.code, self.context.code)
3222            )
3223        hitlist = []
3224        for student in students:
3225            hitlist.append(StudentQueryResultItem(student, view=self))
3226        return hitlist
3227
3228class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3229    """Page that filters and lists students.
3230    """
3231    grok.context(ICourse)
3232    grok.require('waeup.viewStudent')
3233
3234    session_label = _('Session')
3235    level_label = _('Level')
3236
3237    def label(self):
3238        return 'Students registered for %s' % self.context.longtitle
3239
3240    def _searchCatalog(self, session, level):
3241        if level not in (10, 999, None):
3242            start_level = 100 * (level // 100)
3243            end_level = start_level + 90
3244        else:
3245            start_level = end_level = level
3246        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3247        coursetickets = cat.searchResults(
3248            session=(session, session),
3249            level=(start_level, end_level),
3250            code=(self.context.code, self.context.code)
3251            )
3252        hitlist = []
3253        for ticket in coursetickets:
3254            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3255        return list(set(hitlist))
3256
3257class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3258    """ Clear all students of a department in state 'clearance requested'.
3259    """
3260    grok.context(IDepartment)
3261    grok.name('clearallstudents')
3262    grok.require('waeup.clearAllStudents')
3263
3264    def update(self):
3265        cat = queryUtility(ICatalog, name='students_catalog')
3266        students = cat.searchResults(
3267            depcode=(self.context.code, self.context.code),
3268            state=(REQUESTED, REQUESTED)
3269            )
3270        num = 0
3271        for student in students:
3272            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3273                continue
3274            IWorkflowInfo(student).fireTransition('clear')
3275            num += 1
3276        self.flash(_('%d students have been cleared.' % num))
3277        self.redirect(self.url(self.context))
3278        return
3279
3280    def render(self):
3281        return
3282
3283
3284class EditScoresPage(KofaPage):
3285    """Page that allows to edit batches of scores.
3286    """
3287    grok.context(ICourse)
3288    grok.require('waeup.editScores')
3289    grok.name('edit_scores')
3290    grok.template('editscorespage')
3291    pnav = 1
3292    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3293
3294    def label(self):
3295        return '%s tickets in academic session %s' % (
3296            self.context.code, self.session_title)
3297
3298    def _searchCatalog(self, session):
3299        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3300        coursetickets = cat.searchResults(
3301            session=(session, session),
3302            code=(self.context.code, self.context.code)
3303            )
3304        return list(coursetickets)
3305
3306    def _extract_uploadfile(self, uploadfile):
3307        """Get a mapping of student-ids to scores.
3308
3309        The mapping is constructed by reading contents from `uploadfile`.
3310
3311        We expect uploadfile to be a regular CSV file with columns
3312        ``student_id`` and ``score`` (other cols are ignored).
3313        """
3314        result = dict()
3315        data = StringIO(uploadfile.read())  # ensure we have something seekable
3316        reader = csv.DictReader(data)
3317        for row in reader:
3318            if not 'student_id' in row or not 'score' in row:
3319                continue
3320            result[row['student_id']] = row['score']
3321        return result
3322
3323    def _update_scores(self, form):
3324        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3325        error = ''
3326        if 'UPDATE_FILE' in form:
3327            if form['uploadfile']:
3328                try:
3329                    formvals = self._extract_uploadfile(form['uploadfile'])
3330                except:
3331                    self.flash(
3332                        _('Uploaded file contains illegal data. Ignored'),
3333                        type="danger")
3334                    return False
3335            else:
3336                self.flash(
3337                    _('No file provided.'), type="danger")
3338                return False
3339        else:
3340            formvals = dict(zip(form['sids'], form['scores']))
3341        for ticket in self.editable_tickets:
3342            score = ticket.score
3343            sid = ticket.student.student_id
3344            if sid not in formvals:
3345                continue
3346            if formvals[sid] == '':
3347                score = None
3348            else:
3349                try:
3350                    score = int(formvals[sid])
3351                except ValueError:
3352                    error += '%s, ' % ticket.student.display_fullname
3353            if ticket.score != score:
3354                ticket.score = score
3355                ticket.student.__parent__.logger.info(
3356                    '%s - %s %s/%s score updated (%s)' % (
3357                        ob_class, ticket.student.student_id,
3358                        ticket.level, ticket.code, score)
3359                    )
3360        if error:
3361            self.flash(
3362                _('Error: Score(s) of following students have not been '
3363                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3364                type="danger")
3365        return True
3366
3367    def update(self,  *args, **kw):
3368        form = self.request.form
3369        self.current_academic_session = grok.getSite()[
3370            'configuration'].current_academic_session
3371        if self.context.__parent__.__parent__.score_editing_disabled:
3372            self.flash(_('Score editing disabled.'), type="warning")
3373            self.redirect(self.url(self.context))
3374            return
3375        if not self.current_academic_session:
3376            self.flash(_('Current academic session not set.'), type="warning")
3377            self.redirect(self.url(self.context))
3378            return
3379        self.session_title = academic_sessions_vocab.getTerm(
3380            self.current_academic_session).title
3381        self.tickets = self._searchCatalog(self.current_academic_session)
3382        if not self.tickets:
3383            self.flash(_('No student found.'), type="warning")
3384            self.redirect(self.url(self.context))
3385            return
3386        self.editable_tickets = [
3387            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3388        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3389            return
3390        if not self.editable_tickets:
3391            return
3392        success = self._update_scores(form)
3393        if success:
3394            self.flash(_('You successfully updated course results.'))
3395        return
3396
3397
3398class DownloadScoresView(UtilityView, grok.View):
3399    """View that exports scores.
3400    """
3401    grok.context(ICourse)
3402    grok.require('waeup.editScores')
3403    grok.name('download_scores')
3404
3405    def update(self):
3406        self.current_academic_session = grok.getSite()[
3407            'configuration'].current_academic_session
3408        if self.context.__parent__.__parent__.score_editing_disabled:
3409            self.flash(_('Score editing disabled.'), type="warning")
3410            self.redirect(self.url(self.context))
3411            return
3412        if not self.current_academic_session:
3413            self.flash(_('Current academic session not set.'), type="warning")
3414            self.redirect(self.url(self.context))
3415            return
3416        site = grok.getSite()
3417        exporter = getUtility(ICSVExporter, name='lecturer')
3418        self.csv = exporter.export_filtered(site, filepath=None,
3419                                 catalog='coursetickets',
3420                                 session=self.current_academic_session,
3421                                 level=None,
3422                                 code=self.context.code)
3423        return
3424
3425    def render(self):
3426        filename = 'results_%s_%s.csv' % (
3427            self.context.code, self.current_academic_session)
3428        self.response.setHeader(
3429            'Content-Type', 'text/csv; charset=UTF-8')
3430        self.response.setHeader(
3431            'Content-Disposition:', 'attachment; filename="%s' % filename)
3432        return self.csv
3433
3434class ExportPDFScoresSlip(UtilityView, grok.View,
3435    LocalRoleAssignmentUtilityView):
3436    """Deliver a PDF slip of course tickets for a lecturer.
3437    """
3438    grok.context(ICourse)
3439    grok.name('coursetickets.pdf')
3440    grok.require('waeup.editScores')
3441
3442    def data(self, session):
3443        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3444        coursetickets = cat.searchResults(
3445            session=(session, session),
3446            code=(self.context.code, self.context.code)
3447            )
3448        header = [[_('Matric No.'),
3449                   _('Reg. No.'),
3450                   _('Fullname'),
3451                   _('Status'),
3452                   _('Course of Studies'),
3453                   _('Level'),
3454                   _('Score') ],]
3455        tickets = []
3456        for ticket in list(coursetickets):
3457            row = [ticket.student.matric_number,
3458                  ticket.student.reg_number,
3459                  ticket.student.display_fullname,
3460                  ticket.student.translated_state,
3461                  ticket.student.certcode,
3462                  ticket.level,
3463                  ticket.score]
3464            tickets.append(row)
3465        return header + sorted(tickets, key=lambda value: value[0]), None
3466
3467    def render(self):
3468        session = grok.getSite()['configuration'].current_academic_session
3469        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3470                     if i['local_role'] == 'waeup.local.Lecturer']
3471        lecturers =  ', '.join(lecturers)
3472        students_utils = getUtility(IStudentsUtils)
3473        return students_utils.renderPDFCourseticketsOverview(
3474            self, session, self.data(session), lecturers, 'landscape', 90)
3475
3476class ExportJobContainerOverview(KofaPage):
3477    """Page that lists active student data export jobs and provides links
3478    to discard or download CSV files.
3479
3480    """
3481    grok.context(VirtualExportJobContainer)
3482    grok.require('waeup.showStudents')
3483    grok.name('index.html')
3484    grok.template('exportjobsindex')
3485    label = _('Student Data Exports')
3486    pnav = 1
3487    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3488
3489    def update(self, CREATE=None, DISCARD=None, job_id=None):
3490        if CREATE:
3491            self.redirect(self.url('@@exportconfig'))
3492            return
3493        if DISCARD and job_id:
3494            entry = self.context.entry_from_job_id(job_id)
3495            self.context.delete_export_entry(entry)
3496            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3497            self.context.logger.info(
3498                '%s - discarded: job_id=%s' % (ob_class, job_id))
3499            self.flash(_('Discarded export') + ' %s' % job_id)
3500        self.entries = doll_up(self, user=self.request.principal.id)
3501        return
3502
3503class ExportJobContainerJobConfig(KofaPage):
3504    """Page that configures a students export job.
3505
3506    This is a baseclass.
3507    """
3508    grok.baseclass()
3509    grok.name('exportconfig')
3510    grok.require('waeup.showStudents')
3511    grok.template('exportconfig')
3512    label = _('Configure student data export')
3513    pnav = 1
3514    redirect_target = ''
3515    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3516
3517    def _set_session_values(self):
3518        vocab_terms = academic_sessions_vocab.by_value.values()
3519        self.sessions = [(_('All Sessions'), 'all')]
3520        self.sessions += sorted(
3521            [(x.title, x.token) for x in vocab_terms], reverse=True)
3522        return
3523
3524    def _set_level_values(self):
3525        vocab_terms = course_levels.by_value.values()
3526        self.levels = [(_('All Levels'), 'all')]
3527        self.levels += sorted(
3528            [(x.title, x.token) for x in vocab_terms])
3529        return
3530
3531    def _set_mode_values(self):
3532        utils = getUtility(IKofaUtils)
3533        self.modes =[(_('All Modes'), 'all')]
3534        self.modes += sorted([(value, key) for key, value in
3535                      utils.STUDY_MODES_DICT.items()])
3536        return
3537
3538    def _set_paycat_values(self):
3539        utils = getUtility(IKofaUtils)
3540        self.paycats =[(_('All Payment Categories'), 'all')]
3541        self.paycats += sorted([(value, key) for key, value in
3542                      utils.PAYMENT_CATEGORIES.items()])
3543        return
3544
3545    def _set_exporter_values(self):
3546        # We provide all student exporters, nothing else, yet.
3547        # Bursary or Department Officers don't have the general exportData
3548        # permission and are only allowed to export bursary or payments
3549        # overview data respectively. This is the only place where
3550        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3551        # are used.
3552        exporters = []
3553        if not checkPermission('waeup.exportData', self.context):
3554            if checkPermission('waeup.exportBursaryData', self.context):
3555                exporters += [('Bursary Data', 'bursary')]
3556            if checkPermission('waeup.exportPaymentsOverview', self.context):
3557                exporters += [('School Fee Payments Overview',
3558                               'sfpaymentsoverview'),
3559                              ('Session Payments Overview',
3560                               'sessionpaymentsoverview')]
3561            self.exporters = exporters
3562            return
3563        STUDENT_EXPORTER_NAMES = getUtility(
3564            IStudentsUtils).STUDENT_EXPORTER_NAMES
3565        for name in STUDENT_EXPORTER_NAMES:
3566            util = getUtility(ICSVExporter, name=name)
3567            exporters.append((util.title, name),)
3568        self.exporters = exporters
3569        return
3570
3571    @property
3572    def faccode(self):
3573        return None
3574
3575    @property
3576    def depcode(self):
3577        return None
3578
3579    @property
3580    def certcode(self):
3581        return None
3582
3583    def update(self, START=None, session=None, level=None, mode=None,
3584               payments_start=None, payments_end=None, ct_level=None,
3585               ct_session=None, paycat=None, paysession=None, exporter=None):
3586        self._set_session_values()
3587        self._set_level_values()
3588        self._set_mode_values()
3589        self._set_paycat_values()
3590        self._set_exporter_values()
3591        if START is None:
3592            return
3593        ena = exports_not_allowed(self)
3594        if ena:
3595            self.flash(ena, type='danger')
3596            return
3597        if payments_start or payments_end:
3598            date_format = '%d/%m/%Y'
3599            try:
3600                datetime.strptime(payments_start, date_format)
3601                datetime.strptime(payments_end, date_format)
3602            except ValueError:
3603                self.flash(_('Payment dates do not match format d/m/Y.'),
3604                           type="danger")
3605                return
3606        if session == 'all':
3607            session=None
3608        if level == 'all':
3609            level = None
3610        if mode == 'all':
3611            mode = None
3612        if (mode,
3613            level,
3614            session,
3615            self.faccode,
3616            self.depcode,
3617            self.certcode) == (None, None, None, None, None, None):
3618            # Export all students including those without certificate
3619            job_id = self.context.start_export_job(exporter,
3620                                          self.request.principal.id,
3621                                          payments_start = payments_start,
3622                                          payments_end = payments_end,
3623                                          paycat=paycat,
3624                                          paysession=paysession,
3625                                          ct_level = ct_level,
3626                                          ct_session = ct_session,
3627                                          )
3628        else:
3629            job_id = self.context.start_export_job(exporter,
3630                                          self.request.principal.id,
3631                                          current_session=session,
3632                                          current_level=level,
3633                                          current_mode=mode,
3634                                          faccode=self.faccode,
3635                                          depcode=self.depcode,
3636                                          certcode=self.certcode,
3637                                          payments_start = payments_start,
3638                                          payments_end = payments_end,
3639                                          paycat=paycat,
3640                                          paysession=paysession,
3641                                          ct_level = ct_level,
3642                                          ct_session = ct_session,)
3643        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3644        self.context.logger.info(
3645            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3646            % (ob_class, exporter, session, level, mode, self.faccode,
3647            self.depcode, self.certcode, payments_start, payments_end,
3648            ct_level, ct_session, paycat, paysession, job_id))
3649        self.flash(_('Export started for students with') +
3650                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3651                   session, level, mode))
3652        self.redirect(self.url(self.redirect_target))
3653        return
3654
3655class ExportJobContainerDownload(ExportCSVView):
3656    """Page that downloads a students export csv file.
3657
3658    """
3659    grok.context(VirtualExportJobContainer)
3660    grok.require('waeup.showStudents')
3661
3662class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3663    """Page that configures a students export job in datacenter.
3664
3665    """
3666    grok.context(IDataCenter)
3667    redirect_target = '@@export'
3668
3669class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3670    """Page that configures a students export job in datacenter.
3671
3672    """
3673    grok.name('exportselected')
3674    grok.context(IDataCenter)
3675    redirect_target = '@@export'
3676    grok.template('exportselected')
3677    label = _('Configure student data export')
3678
3679    def update(self, START=None, students=None, exporter=None):
3680        self._set_exporter_values()
3681        if START is None:
3682            return
3683        ena = exports_not_allowed(self)
3684        if ena:
3685            self.flash(ena, type='danger')
3686            return
3687        try:
3688            ids = students.replace(',', ' ').split()
3689        except:
3690            self.flash(sys.exc_info()[1])
3691            self.redirect(self.url(self.redirect_target))
3692            return
3693        job_id = self.context.start_export_job(
3694            exporter, self.request.principal.id, selected=ids)
3695        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3696        self.context.logger.info(
3697            '%s - selected students exported: %s, job_id=%s' %
3698            (ob_class, exporter, job_id))
3699        self.flash(_('Export of selected students started.'))
3700        self.redirect(self.url(self.redirect_target))
3701        return
3702
3703class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3704    """Page that configures a students export job in facultiescontainer.
3705
3706    """
3707    grok.context(VirtualFacultiesExportJobContainer)
3708
3709
3710class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3711    """Page that configures a students export job in faculties.
3712
3713    """
3714    grok.context(VirtualFacultyExportJobContainer)
3715
3716    @property
3717    def faccode(self):
3718        return self.context.__parent__.code
3719
3720class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3721    """Page that configures a students export job in departments.
3722
3723    """
3724    grok.context(VirtualDepartmentExportJobContainer)
3725
3726    @property
3727    def depcode(self):
3728        return self.context.__parent__.code
3729
3730class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3731    """Page that configures a students export job for certificates.
3732
3733    """
3734    grok.context(VirtualCertificateExportJobContainer)
3735    grok.template('exportconfig_certificate')
3736
3737    @property
3738    def certcode(self):
3739        return self.context.__parent__.code
3740
3741class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3742    """Page that configures a students export job for courses.
3743
3744    In contrast to department or certificate student data exports the
3745    coursetickets_catalog is searched here. Therefore the update
3746    method from the base class is customized.
3747    """
3748    grok.context(VirtualCourseExportJobContainer)
3749    grok.template('exportconfig_course')
3750
3751    def _set_exporter_values(self):
3752        # We provide only the 'coursetickets' and 'lecturer' exporter
3753        # but can add more.
3754        exporters = []
3755        for name in ('coursetickets', 'lecturer'):
3756            util = getUtility(ICSVExporter, name=name)
3757            exporters.append((util.title, name),)
3758        self.exporters = exporters
3759
3760    def _set_session_values(self):
3761        # We allow only current academic session
3762        academic_session = grok.getSite()['configuration'].current_academic_session
3763        if not academic_session:
3764            self.sessions = []
3765            return
3766        x = academic_sessions_vocab.getTerm(academic_session)
3767        self.sessions = [(x.title, x.token)]
3768        return
3769
3770    def update(self, START=None, session=None, level=None, mode=None,
3771               exporter=None):
3772        self._set_session_values()
3773        self._set_level_values()
3774        self._set_mode_values()
3775        self._set_exporter_values()
3776        if not self.sessions:
3777            self.flash(
3778                _('Academic session not set. '
3779                  'Please contact the administrator.'),
3780                type='danger')
3781            self.redirect(self.url(self.context))
3782            return
3783        if START is None:
3784            return
3785        ena = exports_not_allowed(self)
3786        if ena:
3787            self.flash(ena, type='danger')
3788            return
3789        if session == 'all':
3790            session = None
3791        if level == 'all':
3792            level = None
3793        job_id = self.context.start_export_job(exporter,
3794                                      self.request.principal.id,
3795                                      # Use a different catalog and
3796                                      # pass different keywords than
3797                                      # for the (default) students_catalog
3798                                      catalog='coursetickets',
3799                                      session=session,
3800                                      level=level,
3801                                      code=self.context.__parent__.code)
3802        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3803        self.context.logger.info(
3804            '%s - exported: %s (%s, %s, %s), job_id=%s'
3805            % (ob_class, exporter, session, level,
3806            self.context.__parent__.code, job_id))
3807        self.flash(_('Export started for course tickets with') +
3808                   ' level_session=%s, level=%s' % (
3809                   session, level))
3810        self.redirect(self.url(self.redirect_target))
3811        return
Note: See TracBrowser for help on using the repository browser.