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

Last change on this file since 16254 was 16251, checked in by Henrik Bettermann, 4 years ago

Implement ExportPDFPersonalDataSlip (without button
in the base package). Rename ExportPDFBaseDataPlusSlip
ExportPDFStudyCourseBaseDataSlip.

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