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

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

Add fields for transcript signatures.
Adjust search function.

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