source: main/waeup.kofa/branches/henrik-transcript-workflow/src/waeup/kofa/students/browser.py @ 15141

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

Implement transcript validation workflow. More tests will follow.

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