source: main/waeup.kofa/branches/uli-py3/src/waeup/kofa/students/browser.py @ 17664

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

Adjust log messages and tests.

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