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

Last change on this file since 16113 was 16086, checked in by Henrik Bettermann, 5 years ago

Implement ExportPDFBaseDataPlusSlip (without button in the base package)

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