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

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

Use MessageFactory? for all messages.

  • Property svn:keywords set to Id
File size: 142.5 KB
Line 
1## $Id: browser.py 15254 2018-11-23 13:35:46Z 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        note = None
2409        n = grok.getSite()['hostels'].allocation_expiration
2410        if n:
2411            note = _("""
2412<br /><br /><br /><br /><br /><font size="12">
2413Please endeavour to pay your hostel maintenance charge within ${a} days
2414 of being allocated a space or else you are deemed to have
2415 voluntarily forfeited it and it goes back into circulation to be
2416 available for booking afresh!</font>)
2417""")
2418            note = _(note, mapping={'a': n})
2419            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2420            note = translate(
2421                note, 'waeup.kofa', target_language=portal_language)
2422        return students_utils.renderPDF(
2423            self, 'bed_allocation_slip.pdf',
2424            self.context.student, studentview,
2425            omit_fields=self.omit_fields,
2426            note=note)
2427
2428class BedTicketRelocationView(UtilityView, grok.View):
2429    """ Callback view
2430    """
2431    grok.context(IBedTicket)
2432    grok.name('relocate')
2433    grok.require('waeup.manageHostels')
2434
2435    # Relocate student if student parameters have changed or the bed_type
2436    # of the bed has changed
2437    def update(self):
2438        success, msg = self.context.relocateStudent()
2439        if not success:
2440            self.flash(msg, type="warning")
2441        else:
2442            self.flash(msg)
2443        self.redirect(self.url(self.context))
2444        return
2445
2446    def render(self):
2447        return
2448
2449class StudentHistoryPage(KofaPage):
2450    """ Page to display student history
2451    """
2452    grok.context(IStudent)
2453    grok.name('history')
2454    grok.require('waeup.viewStudent')
2455    grok.template('studenthistory')
2456    pnav = 4
2457
2458    @property
2459    def label(self):
2460        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2461
2462# Pages for students only
2463
2464class StudentBaseEditFormPage(KofaEditFormPage):
2465    """ View to edit student base data
2466    """
2467    grok.context(IStudent)
2468    grok.name('edit_base')
2469    grok.require('waeup.handleStudent')
2470    form_fields = grok.AutoFields(IStudentBase).select(
2471        'email', 'phone')
2472    label = _('Edit base data')
2473    pnav = 4
2474
2475    @action(_('Save'), style='primary')
2476    def save(self, **data):
2477        msave(self, **data)
2478        return
2479
2480class StudentChangePasswordPage(KofaEditFormPage):
2481    """ View to edit student passwords
2482    """
2483    grok.context(IStudent)
2484    grok.name('change_password')
2485    grok.require('waeup.handleStudent')
2486    grok.template('change_password')
2487    label = _('Change password')
2488    pnav = 4
2489
2490    @action(_('Save'), style='primary')
2491    def save(self, **data):
2492        form = self.request.form
2493        password = form.get('change_password', None)
2494        password_ctl = form.get('change_password_repeat', None)
2495        if password:
2496            validator = getUtility(IPasswordValidator)
2497            errors = validator.validate_password(password, password_ctl)
2498            if not errors:
2499                IUserAccount(self.context).setPassword(password)
2500                # Unset temporary password
2501                self.context.temp_password = None
2502                self.context.writeLogMessage(self, 'saved: password')
2503                self.flash(_('Password changed.'))
2504            else:
2505                self.flash( ' '.join(errors), type="warning")
2506        return
2507
2508class StudentFilesUploadPage(KofaPage):
2509    """ View to upload files by student
2510    """
2511    grok.context(IStudent)
2512    grok.name('change_portrait')
2513    grok.require('waeup.uploadStudentFile')
2514    grok.template('filesuploadpage')
2515    label = _('Upload portrait')
2516    pnav = 4
2517
2518    def update(self):
2519        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2520        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2521            emit_lock_message(self)
2522            return
2523        super(StudentFilesUploadPage, self).update()
2524        return
2525
2526class StartClearancePage(KofaPage):
2527    grok.context(IStudent)
2528    grok.name('start_clearance')
2529    grok.require('waeup.handleStudent')
2530    grok.template('enterpin')
2531    label = _('Start clearance')
2532    ac_prefix = 'CLR'
2533    notice = ''
2534    pnav = 4
2535    buttonname = _('Start clearance now')
2536    with_ac = True
2537
2538    @property
2539    def all_required_fields_filled(self):
2540        if not self.context.email:
2541            return _("Email address is missing."), 'edit_base'
2542        if not self.context.phone:
2543            return _("Phone number is missing."), 'edit_base'
2544        return
2545
2546    @property
2547    def portrait_uploaded(self):
2548        store = getUtility(IExtFileStore)
2549        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2550            return True
2551        return False
2552
2553    def update(self, SUBMIT=None):
2554        if not self.context.state == ADMITTED:
2555            self.flash(_("Wrong state"), type="warning")
2556            self.redirect(self.url(self.context))
2557            return
2558        if not self.portrait_uploaded:
2559            self.flash(_("No portrait uploaded."), type="warning")
2560            self.redirect(self.url(self.context, 'change_portrait'))
2561            return
2562        if self.all_required_fields_filled:
2563            arf_warning = self.all_required_fields_filled[0]
2564            arf_redirect = self.all_required_fields_filled[1]
2565            self.flash(arf_warning, type="warning")
2566            self.redirect(self.url(self.context, arf_redirect))
2567            return
2568        if self.with_ac:
2569            self.ac_series = self.request.form.get('ac_series', None)
2570            self.ac_number = self.request.form.get('ac_number', None)
2571        if SUBMIT is None:
2572            return
2573        if self.with_ac:
2574            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2575            code = get_access_code(pin)
2576            if not code:
2577                self.flash(_('Activation code is invalid.'), type="warning")
2578                return
2579            if code.state == USED:
2580                self.flash(_('Activation code has already been used.'),
2581                           type="warning")
2582                return
2583            # Mark pin as used (this also fires a pin related transition)
2584            # and fire transition start_clearance
2585            comment = _(u"invalidated")
2586            # Here we know that the ac is in state initialized so we do not
2587            # expect an exception, but the owner might be different
2588            if not invalidate_accesscode(pin, comment, self.context.student_id):
2589                self.flash(_('You are not the owner of this access code.'),
2590                           type="warning")
2591                return
2592            self.context.clr_code = pin
2593        IWorkflowInfo(self.context).fireTransition('start_clearance')
2594        self.flash(_('Clearance process has been started.'))
2595        self.redirect(self.url(self.context,'cedit'))
2596        return
2597
2598class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2599    """ View to edit student clearance data by student
2600    """
2601    grok.context(IStudent)
2602    grok.name('cedit')
2603    grok.require('waeup.handleStudent')
2604    label = _('Edit clearance data')
2605
2606    @property
2607    def form_fields(self):
2608        if self.context.is_postgrad:
2609            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2610                'clr_code', 'officer_comment')
2611        else:
2612            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2613                'clr_code', 'officer_comment')
2614        return form_fields
2615
2616    def update(self):
2617        if self.context.clearance_locked:
2618            emit_lock_message(self)
2619            return
2620        return super(StudentClearanceEditFormPage, self).update()
2621
2622    @action(_('Save'), style='primary')
2623    def save(self, **data):
2624        self.applyData(self.context, **data)
2625        self.flash(_('Clearance form has been saved.'))
2626        return
2627
2628    def dataNotComplete(self):
2629        """To be implemented in the customization package.
2630        """
2631        return False
2632
2633    @action(_('Save and request clearance'), style='primary',
2634            warning=_('You can not edit your data after '
2635            'requesting clearance. You really want to request clearance now?'))
2636    def requestClearance(self, **data):
2637        self.applyData(self.context, **data)
2638        if self.dataNotComplete():
2639            self.flash(self.dataNotComplete(), type="warning")
2640            return
2641        self.flash(_('Clearance form has been saved.'))
2642        if self.context.clr_code:
2643            self.redirect(self.url(self.context, 'request_clearance'))
2644        else:
2645            # We bypass the request_clearance page if student
2646            # has been imported in state 'clearance started' and
2647            # no clr_code was entered before.
2648            state = IWorkflowState(self.context).getState()
2649            if state != CLEARANCE:
2650                # This shouldn't happen, but the application officer
2651                # might have forgotten to lock the form after changing the state
2652                self.flash(_('This form cannot be submitted. Wrong state!'),
2653                           type="danger")
2654                return
2655            IWorkflowInfo(self.context).fireTransition('request_clearance')
2656            self.flash(_('Clearance has been requested.'))
2657            self.redirect(self.url(self.context))
2658        return
2659
2660class RequestClearancePage(KofaPage):
2661    grok.context(IStudent)
2662    grok.name('request_clearance')
2663    grok.require('waeup.handleStudent')
2664    grok.template('enterpin')
2665    label = _('Request clearance')
2666    notice = _('Enter the CLR access code used for starting clearance.')
2667    ac_prefix = 'CLR'
2668    pnav = 4
2669    buttonname = _('Request clearance now')
2670    with_ac = True
2671
2672    def update(self, SUBMIT=None):
2673        if self.with_ac:
2674            self.ac_series = self.request.form.get('ac_series', None)
2675            self.ac_number = self.request.form.get('ac_number', None)
2676        if SUBMIT is None:
2677            return
2678        if self.with_ac:
2679            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2680            if self.context.clr_code and self.context.clr_code != pin:
2681                self.flash(_("This isn't your CLR access code."), type="danger")
2682                return
2683        state = IWorkflowState(self.context).getState()
2684        if state != CLEARANCE:
2685            # This shouldn't happen, but the application officer
2686            # might have forgotten to lock the form after changing the state
2687            self.flash(_('This form cannot be submitted. Wrong state!'),
2688                       type="danger")
2689            return
2690        IWorkflowInfo(self.context).fireTransition('request_clearance')
2691        self.flash(_('Clearance has been requested.'))
2692        self.redirect(self.url(self.context))
2693        return
2694
2695class StartSessionPage(KofaPage):
2696    grok.context(IStudentStudyCourse)
2697    grok.name('start_session')
2698    grok.require('waeup.handleStudent')
2699    grok.template('enterpin')
2700    label = _('Start session')
2701    ac_prefix = 'SFE'
2702    notice = ''
2703    pnav = 4
2704    buttonname = _('Start now')
2705    with_ac = True
2706
2707    def update(self, SUBMIT=None):
2708        if not self.context.is_current:
2709            emit_lock_message(self)
2710            return
2711        super(StartSessionPage, self).update()
2712        if not self.context.next_session_allowed:
2713            self.flash(_("You are not entitled to start session."),
2714                       type="warning")
2715            self.redirect(self.url(self.context))
2716            return
2717        if self.with_ac:
2718            self.ac_series = self.request.form.get('ac_series', None)
2719            self.ac_number = self.request.form.get('ac_number', None)
2720        if SUBMIT is None:
2721            return
2722        if self.with_ac:
2723            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2724            code = get_access_code(pin)
2725            if not code:
2726                self.flash(_('Activation code is invalid.'), type="warning")
2727                return
2728            # Mark pin as used (this also fires a pin related transition)
2729            if code.state == USED:
2730                self.flash(_('Activation code has already been used.'),
2731                           type="warning")
2732                return
2733            else:
2734                comment = _(u"invalidated")
2735                # Here we know that the ac is in state initialized so we do not
2736                # expect an error, but the owner might be different
2737                if not invalidate_accesscode(
2738                    pin,comment,self.context.student.student_id):
2739                    self.flash(_('You are not the owner of this access code.'),
2740                               type="warning")
2741                    return
2742        try:
2743            if self.context.student.state == CLEARED:
2744                IWorkflowInfo(self.context.student).fireTransition(
2745                    'pay_first_school_fee')
2746            elif self.context.student.state == RETURNING:
2747                IWorkflowInfo(self.context.student).fireTransition(
2748                    'pay_school_fee')
2749            elif self.context.student.state == PAID:
2750                IWorkflowInfo(self.context.student).fireTransition(
2751                    'pay_pg_fee')
2752        except ConstraintNotSatisfied:
2753            self.flash(_('An error occurred, please contact the system administrator.'),
2754                       type="danger")
2755            return
2756        self.flash(_('Session started.'))
2757        self.redirect(self.url(self.context))
2758        return
2759
2760class AddStudyLevelFormPage(KofaEditFormPage):
2761    """ Page for students to add current study levels
2762    """
2763    grok.context(IStudentStudyCourse)
2764    grok.name('add')
2765    grok.require('waeup.handleStudent')
2766    grok.template('studyleveladdpage')
2767    form_fields = grok.AutoFields(IStudentStudyCourse)
2768    pnav = 4
2769
2770    @property
2771    def label(self):
2772        studylevelsource = StudyLevelSource().factory
2773        code = self.context.current_level
2774        title = studylevelsource.getTitle(self.context, code)
2775        return _('Add current level ${a}', mapping = {'a':title})
2776
2777    def update(self):
2778        if not self.context.is_current \
2779            or self.context.student.studycourse_locked:
2780            emit_lock_message(self)
2781            return
2782        if self.context.student.state != PAID:
2783            emit_lock_message(self)
2784            return
2785        code = self.context.current_level
2786        if code is None:
2787            self.flash(_('Your data are incomplete'), type="danger")
2788            self.redirect(self.url(self.context))
2789            return
2790        super(AddStudyLevelFormPage, self).update()
2791        return
2792
2793    @action(_('Create course list now'), style='primary')
2794    def addStudyLevel(self, **data):
2795        studylevel = createObject(u'waeup.StudentStudyLevel')
2796        studylevel.level = self.context.current_level
2797        studylevel.level_session = self.context.current_session
2798        try:
2799            self.context.addStudentStudyLevel(
2800                self.context.certificate,studylevel)
2801        except KeyError:
2802            self.flash(_('This level exists.'), type="warning")
2803            self.redirect(self.url(self.context))
2804            return
2805        except RequiredMissing:
2806            self.flash(_('Your data are incomplete.'), type="danger")
2807            self.redirect(self.url(self.context))
2808            return
2809        self.flash(_('You successfully created a new course list.'))
2810        self.redirect(self.url(self.context, str(studylevel.level)))
2811        return
2812
2813class StudyLevelEditFormPage(KofaEditFormPage):
2814    """ Page to edit the student study level data by students
2815    """
2816    grok.context(IStudentStudyLevel)
2817    grok.name('edit')
2818    grok.require('waeup.editStudyLevel')
2819    grok.template('studyleveleditpage')
2820    pnav = 4
2821    placeholder = _('Enter valid course code')
2822
2823    def update(self, ADD=None, course=None):
2824        if not self.context.__parent__.is_current:
2825            emit_lock_message(self)
2826            return
2827        if self.context.student.state != PAID or \
2828            not self.context.is_current_level:
2829            emit_lock_message(self)
2830            return
2831        super(StudyLevelEditFormPage, self).update()
2832        if ADD is not None:
2833            if not course:
2834                self.flash(_('No valid course code entered.'), type="warning")
2835                return
2836            cat = queryUtility(ICatalog, name='courses_catalog')
2837            result = cat.searchResults(code=(course, course))
2838            if len(result) != 1:
2839                self.flash(_('Course not found.'), type="warning")
2840                return
2841            course = list(result)[0]
2842            addCourseTicket(self, course)
2843        return
2844
2845    @property
2846    def label(self):
2847        # Here we know that the cookie has been set
2848        lang = self.request.cookies.get('kofa.language')
2849        level_title = translate(self.context.level_title, 'waeup.kofa',
2850            target_language=lang)
2851        return _('Edit course list of ${a}',
2852            mapping = {'a':level_title})
2853
2854    @property
2855    def translated_values(self):
2856        return translated_values(self)
2857
2858    def _delCourseTicket(self, **data):
2859        form = self.request.form
2860        if 'val_id' in form:
2861            child_id = form['val_id']
2862        else:
2863            self.flash(_('No ticket selected.'), type="warning")
2864            self.redirect(self.url(self.context, '@@edit'))
2865            return
2866        if not isinstance(child_id, list):
2867            child_id = [child_id]
2868        deleted = []
2869        for id in child_id:
2870            # Students are not allowed to remove core tickets
2871            if id in self.context and \
2872                self.context[id].removable_by_student:
2873                del self.context[id]
2874                deleted.append(id)
2875        if len(deleted):
2876            self.flash(_('Successfully removed: ${a}',
2877                mapping = {'a':', '.join(deleted)}))
2878            self.context.writeLogMessage(
2879                self,'removed: %s at %s' %
2880                (', '.join(deleted), self.context.level))
2881        self.redirect(self.url(self.context, u'@@edit'))
2882        return
2883
2884    @jsaction(_('Remove selected tickets'))
2885    def delCourseTicket(self, **data):
2886        self._delCourseTicket(**data)
2887        return
2888
2889    def _updateTickets(self, **data):
2890        cat = queryUtility(ICatalog, name='courses_catalog')
2891        invalidated = list()
2892        for value in self.context.values():
2893            result = cat.searchResults(code=(value.code, value.code))
2894            if len(result) != 1:
2895                course = None
2896            else:
2897                course = list(result)[0]
2898            invalid = self.context.updateCourseTicket(value, course)
2899            if invalid:
2900                invalidated.append(invalid)
2901        if invalidated:
2902            invalidated_string = ', '.join(invalidated)
2903            self.context.writeLogMessage(
2904                self, 'course tickets invalidated: %s' % invalidated_string)
2905        self.flash(_('All course tickets updated.'))
2906        return
2907
2908    @action(_('Update all tickets'),
2909        tooltip=_('Update all course parameters including course titles.'))
2910    def updateTickets(self, **data):
2911        self._updateTickets(**data)
2912        return
2913
2914    def _registerCourses(self, **data):
2915        if self.context.student.is_postgrad and \
2916            not self.context.student.is_special_postgrad:
2917            self.flash(_(
2918                "You are a postgraduate student, "
2919                "your course list can't bee registered."), type="warning")
2920            self.redirect(self.url(self.context))
2921            return
2922        students_utils = getUtility(IStudentsUtils)
2923        warning = students_utils.warnCreditsOOR(self.context)
2924        if warning:
2925            self.flash(warning, type="warning")
2926            return
2927        msg = self.context.course_registration_forbidden
2928        if msg:
2929            self.flash(msg, type="warning")
2930            return
2931        IWorkflowInfo(self.context.student).fireTransition(
2932            'register_courses')
2933        self.flash(_('Course list has been registered.'))
2934        self.redirect(self.url(self.context))
2935        return
2936
2937    @action(_('Register course list'), style='primary',
2938        warning=_('You can not edit your course list after registration.'
2939            ' You really want to register?'))
2940    def registerCourses(self, **data):
2941        self._registerCourses(**data)
2942        return
2943
2944class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2945    """Add a course ticket by student.
2946    """
2947    grok.name('ctadd')
2948    grok.require('waeup.handleStudent')
2949    form_fields = grok.AutoFields(ICourseTicketAdd)
2950
2951    def update(self):
2952        if self.context.student.state != PAID or \
2953            not self.context.is_current_level:
2954            emit_lock_message(self)
2955            return
2956        super(CourseTicketAddFormPage2, self).update()
2957        return
2958
2959    @action(_('Add course ticket'))
2960    def addCourseTicket(self, **data):
2961        # Safety belt
2962        if self.context.student.state != PAID:
2963            return
2964        course = data['course']
2965        success = addCourseTicket(self, course)
2966        if success:
2967            self.redirect(self.url(self.context, u'@@edit'))
2968        return
2969
2970class SetPasswordPage(KofaPage):
2971    grok.context(IKofaObject)
2972    grok.name('setpassword')
2973    grok.require('waeup.Anonymous')
2974    grok.template('setpassword')
2975    label = _('Set password for first-time login')
2976    ac_prefix = 'PWD'
2977    pnav = 0
2978    set_button = _('Set')
2979
2980    def update(self, SUBMIT=None):
2981        self.reg_number = self.request.form.get('reg_number', None)
2982        self.ac_series = self.request.form.get('ac_series', None)
2983        self.ac_number = self.request.form.get('ac_number', None)
2984
2985        if SUBMIT is None:
2986            return
2987        hitlist = search(query=self.reg_number,
2988            searchtype='reg_number', view=self)
2989        if not hitlist:
2990            self.flash(_('No student found.'), type="warning")
2991            return
2992        if len(hitlist) != 1:   # Cannot happen but anyway
2993            self.flash(_('More than one student found.'), type="warning")
2994            return
2995        student = hitlist[0].context
2996        self.student_id = student.student_id
2997        student_pw = student.password
2998        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2999        code = get_access_code(pin)
3000        if not code:
3001            self.flash(_('Access code is invalid.'), type="warning")
3002            return
3003        if student_pw and pin == student.adm_code:
3004            self.flash(_(
3005                'Password has already been set. Your Student Id is ${a}',
3006                mapping = {'a':self.student_id}))
3007            return
3008        elif student_pw:
3009            self.flash(
3010                _('Password has already been set. You are using the ' +
3011                'wrong Access Code.'), type="warning")
3012            return
3013        # Mark pin as used (this also fires a pin related transition)
3014        # and set student password
3015        if code.state == USED:
3016            self.flash(_('Access code has already been used.'), type="warning")
3017            return
3018        else:
3019            comment = _(u"invalidated")
3020            # Here we know that the ac is in state initialized so we do not
3021            # expect an exception
3022            invalidate_accesscode(pin,comment)
3023            IUserAccount(student).setPassword(self.ac_number)
3024            student.adm_code = pin
3025        self.flash(_('Password has been set. Your Student Id is ${a}',
3026            mapping = {'a':self.student_id}))
3027        return
3028
3029class StudentRequestPasswordPage(KofaAddFormPage):
3030    """Captcha'd request password page for students.
3031    """
3032    grok.name('requestpw')
3033    grok.require('waeup.Anonymous')
3034    grok.template('requestpw')
3035    form_fields = grok.AutoFields(IStudentRequestPW).select(
3036        'lastname','number','email')
3037    label = _('Request password for first-time login')
3038
3039    def update(self):
3040        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3041        if blocker:
3042            self.flash(_('The portal is in maintenance mode. '
3043                        'Password request forms are temporarily disabled.'),
3044                       type='warning')
3045            self.redirect(self.url(self.context))
3046            return
3047        # Handle captcha
3048        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3049        self.captcha_result = self.captcha.verify(self.request)
3050        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3051        return
3052
3053    def _redirect(self, email, password, student_id):
3054        # Forward only email to landing page in base package.
3055        self.redirect(self.url(self.context, 'requestpw_complete',
3056            data = dict(email=email)))
3057        return
3058
3059    def _redirect_no_student(self):
3060        # No record found, this is the truth. We do not redirect here.
3061        # We are using this method in custom packages
3062        # for redirecting alumni to the application section.
3063        self.flash(_('No student record found.'), type="warning")
3064        return
3065
3066    def _pw_used(self):
3067        # XXX: False if password has not been used. We need an extra
3068        #      attribute which remembers if student logged in.
3069        return True
3070
3071    @action(_('Send login credentials to email address'), style='primary')
3072    def get_credentials(self, **data):
3073        if not self.captcha_result.is_valid:
3074            # Captcha will display error messages automatically.
3075            # No need to flash something.
3076            return
3077        number = data.get('number','')
3078        lastname = data.get('lastname','')
3079        cat = getUtility(ICatalog, name='students_catalog')
3080        results = list(
3081            cat.searchResults(reg_number=(number, number)))
3082        if not results:
3083            results = list(
3084                cat.searchResults(matric_number=(number, number)))
3085        if results:
3086            student = results[0]
3087            if getattr(student,'lastname',None) is None:
3088                self.flash(_('An error occurred.'), type="danger")
3089                return
3090            elif student.lastname.lower() != lastname.lower():
3091                # Don't tell the truth here. Anonymous must not
3092                # know that a record was found and only the lastname
3093                # verification failed.
3094                self.flash(_('No student record found.'), type="warning")
3095                return
3096            elif student.password is not None and self._pw_used:
3097                self.flash(_('Your password has already been set and used. '
3098                             'Please proceed to the login page.'),
3099                           type="warning")
3100                return
3101            # Store email address but nothing else.
3102            student.email = data['email']
3103            notify(grok.ObjectModifiedEvent(student))
3104        else:
3105            self._redirect_no_student()
3106            return
3107
3108        kofa_utils = getUtility(IKofaUtils)
3109        password = kofa_utils.genPassword()
3110        mandate = PasswordMandate()
3111        mandate.params['password'] = password
3112        mandate.params['user'] = student
3113        site = grok.getSite()
3114        site['mandates'].addMandate(mandate)
3115        # Send email with credentials
3116        args = {'mandate_id':mandate.mandate_id}
3117        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3118        url_info = u'Confirmation link: %s' % mandate_url
3119        msg = _('You have successfully requested a password for the')
3120        if kofa_utils.sendCredentials(IUserAccount(student),
3121            password, url_info, msg):
3122            email_sent = student.email
3123        else:
3124            email_sent = None
3125        self._redirect(email=email_sent, password=password,
3126            student_id=student.student_id)
3127        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3128        self.context.logger.info(
3129            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3130        return
3131
3132class StudentRequestPasswordEmailSent(KofaPage):
3133    """Landing page after successful password request.
3134
3135    """
3136    grok.name('requestpw_complete')
3137    grok.require('waeup.Public')
3138    grok.template('requestpwmailsent')
3139    label = _('Your password request was successful.')
3140
3141    def update(self, email=None, student_id=None, password=None):
3142        self.email = email
3143        self.password = password
3144        self.student_id = student_id
3145        return
3146
3147class FilterStudentsInDepartmentPage(KofaPage):
3148    """Page that filters and lists students.
3149    """
3150    grok.context(IDepartment)
3151    grok.require('waeup.showStudents')
3152    grok.name('students')
3153    grok.template('filterstudentspage')
3154    pnav = 1
3155    session_label = _('Current Session')
3156    level_label = _('Current Level')
3157
3158    def label(self):
3159        return 'Students in %s' % self.context.longtitle
3160
3161    def _set_session_values(self):
3162        vocab_terms = academic_sessions_vocab.by_value.values()
3163        self.sessions = sorted(
3164            [(x.title, x.token) for x in vocab_terms], reverse=True)
3165        self.sessions += [('All Sessions', 'all')]
3166        return
3167
3168    def _set_level_values(self):
3169        vocab_terms = course_levels.by_value.values()
3170        self.levels = sorted(
3171            [(x.title, x.token) for x in vocab_terms])
3172        self.levels += [('All Levels', 'all')]
3173        return
3174
3175    def _searchCatalog(self, session, level):
3176        if level not in (10, 999, None):
3177            start_level = 100 * (level // 100)
3178            end_level = start_level + 90
3179        else:
3180            start_level = end_level = level
3181        cat = queryUtility(ICatalog, name='students_catalog')
3182        students = cat.searchResults(
3183            current_session=(session, session),
3184            current_level=(start_level, end_level),
3185            depcode=(self.context.code, self.context.code)
3186            )
3187        hitlist = []
3188        for student in students:
3189            hitlist.append(StudentQueryResultItem(student, view=self))
3190        return hitlist
3191
3192    def update(self, SHOW=None, session=None, level=None):
3193        self.parent_url = self.url(self.context.__parent__)
3194        self._set_session_values()
3195        self._set_level_values()
3196        self.hitlist = []
3197        self.session_default = session
3198        self.level_default = level
3199        if SHOW is not None:
3200            if session != 'all':
3201                self.session = int(session)
3202                self.session_string = '%s %s/%s' % (
3203                    self.session_label, self.session, self.session+1)
3204            else:
3205                self.session = None
3206                self.session_string = _('in any session')
3207            if level != 'all':
3208                self.level = int(level)
3209                self.level_string = '%s %s' % (self.level_label, self.level)
3210            else:
3211                self.level = None
3212                self.level_string = _('at any level')
3213            self.hitlist = self._searchCatalog(self.session, self.level)
3214            if not self.hitlist:
3215                self.flash(_('No student found.'), type="warning")
3216        return
3217
3218class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3219    """Page that filters and lists students.
3220    """
3221    grok.context(ICertificate)
3222
3223    def label(self):
3224        return 'Students studying %s' % self.context.longtitle
3225
3226    def _searchCatalog(self, session, level):
3227        if level not in (10, 999, None):
3228            start_level = 100 * (level // 100)
3229            end_level = start_level + 90
3230        else:
3231            start_level = end_level = level
3232        cat = queryUtility(ICatalog, name='students_catalog')
3233        students = cat.searchResults(
3234            current_session=(session, session),
3235            current_level=(start_level, end_level),
3236            certcode=(self.context.code, self.context.code)
3237            )
3238        hitlist = []
3239        for student in students:
3240            hitlist.append(StudentQueryResultItem(student, view=self))
3241        return hitlist
3242
3243class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3244    """Page that filters and lists students.
3245    """
3246    grok.context(ICourse)
3247    grok.require('waeup.viewStudent')
3248
3249    session_label = _('Session')
3250    level_label = _('Level')
3251
3252    def label(self):
3253        return 'Students registered for %s' % self.context.longtitle
3254
3255    def _searchCatalog(self, session, level):
3256        if level not in (10, 999, None):
3257            start_level = 100 * (level // 100)
3258            end_level = start_level + 90
3259        else:
3260            start_level = end_level = level
3261        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3262        coursetickets = cat.searchResults(
3263            session=(session, session),
3264            level=(start_level, end_level),
3265            code=(self.context.code, self.context.code)
3266            )
3267        hitlist = []
3268        for ticket in coursetickets:
3269            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3270        return list(set(hitlist))
3271
3272class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3273    """ Clear all students of a department in state 'clearance requested'.
3274    """
3275    grok.context(IDepartment)
3276    grok.name('clearallstudents')
3277    grok.require('waeup.clearAllStudents')
3278
3279    def update(self):
3280        cat = queryUtility(ICatalog, name='students_catalog')
3281        students = cat.searchResults(
3282            depcode=(self.context.code, self.context.code),
3283            state=(REQUESTED, REQUESTED)
3284            )
3285        num = 0
3286        for student in students:
3287            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3288                continue
3289            IWorkflowInfo(student).fireTransition('clear')
3290            num += 1
3291        self.flash(_('%d students have been cleared.' % num))
3292        self.redirect(self.url(self.context))
3293        return
3294
3295    def render(self):
3296        return
3297
3298
3299class EditScoresPage(KofaPage):
3300    """Page that allows to edit batches of scores.
3301    """
3302    grok.context(ICourse)
3303    grok.require('waeup.editScores')
3304    grok.name('edit_scores')
3305    grok.template('editscorespage')
3306    pnav = 1
3307    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3308
3309    def label(self):
3310        return '%s tickets in academic session %s' % (
3311            self.context.code, self.session_title)
3312
3313    def _searchCatalog(self, session):
3314        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3315        # Attention: Also tickets of previous studycourses are found
3316        coursetickets = cat.searchResults(
3317            session=(session, session),
3318            code=(self.context.code, self.context.code)
3319            )
3320        return list(coursetickets)
3321
3322    def _extract_uploadfile(self, uploadfile):
3323        """Get a mapping of student-ids to scores.
3324
3325        The mapping is constructed by reading contents from `uploadfile`.
3326
3327        We expect uploadfile to be a regular CSV file with columns
3328        ``student_id`` and ``score`` (other cols are ignored).
3329        """
3330        result = dict()
3331        data = StringIO(uploadfile.read())  # ensure we have something seekable
3332        reader = csv.DictReader(data)
3333        for row in reader:
3334            if not 'student_id' in row or not 'score' in row:
3335                continue
3336            result[row['student_id']] = row['score']
3337        return result
3338
3339    def _update_scores(self, form):
3340        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3341        error = ''
3342        if 'UPDATE_FILE' in form:
3343            if form['uploadfile']:
3344                try:
3345                    formvals = self._extract_uploadfile(form['uploadfile'])
3346                except:
3347                    self.flash(
3348                        _('Uploaded file contains illegal data. Ignored'),
3349                        type="danger")
3350                    return False
3351            else:
3352                self.flash(
3353                    _('No file provided.'), type="danger")
3354                return False
3355        else:
3356            formvals = dict(zip(form['sids'], form['scores']))
3357        for ticket in self.editable_tickets:
3358            score = ticket.score
3359            sid = ticket.student.student_id
3360            if sid not in formvals:
3361                continue
3362            if formvals[sid] == '':
3363                score = None
3364            else:
3365                try:
3366                    score = int(formvals[sid])
3367                except ValueError:
3368                    error += '%s, ' % ticket.student.display_fullname
3369            if ticket.score != score:
3370                ticket.score = score
3371                ticket.student.__parent__.logger.info(
3372                    '%s - %s %s/%s score updated (%s)' % (
3373                        ob_class, ticket.student.student_id,
3374                        ticket.level, ticket.code, score)
3375                    )
3376        if error:
3377            self.flash(
3378                _('Error: Score(s) of following students have not been '
3379                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3380                type="danger")
3381        return True
3382
3383    def update(self,  *args, **kw):
3384        form = self.request.form
3385        self.current_academic_session = grok.getSite()[
3386            'configuration'].current_academic_session
3387        if self.context.__parent__.__parent__.score_editing_disabled:
3388            self.flash(_('Score editing disabled.'), type="warning")
3389            self.redirect(self.url(self.context))
3390            return
3391        if not self.current_academic_session:
3392            self.flash(_('Current academic session not set.'), type="warning")
3393            self.redirect(self.url(self.context))
3394            return
3395        self.session_title = academic_sessions_vocab.getTerm(
3396            self.current_academic_session).title
3397        self.tickets = self._searchCatalog(self.current_academic_session)
3398        if not self.tickets:
3399            self.flash(_('No student found.'), type="warning")
3400            self.redirect(self.url(self.context))
3401            return
3402        self.editable_tickets = [
3403            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3404        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3405            return
3406        if not self.editable_tickets:
3407            return
3408        success = self._update_scores(form)
3409        if success:
3410            self.flash(_('You successfully updated course results.'))
3411        return
3412
3413
3414class DownloadScoresView(UtilityView, grok.View):
3415    """View that exports scores.
3416    """
3417    grok.context(ICourse)
3418    grok.require('waeup.editScores')
3419    grok.name('download_scores')
3420
3421    def update(self):
3422        self.current_academic_session = grok.getSite()[
3423            'configuration'].current_academic_session
3424        if self.context.__parent__.__parent__.score_editing_disabled:
3425            self.flash(_('Score editing disabled.'), type="warning")
3426            self.redirect(self.url(self.context))
3427            return
3428        if not self.current_academic_session:
3429            self.flash(_('Current academic session not set.'), type="warning")
3430            self.redirect(self.url(self.context))
3431            return
3432        site = grok.getSite()
3433        exporter = getUtility(ICSVExporter, name='lecturer')
3434        self.csv = exporter.export_filtered(site, filepath=None,
3435                                 catalog='coursetickets',
3436                                 session=self.current_academic_session,
3437                                 level=None,
3438                                 code=self.context.code)
3439        return
3440
3441    def render(self):
3442        filename = 'results_%s_%s.csv' % (
3443            self.context.code, self.current_academic_session)
3444        self.response.setHeader(
3445            'Content-Type', 'text/csv; charset=UTF-8')
3446        self.response.setHeader(
3447            'Content-Disposition:', 'attachment; filename="%s' % filename)
3448        return self.csv
3449
3450class ExportPDFScoresSlip(UtilityView, grok.View,
3451    LocalRoleAssignmentUtilityView):
3452    """Deliver a PDF slip of course tickets for a lecturer.
3453    """
3454    grok.context(ICourse)
3455    grok.name('coursetickets.pdf')
3456    grok.require('waeup.editScores')
3457
3458    @property
3459    def note(self):
3460        return
3461
3462    def data(self, session):
3463        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3464        # Attention: Also tickets of previous studycourses are found
3465        coursetickets = cat.searchResults(
3466            session=(session, session),
3467            code=(self.context.code, self.context.code)
3468            )
3469        header = [[_('Matric No.'),
3470                   _('Reg. No.'),
3471                   _('Fullname'),
3472                   _('Status'),
3473                   _('Course of Studies'),
3474                   _('Level'),
3475                   _('Score') ],]
3476        tickets = []
3477        for ticket in list(coursetickets):
3478            row = [ticket.student.matric_number,
3479                  ticket.student.reg_number,
3480                  ticket.student.display_fullname,
3481                  ticket.student.translated_state,
3482                  ticket.student.certcode,
3483                  ticket.level,
3484                  ticket.score]
3485            tickets.append(row)
3486        return header + sorted(tickets, key=lambda value: value[0]), None
3487
3488    def render(self):
3489        session = grok.getSite()['configuration'].current_academic_session
3490        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3491                     if i['local_role'] == 'waeup.local.Lecturer']
3492        lecturers =  ', '.join(lecturers)
3493        students_utils = getUtility(IStudentsUtils)
3494        return students_utils.renderPDFCourseticketsOverview(
3495            self, session, self.data(session), lecturers, 'landscape', 90,
3496            self.note)
3497
3498class ExportJobContainerOverview(KofaPage):
3499    """Page that lists active student data export jobs and provides links
3500    to discard or download CSV files.
3501
3502    """
3503    grok.context(VirtualExportJobContainer)
3504    grok.require('waeup.showStudents')
3505    grok.name('index.html')
3506    grok.template('exportjobsindex')
3507    label = _('Student Data Exports')
3508    pnav = 1
3509    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3510
3511    def update(self, CREATE=None, DISCARD=None, job_id=None):
3512        if CREATE:
3513            self.redirect(self.url('@@exportconfig'))
3514            return
3515        if DISCARD and job_id:
3516            entry = self.context.entry_from_job_id(job_id)
3517            self.context.delete_export_entry(entry)
3518            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3519            self.context.logger.info(
3520                '%s - discarded: job_id=%s' % (ob_class, job_id))
3521            self.flash(_('Discarded export') + ' %s' % job_id)
3522        self.entries = doll_up(self, user=self.request.principal.id)
3523        return
3524
3525class ExportJobContainerJobConfig(KofaPage):
3526    """Page that configures a students export job.
3527
3528    This is a baseclass.
3529    """
3530    grok.baseclass()
3531    grok.name('exportconfig')
3532    grok.require('waeup.showStudents')
3533    grok.template('exportconfig')
3534    label = _('Configure student data export')
3535    pnav = 1
3536    redirect_target = ''
3537    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3538
3539    def _set_session_values(self):
3540        vocab_terms = academic_sessions_vocab.by_value.values()
3541        self.sessions = [(_('All Sessions'), 'all')]
3542        self.sessions += sorted(
3543            [(x.title, x.token) for x in vocab_terms], reverse=True)
3544        return
3545
3546    def _set_level_values(self):
3547        vocab_terms = course_levels.by_value.values()
3548        self.levels = [(_('All Levels'), 'all')]
3549        self.levels += sorted(
3550            [(x.title, x.token) for x in vocab_terms])
3551        return
3552
3553    def _set_mode_values(self):
3554        utils = getUtility(IKofaUtils)
3555        self.modes =[(_('All Modes'), 'all')]
3556        self.modes += sorted([(value, key) for key, value in
3557                      utils.STUDY_MODES_DICT.items()])
3558        return
3559
3560    def _set_paycat_values(self):
3561        utils = getUtility(IKofaUtils)
3562        self.paycats =[(_('All Payment Categories'), 'all')]
3563        self.paycats += sorted([(value, key) for key, value in
3564                      utils.PAYMENT_CATEGORIES.items()])
3565        return
3566
3567    def _set_exporter_values(self):
3568        # We provide all student exporters, nothing else, yet.
3569        # Bursary or Department Officers don't have the general exportData
3570        # permission and are only allowed to export bursary or payments
3571        # overview data respectively. This is the only place where
3572        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3573        # are used.
3574        exporters = []
3575        if not checkPermission('waeup.exportData', self.context):
3576            if checkPermission('waeup.exportBursaryData', self.context):
3577                exporters += [('Bursary Data', 'bursary')]
3578            if checkPermission('waeup.exportPaymentsOverview', self.context):
3579                exporters += [('School Fee Payments Overview',
3580                               'sfpaymentsoverview'),
3581                              ('Session Payments Overview',
3582                               'sessionpaymentsoverview')]
3583            self.exporters = exporters
3584            return
3585        STUDENT_EXPORTER_NAMES = getUtility(
3586            IStudentsUtils).STUDENT_EXPORTER_NAMES
3587        for name in STUDENT_EXPORTER_NAMES:
3588            util = getUtility(ICSVExporter, name=name)
3589            exporters.append((util.title, name),)
3590        self.exporters = exporters
3591        return
3592
3593    @property
3594    def faccode(self):
3595        return None
3596
3597    @property
3598    def depcode(self):
3599        return None
3600
3601    @property
3602    def certcode(self):
3603        return None
3604
3605    def update(self, START=None, session=None, level=None, mode=None,
3606               payments_start=None, payments_end=None, ct_level=None,
3607               ct_session=None, paycat=None, paysession=None, exporter=None):
3608        self._set_session_values()
3609        self._set_level_values()
3610        self._set_mode_values()
3611        self._set_paycat_values()
3612        self._set_exporter_values()
3613        if START is None:
3614            return
3615        ena = exports_not_allowed(self)
3616        if ena:
3617            self.flash(ena, type='danger')
3618            return
3619        if payments_start or payments_end:
3620            date_format = '%d/%m/%Y'
3621            try:
3622                datetime.strptime(payments_start, date_format)
3623                datetime.strptime(payments_end, date_format)
3624            except ValueError:
3625                self.flash(_('Payment dates do not match format d/m/Y.'),
3626                           type="danger")
3627                return
3628        if session == 'all':
3629            session=None
3630        if level == 'all':
3631            level = None
3632        if mode == 'all':
3633            mode = None
3634        if (mode,
3635            level,
3636            session,
3637            self.faccode,
3638            self.depcode,
3639            self.certcode) == (None, None, None, None, None, None):
3640            # Export all students including those without certificate
3641            job_id = self.context.start_export_job(exporter,
3642                                          self.request.principal.id,
3643                                          payments_start = payments_start,
3644                                          payments_end = payments_end,
3645                                          paycat=paycat,
3646                                          paysession=paysession,
3647                                          ct_level = ct_level,
3648                                          ct_session = ct_session,
3649                                          )
3650        else:
3651            job_id = self.context.start_export_job(exporter,
3652                                          self.request.principal.id,
3653                                          current_session=session,
3654                                          current_level=level,
3655                                          current_mode=mode,
3656                                          faccode=self.faccode,
3657                                          depcode=self.depcode,
3658                                          certcode=self.certcode,
3659                                          payments_start = payments_start,
3660                                          payments_end = payments_end,
3661                                          paycat=paycat,
3662                                          paysession=paysession,
3663                                          ct_level = ct_level,
3664                                          ct_session = ct_session,)
3665        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3666        self.context.logger.info(
3667            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3668            % (ob_class, exporter, session, level, mode, self.faccode,
3669            self.depcode, self.certcode, payments_start, payments_end,
3670            ct_level, ct_session, paycat, paysession, job_id))
3671        self.flash(_('Export started for students with') +
3672                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3673                   session, level, mode))
3674        self.redirect(self.url(self.redirect_target))
3675        return
3676
3677class ExportJobContainerDownload(ExportCSVView):
3678    """Page that downloads a students export csv file.
3679
3680    """
3681    grok.context(VirtualExportJobContainer)
3682    grok.require('waeup.showStudents')
3683
3684class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3685    """Page that configures a students export job in datacenter.
3686
3687    """
3688    grok.context(IDataCenter)
3689    redirect_target = '@@export'
3690
3691class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3692    """Page that configures a students export job in datacenter.
3693
3694    """
3695    grok.name('exportselected')
3696    grok.context(IDataCenter)
3697    redirect_target = '@@export'
3698    grok.template('exportselected')
3699    label = _('Configure student data export')
3700
3701    def update(self, START=None, students=None, exporter=None):
3702        self._set_exporter_values()
3703        if START is None:
3704            return
3705        ena = exports_not_allowed(self)
3706        if ena:
3707            self.flash(ena, type='danger')
3708            return
3709        try:
3710            ids = students.replace(',', ' ').split()
3711        except:
3712            self.flash(sys.exc_info()[1])
3713            self.redirect(self.url(self.redirect_target))
3714            return
3715        job_id = self.context.start_export_job(
3716            exporter, self.request.principal.id, selected=ids)
3717        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3718        self.context.logger.info(
3719            '%s - selected students exported: %s, job_id=%s' %
3720            (ob_class, exporter, job_id))
3721        self.flash(_('Export of selected students started.'))
3722        self.redirect(self.url(self.redirect_target))
3723        return
3724
3725class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3726    """Page that configures a students export job in facultiescontainer.
3727
3728    """
3729    grok.context(VirtualFacultiesExportJobContainer)
3730
3731
3732class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3733    """Page that configures a students export job in faculties.
3734
3735    """
3736    grok.context(VirtualFacultyExportJobContainer)
3737
3738    @property
3739    def faccode(self):
3740        return self.context.__parent__.code
3741
3742class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3743    """Page that configures a students export job in departments.
3744
3745    """
3746    grok.context(VirtualDepartmentExportJobContainer)
3747
3748    @property
3749    def depcode(self):
3750        return self.context.__parent__.code
3751
3752class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3753    """Page that configures a students export job for certificates.
3754
3755    """
3756    grok.context(VirtualCertificateExportJobContainer)
3757    grok.template('exportconfig_certificate')
3758
3759    @property
3760    def certcode(self):
3761        return self.context.__parent__.code
3762
3763class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3764    """Page that configures a students export job for courses.
3765
3766    In contrast to department or certificate student data exports the
3767    coursetickets_catalog is searched here. Therefore the update
3768    method from the base class is customized.
3769    """
3770    grok.context(VirtualCourseExportJobContainer)
3771    grok.template('exportconfig_course')
3772
3773    def _set_exporter_values(self):
3774        # We provide only the 'coursetickets' and 'lecturer' exporter
3775        # but can add more.
3776        exporters = []
3777        for name in ('coursetickets', 'lecturer'):
3778            util = getUtility(ICSVExporter, name=name)
3779            exporters.append((util.title, name),)
3780        self.exporters = exporters
3781
3782    def _set_session_values(self):
3783        # We allow only current academic session
3784        academic_session = grok.getSite()['configuration'].current_academic_session
3785        if not academic_session:
3786            self.sessions = []
3787            return
3788        x = academic_sessions_vocab.getTerm(academic_session)
3789        self.sessions = [(x.title, x.token)]
3790        return
3791
3792    def update(self, START=None, session=None, level=None, mode=None,
3793               exporter=None):
3794        self._set_session_values()
3795        self._set_level_values()
3796        self._set_mode_values()
3797        self._set_exporter_values()
3798        if not self.sessions:
3799            self.flash(
3800                _('Academic session not set. '
3801                  'Please contact the administrator.'),
3802                type='danger')
3803            self.redirect(self.url(self.context))
3804            return
3805        if START is None:
3806            return
3807        ena = exports_not_allowed(self)
3808        if ena:
3809            self.flash(ena, type='danger')
3810            return
3811        if session == 'all':
3812            session = None
3813        if level == 'all':
3814            level = None
3815        job_id = self.context.start_export_job(exporter,
3816                                      self.request.principal.id,
3817                                      # Use a different catalog and
3818                                      # pass different keywords than
3819                                      # for the (default) students_catalog
3820                                      catalog='coursetickets',
3821                                      session=session,
3822                                      level=level,
3823                                      code=self.context.__parent__.code)
3824        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3825        self.context.logger.info(
3826            '%s - exported: %s (%s, %s, %s), job_id=%s'
3827            % (ob_class, exporter, session, level,
3828            self.context.__parent__.code, job_id))
3829        self.flash(_('Export started for course tickets with') +
3830                   ' level_session=%s, level=%s' % (
3831                   session, level))
3832        self.redirect(self.url(self.redirect_target))
3833        return
Note: See TracBrowser for help on using the repository browser.