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

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

Fix typo.

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