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

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

Do not allow course validation if no score has been entered.

  • Property svn:keywords set to Id
File size: 159.0 KB
Line 
1## $Id: browser.py 16007 2020-02-19 08:54:47Z 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 at %s' %
1716                (', '.join(deleted), self.context.level))
1717        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1718        return
1719
1720class StudyLevelRemarkFormPage(KofaEditFormPage):
1721    """ Page to edit the student study level transcript remark only
1722    """
1723    grok.context(IStudentStudyLevel)
1724    grok.name('remark')
1725    grok.require('waeup.processTranscript')
1726    grok.template('studylevelremarkpage')
1727    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1728    form_fields['level_session'].for_display = True
1729    form_fields['level_verdict'].for_display = True
1730    form_fields['validation_date'].for_display = True
1731    form_fields['validated_by'].for_display = True
1732
1733    def update(self, ADD=None, course=None):
1734        if self.context.student.studycourse_locked:
1735            emit_lock_message(self)
1736            return
1737        super(StudyLevelRemarkFormPage, self).update()
1738
1739    @property
1740    def label(self):
1741        lang = self.request.cookies.get('kofa.language')
1742        level_title = translate(self.context.level_title, 'waeup.kofa',
1743            target_language=lang)
1744        return _(
1745            'Edit transcript remark of level ${a}', mapping = {'a':level_title})
1746
1747    @property
1748    def translated_values(self):
1749        return translated_values(self)
1750
1751    @action(_('Save remark and go and back to transcript validation page'),
1752        style='primary')
1753    def save(self, **data):
1754        msave(self, **data)
1755        self.redirect(self.url(self.context.student)
1756            + '/studycourse/validate_transcript#tab4')
1757        return
1758
1759class ValidateCoursesView(UtilityView, grok.View):
1760    """ Validate course list by course adviser
1761    """
1762    grok.context(IStudentStudyLevel)
1763    grok.name('validate_courses')
1764    grok.require('waeup.validateStudent')
1765
1766    def update(self):
1767        if not self.context.__parent__.is_current:
1768            emit_lock_message(self)
1769            return
1770        if str(self.context.student.current_level) != self.context.__name__:
1771            self.flash(_('This is not the student\'s current level.'),
1772                       type="danger")
1773        elif self.context.student.state == REGISTERED:
1774            IWorkflowInfo(self.context.student).fireTransition(
1775                'validate_courses')
1776            self.flash(_('Course list has been validated.'))
1777        else:
1778            self.flash(_('Student is in the wrong state.'), type="warning")
1779        self.redirect(self.url(self.context))
1780        return
1781
1782    def render(self):
1783        return
1784
1785class RejectCoursesView(UtilityView, grok.View):
1786    """ Reject course list by course adviser
1787    """
1788    grok.context(IStudentStudyLevel)
1789    grok.name('reject_courses')
1790    grok.require('waeup.validateStudent')
1791
1792    def update(self):
1793        if not self.context.__parent__.is_current:
1794            emit_lock_message(self)
1795            return
1796        if str(self.context.__parent__.current_level) != self.context.__name__:
1797            self.flash(_('This is not the student\'s current level.'),
1798                       type="danger")
1799            self.redirect(self.url(self.context))
1800            return
1801        elif self.context.student.state == VALIDATED:
1802            IWorkflowInfo(self.context.student).fireTransition('reset8')
1803            message = _('Course list request has been annulled.')
1804            self.flash(message)
1805        elif self.context.student.state == REGISTERED:
1806            IWorkflowInfo(self.context.student).fireTransition('reset7')
1807            message = _('Course list has been unregistered.')
1808            self.flash(message)
1809        else:
1810            self.flash(_('Student is in the wrong state.'), type="warning")
1811            self.redirect(self.url(self.context))
1812            return
1813        args = {'subject':message}
1814        self.redirect(self.url(self.context.student) +
1815            '/contactstudent?%s' % urlencode(args))
1816        return
1817
1818    def render(self):
1819        return
1820
1821class UnregisterCoursesView(UtilityView, grok.View):
1822    """Unregister course list by student
1823    """
1824    grok.context(IStudentStudyLevel)
1825    grok.name('unregister_courses')
1826    grok.require('waeup.handleStudent')
1827
1828    def update(self):
1829        if not self.context.__parent__.is_current:
1830            emit_lock_message(self)
1831            return
1832        try:
1833            deadline = grok.getSite()['configuration'][
1834                str(self.context.level_session)].coursereg_deadline
1835        except (TypeError, KeyError):
1836            deadline = None
1837        if deadline and deadline < datetime.now(pytz.utc):
1838            self.flash(_(
1839                "Course registration has ended. "
1840                "Unregistration is disabled."), type="warning")
1841        elif str(self.context.__parent__.current_level) != self.context.__name__:
1842            self.flash(_('This is not your current level.'), type="danger")
1843        elif self.context.student.state == REGISTERED:
1844            IWorkflowInfo(self.context.student).fireTransition('reset7')
1845            message = _('Course list has been unregistered.')
1846            self.flash(message)
1847        else:
1848            self.flash(_('You are in the wrong state.'), type="warning")
1849        self.redirect(self.url(self.context))
1850        return
1851
1852    def render(self):
1853        return
1854
1855class CourseTicketAddFormPage(KofaAddFormPage):
1856    """Add a course ticket.
1857    """
1858    grok.context(IStudentStudyLevel)
1859    grok.name('add')
1860    grok.require('waeup.manageStudent')
1861    label = _('Add course ticket')
1862    form_fields = grok.AutoFields(ICourseTicketAdd)
1863    pnav = 4
1864
1865    def update(self):
1866        if not self.context.__parent__.is_current \
1867            or self.context.student.studycourse_locked:
1868            emit_lock_message(self)
1869            return
1870        super(CourseTicketAddFormPage, self).update()
1871        return
1872
1873    @action(_('Add course ticket'), style='primary')
1874    def addCourseTicket(self, **data):
1875        course = data['course']
1876        success = addCourseTicket(self, course)
1877        if success:
1878            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1879        return
1880
1881    @action(_('Cancel'), validator=NullValidator)
1882    def cancel(self, **data):
1883        self.redirect(self.url(self.context))
1884
1885class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1886    """ Page to display course tickets
1887    """
1888    grok.context(ICourseTicket)
1889    grok.name('index')
1890    grok.require('waeup.viewStudent')
1891    form_fields = grok.AutoFields(ICourseTicket).omit('course_category',
1892        'ticket_session')
1893    grok.template('courseticketpage')
1894    pnav = 4
1895
1896    @property
1897    def label(self):
1898        return _('${a}: Course Ticket ${b}', mapping = {
1899            'a':self.context.student.display_fullname,
1900            'b':self.context.code})
1901
1902class CourseTicketManageFormPage(KofaEditFormPage):
1903    """ Page to manage course tickets
1904    """
1905    grok.context(ICourseTicket)
1906    grok.name('manage')
1907    grok.require('waeup.manageStudent')
1908    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1909    form_fields['title'].for_display = True
1910    form_fields['fcode'].for_display = True
1911    form_fields['dcode'].for_display = True
1912    form_fields['semester'].for_display = True
1913    form_fields['passmark'].for_display = True
1914    form_fields['credits'].for_display = True
1915    form_fields['mandatory'].for_display = False
1916    form_fields['automatic'].for_display = True
1917    form_fields['carry_over'].for_display = True
1918    form_fields['ticket_session'].for_display = True
1919    pnav = 4
1920    grok.template('courseticketmanagepage')
1921
1922    def update(self):
1923        if not self.context.__parent__.__parent__.is_current \
1924            or self.context.student.studycourse_locked:
1925            emit_lock_message(self)
1926            return
1927        super(CourseTicketManageFormPage, self).update()
1928        return
1929
1930    @property
1931    def label(self):
1932        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1933
1934    @action('Save', style='primary')
1935    def save(self, **data):
1936        msave(self, **data)
1937        return
1938
1939class PaymentsManageFormPage(KofaEditFormPage):
1940    """ Page to manage the student payments
1941
1942    This manage form page is for both students and students officers.
1943    """
1944    grok.context(IStudentPaymentsContainer)
1945    grok.name('index')
1946    grok.require('waeup.viewStudent')
1947    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1948    grok.template('paymentsmanagepage')
1949    pnav = 4
1950
1951    @property
1952    def manage_payments_allowed(self):
1953        return checkPermission('waeup.payStudent', self.context)
1954
1955    def unremovable(self, ticket):
1956        usertype = getattr(self.request.principal, 'user_type', None)
1957        if not usertype:
1958            return False
1959        if not self.manage_payments_allowed:
1960            return True
1961        return (self.request.principal.user_type == 'student' and ticket.r_code)
1962
1963    @property
1964    def label(self):
1965        return _('${a}: Payments',
1966            mapping = {'a':self.context.__parent__.display_fullname})
1967
1968    @jsaction(_('Remove selected tickets'))
1969    def delPaymentTicket(self, **data):
1970        form = self.request.form
1971        if 'val_id' in form:
1972            child_id = form['val_id']
1973        else:
1974            self.flash(_('No payment selected.'), type="warning")
1975            self.redirect(self.url(self.context))
1976            return
1977        if not isinstance(child_id, list):
1978            child_id = [child_id]
1979        deleted = []
1980        for id in child_id:
1981            # Students are not allowed to remove used payment tickets
1982            ticket = self.context.get(id, None)
1983            if ticket is not None and not self.unremovable(ticket):
1984                del self.context[id]
1985                deleted.append(id)
1986        if len(deleted):
1987            self.flash(_('Successfully removed: ${a}',
1988                mapping = {'a': ', '.join(deleted)}))
1989            self.context.writeLogMessage(
1990                self,'removed: %s' % ', '.join(deleted))
1991        self.redirect(self.url(self.context))
1992        return
1993
1994    #@action(_('Add online payment ticket'))
1995    #def addPaymentTicket(self, **data):
1996    #    self.redirect(self.url(self.context, '@@addop'))
1997
1998class OnlinePaymentAddFormPage(KofaAddFormPage):
1999    """ Page to add an online payment ticket
2000    """
2001    grok.context(IStudentPaymentsContainer)
2002    grok.name('addop')
2003    grok.template('onlinepaymentaddform')
2004    grok.require('waeup.payStudent')
2005    form_fields = grok.AutoFields(IStudentOnlinePayment).select('p_combi')
2006    label = _('Add online payment')
2007    pnav = 4
2008
2009    @property
2010    def selectable_categories(self):
2011        student = self.context.__parent__
2012        categories = getUtility(
2013            IKofaUtils).selectable_payment_categories(student)
2014        return sorted(categories.items(), key=lambda value: value[1])
2015
2016    @action(_('Create ticket'), style='primary')
2017    def createTicket(self, **data):
2018        form = self.request.form
2019        p_category = form.get('form.p_category', None)
2020        p_combi = form.get('form.p_combi', [])
2021        if isinstance(form.get('form.p_combi', None), unicode):
2022            p_combi = [p_combi,]
2023        student = self.context.__parent__
2024        # The hostel_application payment category is temporarily used
2025        # by Uniben.
2026        if p_category in ('bed_allocation', 'hostel_application') and student[
2027            'studycourse'].current_session != grok.getSite()[
2028            'hostels'].accommodation_session:
2029                self.flash(
2030                    _('Your current session does not match ' + \
2031                    'accommodation session.'), type="danger")
2032                return
2033        if 'maintenance' in p_category:
2034            current_session = str(student['studycourse'].current_session)
2035            if not current_session in student['accommodation']:
2036                self.flash(_('You have not yet booked accommodation.'),
2037                           type="warning")
2038                return
2039        students_utils = getUtility(IStudentsUtils)
2040        error, payment = students_utils.setPaymentDetails(
2041            p_category, student, None, None, p_combi)
2042        if error is not None:
2043            self.flash(error, type="danger")
2044            return
2045        if p_category == 'transfer':
2046            payment.p_item = form['new_programme']
2047        self.context[payment.p_id] = payment
2048        self.flash(_('Payment ticket created.'))
2049        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2050        self.redirect(self.url(self.context))
2051        return
2052
2053    @action(_('Cancel'), validator=NullValidator)
2054    def cancel(self, **data):
2055        self.redirect(self.url(self.context))
2056
2057class PreviousPaymentAddFormPage(KofaAddFormPage):
2058    """ Page to add an online payment ticket for previous sessions.
2059    """
2060    grok.context(IStudentPaymentsContainer)
2061    grok.name('addpp')
2062    grok.require('waeup.payStudent')
2063    form_fields = grok.AutoFields(IStudentPreviousPayment)
2064    label = _('Add previous session online payment')
2065    pnav = 4
2066
2067    def update(self):
2068        if self.context.student.before_payment:
2069            self.flash(_("No previous payment to be made."), type="warning")
2070            self.redirect(self.url(self.context))
2071        super(PreviousPaymentAddFormPage, self).update()
2072        return
2073
2074    @action(_('Create ticket'), style='primary')
2075    def createTicket(self, **data):
2076        p_category = data['p_category']
2077        previous_session = data.get('p_session', None)
2078        previous_level = data.get('p_level', None)
2079        student = self.context.__parent__
2080        students_utils = getUtility(IStudentsUtils)
2081        error, payment = students_utils.setPaymentDetails(
2082            p_category, student, previous_session, previous_level, None)
2083        if error is not None:
2084            self.flash(error, type="danger")
2085            return
2086        self.context[payment.p_id] = payment
2087        self.flash(_('Payment ticket created.'))
2088        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2089        self.redirect(self.url(self.context))
2090        return
2091
2092    @action(_('Cancel'), validator=NullValidator)
2093    def cancel(self, **data):
2094        self.redirect(self.url(self.context))
2095
2096class BalancePaymentAddFormPage(KofaAddFormPage):
2097    """ Page to add an online payment which can balance s previous session
2098    payment.
2099    """
2100    grok.context(IStudentPaymentsContainer)
2101    grok.name('addbp')
2102    grok.require('waeup.manageStudent')
2103    form_fields = grok.AutoFields(IStudentBalancePayment)
2104    label = _('Add balance')
2105    pnav = 4
2106
2107    @action(_('Create ticket'), style='primary')
2108    def createTicket(self, **data):
2109        p_category = data['p_category']
2110        balance_session = data.get('balance_session', None)
2111        balance_level = data.get('balance_level', None)
2112        balance_amount = data.get('balance_amount', None)
2113        student = self.context.__parent__
2114        students_utils = getUtility(IStudentsUtils)
2115        error, payment = students_utils.setBalanceDetails(
2116            p_category, student, balance_session,
2117            balance_level, balance_amount)
2118        if error is not None:
2119            self.flash(error, type="danger")
2120            return
2121        self.context[payment.p_id] = payment
2122        self.flash(_('Payment ticket created.'))
2123        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
2124        self.redirect(self.url(self.context))
2125        return
2126
2127    @action(_('Cancel'), validator=NullValidator)
2128    def cancel(self, **data):
2129        self.redirect(self.url(self.context))
2130
2131class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
2132    """ Page to view an online payment ticket
2133    """
2134    grok.context(IStudentOnlinePayment)
2135    grok.name('index')
2136    grok.require('waeup.viewStudent')
2137    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2138        'p_item', 'p_combi')
2139    form_fields[
2140        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2141    form_fields[
2142        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2143    pnav = 4
2144
2145    @property
2146    def label(self):
2147        return _('${a}: Online Payment Ticket ${b}', mapping = {
2148            'a':self.context.student.display_fullname,
2149            'b':self.context.p_id})
2150
2151class OnlinePaymentApproveView(UtilityView, grok.View):
2152    """ Callback view
2153    """
2154    grok.context(IStudentOnlinePayment)
2155    grok.name('approve')
2156    grok.require('waeup.managePortal')
2157
2158    def update(self):
2159        flashtype, msg, log = self.context.approveStudentPayment()
2160        if log is not None:
2161            # Add log message to students.log
2162            self.context.writeLogMessage(self,log)
2163            # Add log message to payments.log
2164            self.context.logger.info(
2165                '%s,%s,%s,%s,%s,,,,,,' % (
2166                self.context.student.student_id,
2167                self.context.p_id, self.context.p_category,
2168                self.context.amount_auth, self.context.r_code))
2169        self.flash(msg, type=flashtype)
2170        return
2171
2172    def render(self):
2173        self.redirect(self.url(self.context, '@@index'))
2174        return
2175
2176class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
2177    """ Approval view for students.
2178
2179    This view is used for browser tests only and
2180    must be neutralized on custom pages!
2181    """
2182    grok.name('fake_approve')
2183    grok.require('waeup.payStudent')
2184
2185class ExportPDFPaymentSlip(UtilityView, grok.View):
2186    """Deliver a PDF slip of the context.
2187    """
2188    grok.context(IStudentOnlinePayment)
2189    grok.name('payment_slip.pdf')
2190    grok.require('waeup.viewStudent')
2191    form_fields = grok.AutoFields(IStudentOnlinePayment).omit(
2192        'p_item', 'p_combi')
2193    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2194    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2195    prefix = 'form'
2196    note = None
2197    omit_fields = (
2198        'password', 'suspended', 'phone', 'date_of_birth',
2199        'adm_code', 'sex', 'suspended_comment', 'current_level',
2200        'flash_notice')
2201
2202    @property
2203    def title(self):
2204        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2205        return translate(_('Payment Data'), 'waeup.kofa',
2206            target_language=portal_language)
2207
2208    @property
2209    def label(self):
2210        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2211        return translate(_('Online Payment Slip'),
2212            'waeup.kofa', target_language=portal_language) \
2213            + ' %s' % self.context.p_id
2214
2215    def render(self):
2216        #if self.context.p_state != 'paid':
2217        #    self.flash('Ticket not yet paid.')
2218        #    self.redirect(self.url(self.context))
2219        #    return
2220        studentview = StudentBasePDFFormPage(self.context.student,
2221            self.request, self.omit_fields)
2222        students_utils = getUtility(IStudentsUtils)
2223        return students_utils.renderPDF(self, 'payment_slip.pdf',
2224            self.context.student, studentview, note=self.note,
2225            omit_fields=self.omit_fields)
2226
2227class AccommodationDisplayFormPage(KofaDisplayFormPage):
2228    """ Page to view bed tickets.
2229    This manage form page is for both students and students officers.
2230    """
2231    grok.context(IStudentAccommodation)
2232    grok.name('index')
2233    grok.require('waeup.viewStudent')
2234    form_fields = grok.AutoFields(IStudentAccommodation)
2235    grok.template('accommodationpage')
2236    pnav = 4
2237    with_hostel_selection = True
2238
2239    @property
2240    def label(self):
2241        return _('${a}: Accommodation',
2242            mapping = {'a':self.context.__parent__.display_fullname})
2243
2244    @property
2245    def desired_hostel(self):
2246        if self.context.desired_hostel == 'no':
2247            return _('No favoured hostel')
2248        if self.context.desired_hostel:
2249            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2250            if hostel is not None:
2251                return hostel.hostel_name
2252        return
2253
2254    def update(self):
2255        if checkPermission('waeup.handleAccommodation', self.context):
2256            self.redirect(self.url(self.context, 'manage'))
2257
2258class AccommodationManageFormPage(KofaEditFormPage):
2259    """ Page to manage bed tickets.
2260
2261    This manage form page is for both students and students officers.
2262    """
2263    grok.context(IStudentAccommodation)
2264    grok.name('manage')
2265    grok.require('waeup.handleAccommodation')
2266    form_fields = grok.AutoFields(IStudentAccommodation)
2267    grok.template('accommodationmanagepage')
2268    pnav = 4
2269    with_hostel_selection = True
2270
2271    @property
2272    def booking_allowed(self):
2273        students_utils = getUtility(IStudentsUtils)
2274        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2275        error_message = students_utils.checkAccommodationRequirements(
2276            self.context.student, acc_details)
2277        if error_message:
2278            return False
2279        return True
2280
2281    @property
2282    def actionsgroup1(self):
2283        if not self.booking_allowed:
2284            return []
2285        if not self.with_hostel_selection:
2286            return []
2287        return [_('Save')]
2288
2289    @property
2290    def actionsgroup2(self):
2291        if getattr(self.request.principal, 'user_type', None) == 'student':
2292            ## Book button can be disabled in custom packages by
2293            ## uncommenting the following lines.
2294            #if not self.booking_allowed:
2295            #    return []
2296            return [_('Book accommodation')]
2297        return [_('Book accommodation'), _('Remove selected')]
2298
2299    @property
2300    def label(self):
2301        return _('${a}: Accommodation',
2302            mapping = {'a':self.context.__parent__.display_fullname})
2303
2304    @property
2305    def desired_hostel(self):
2306        if self.context.desired_hostel == 'no':
2307            return _('No favoured hostel')
2308        if self.context.desired_hostel:
2309            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2310            if hostel is not None:
2311                return hostel.hostel_name
2312        return
2313
2314    def getHostels(self):
2315        """Get a list of all stored hostels.
2316        """
2317        yield(dict(name=None, title='--', selected=''))
2318        selected = ''
2319        if self.context.desired_hostel == 'no':
2320          selected = 'selected'
2321        yield(dict(name='no', title=_('No favoured hostel'), selected=selected))
2322        for val in grok.getSite()['hostels'].values():
2323            selected = ''
2324            if val.hostel_id == self.context.desired_hostel:
2325                selected = 'selected'
2326            yield(dict(name=val.hostel_id, title=val.hostel_name,
2327                       selected=selected))
2328
2329    @action(_('Save'), style='primary')
2330    def save(self):
2331        hostel = self.request.form.get('hostel', None)
2332        self.context.desired_hostel = hostel
2333        self.flash(_('Your selection has been saved.'))
2334        return
2335
2336    @action(_('Book accommodation'), style='primary')
2337    def bookAccommodation(self, **data):
2338        self.redirect(self.url(self.context, 'add'))
2339        return
2340
2341    @jsaction(_('Remove selected'))
2342    def delBedTickets(self, **data):
2343        if getattr(self.request.principal, 'user_type', None) == 'student':
2344            self.flash(_('You are not allowed to remove bed tickets.'),
2345                       type="warning")
2346            self.redirect(self.url(self.context))
2347            return
2348        form = self.request.form
2349        if 'val_id' in form:
2350            child_id = form['val_id']
2351        else:
2352            self.flash(_('No bed ticket selected.'), type="warning")
2353            self.redirect(self.url(self.context))
2354            return
2355        if not isinstance(child_id, list):
2356            child_id = [child_id]
2357        deleted = []
2358        for id in child_id:
2359            del self.context[id]
2360            deleted.append(id)
2361        if len(deleted):
2362            self.flash(_('Successfully removed: ${a}',
2363                mapping = {'a':', '.join(deleted)}))
2364            self.context.writeLogMessage(
2365                self,'removed: % s' % ', '.join(deleted))
2366        self.redirect(self.url(self.context))
2367        return
2368
2369class BedTicketAddPage(KofaPage):
2370    """ Page to add a bed ticket
2371    """
2372    grok.context(IStudentAccommodation)
2373    grok.name('add')
2374    grok.require('waeup.handleAccommodation')
2375    #grok.template('enterpin')
2376    ac_prefix = 'HOS'
2377    label = _('Add bed ticket')
2378    pnav = 4
2379    buttonname = _('Create bed ticket')
2380    notice = ''
2381    with_ac = True
2382    with_bedselection = True
2383
2384    @property
2385    def getAvailableBeds(self):
2386        """Get a list of all available beds.
2387        """
2388        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2389        entries = cat.searchResults(
2390            bed_type=(self.acc_details['bt'],self.acc_details['bt']))
2391        available_beds = [
2392            entry for entry in entries if entry.owner == NOT_OCCUPIED]
2393        desired_hostel = self.context.desired_hostel
2394        # Filter desired hostel beds
2395        if desired_hostel and desired_hostel != 'no':
2396            filtered_beds = [bed for bed in available_beds
2397                             if bed.bed_id.startswith(desired_hostel)]
2398            available_beds = filtered_beds
2399        # Add legible bed coordinates
2400        for bed in available_beds:
2401            hall_title = bed.__parent__.hostel_name
2402            coordinates = bed.coordinates[1:]
2403            block, room_nr, bed_nr = coordinates
2404            bed.temp_bed_coordinates = _(
2405                '${a}, Block ${b}, Room ${c}, Bed ${d}', mapping = {
2406                'a':hall_title, 'b':block,
2407                'c':room_nr, 'd':bed_nr})
2408        return available_beds
2409
2410    def update(self, SUBMIT=None):
2411        student = self.context.student
2412        students_utils = getUtility(IStudentsUtils)
2413        self.acc_details  = students_utils.getAccommodationDetails(student)
2414        error_message = students_utils.checkAccommodationRequirements(
2415            student, self.acc_details)
2416        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2417        entries = cat.searchResults(
2418            owner=(student.student_id,student.student_id))
2419        self.show_available_beds = False
2420        if error_message:
2421            self.flash(error_message, type="warning")
2422            self.redirect(self.url(self.context))
2423            return
2424        if self.with_ac:
2425            self.ac_series = self.request.form.get('ac_series', None)
2426            self.ac_number = self.request.form.get('ac_number', None)
2427        available_beds = self.getAvailableBeds
2428        if SUBMIT is None:
2429            if self.with_bedselection and available_beds and not len(entries):
2430                self.show_available_beds = True
2431            return
2432        if self.with_ac:
2433            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2434            code = get_access_code(pin)
2435            if not code:
2436                self.flash(_('Activation code is invalid.'), type="warning")
2437                return
2438        # Search and book bed
2439        if len(entries):
2440            # If bed space has been manually allocated use this bed ...
2441            manual = True
2442            bed = list(entries)[0]
2443        else:
2444            # ... else search for available beds
2445            manual = False
2446            selected_bed = self.request.form.get('bed', None)
2447            if selected_bed:
2448                # Use selected bed
2449                beds = cat.searchResults(
2450                    bed_id=(selected_bed,selected_bed))
2451                bed = list(beds)[0]
2452                bed.bookBed(student.student_id)
2453            elif available_beds:
2454                # Select bed according to selectBed method
2455                students_utils = getUtility(IStudentsUtils)
2456                bed = students_utils.selectBed(available_beds)
2457                bed.bookBed(student.student_id)
2458            else:
2459                self.flash(_('There is no free bed in your category ${a}.',
2460                    mapping = {'a':self.acc_details['bt']}), type="warning")
2461                self.redirect(self.url(self.context))
2462                return
2463        if self.with_ac:
2464            # Mark pin as used (this also fires a pin related transition)
2465            if code.state == USED:
2466                self.flash(_('Activation code has already been used.'),
2467                           type="warning")
2468                if not manual:
2469                    # Release the previously booked bed
2470                    bed.owner = NOT_OCCUPIED
2471                    # Catalog must be informed
2472                    notify(grok.ObjectModifiedEvent(bed))
2473                return
2474            else:
2475                comment = _(u'invalidated')
2476                # Here we know that the ac is in state initialized so we do not
2477                # expect an exception, but the owner might be different
2478                success = invalidate_accesscode(
2479                    pin, comment, self.context.student.student_id)
2480                if not success:
2481                    self.flash(_('You are not the owner of this access code.'),
2482                               type="warning")
2483                    if not manual:
2484                        # Release the previously booked bed
2485                        bed.owner = NOT_OCCUPIED
2486                        # Catalog must be informed
2487                        notify(grok.ObjectModifiedEvent(bed))
2488                    return
2489        # Create bed ticket
2490        bedticket = createObject(u'waeup.BedTicket')
2491        if self.with_ac:
2492            bedticket.booking_code = pin
2493        bedticket.booking_session = self.acc_details['booking_session']
2494        bedticket.bed_type = self.acc_details['bt']
2495        bedticket.bed = bed
2496        hall_title = bed.__parent__.hostel_name
2497        coordinates = bed.coordinates[1:]
2498        block, room_nr, bed_nr = coordinates
2499        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2500            'a':hall_title, 'b':block,
2501            'c':room_nr, 'd':bed_nr,
2502            'e':bed.bed_type})
2503        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2504        bedticket.bed_coordinates = translate(
2505            bc, 'waeup.kofa',target_language=portal_language)
2506        self.context.addBedTicket(bedticket)
2507        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2508        self.flash(_('Bed ticket created and bed booked: ${a}',
2509            mapping = {'a':bedticket.display_coordinates}))
2510        self.redirect(self.url(self.context))
2511        return
2512
2513class BedTicketDisplayFormPage(KofaDisplayFormPage):
2514    """ Page to display bed tickets
2515    """
2516    grok.context(IBedTicket)
2517    grok.name('index')
2518    grok.require('waeup.viewStudent')
2519    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2520    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2521    pnav = 4
2522
2523    @property
2524    def label(self):
2525        return _('Bed Ticket for Session ${a}',
2526            mapping = {'a':self.context.getSessionString()})
2527
2528class ExportPDFBedTicketSlip(UtilityView, grok.View):
2529    """Deliver a PDF slip of the context.
2530    """
2531    grok.context(IBedTicket)
2532    grok.name('bed_allocation_slip.pdf')
2533    grok.require('waeup.viewStudent')
2534    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2535    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2536    prefix = 'form'
2537    omit_fields = (
2538        'password', 'suspended', 'phone', 'adm_code',
2539        'suspended_comment', 'date_of_birth', 'current_level',
2540        'flash_notice')
2541
2542    @property
2543    def title(self):
2544        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2545        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2546            target_language=portal_language)
2547
2548    @property
2549    def label(self):
2550        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2551        #return translate(_('Bed Allocation: '),
2552        #    'waeup.kofa', target_language=portal_language) \
2553        #    + ' %s' % self.context.bed_coordinates
2554        return translate(_('Bed Allocation Slip'),
2555            'waeup.kofa', target_language=portal_language) \
2556            + ' %s' % self.context.getSessionString()
2557
2558    def render(self):
2559        studentview = StudentBasePDFFormPage(self.context.student,
2560            self.request, self.omit_fields)
2561        students_utils = getUtility(IStudentsUtils)
2562        note = None
2563        n = grok.getSite()['hostels'].allocation_expiration
2564        if n:
2565            note = _("""
2566<br /><br /><br /><br /><br /><font size="12">
2567Please endeavour to pay your hostel maintenance charge within ${a} days
2568 of being allocated a space or else you are deemed to have
2569 voluntarily forfeited it and it goes back into circulation to be
2570 available for booking afresh!</font>)
2571""")
2572            note = _(note, mapping={'a': n})
2573            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2574            note = translate(
2575                note, 'waeup.kofa', target_language=portal_language)
2576        return students_utils.renderPDF(
2577            self, 'bed_allocation_slip.pdf',
2578            self.context.student, studentview,
2579            omit_fields=self.omit_fields,
2580            note=note)
2581
2582class BedTicketRelocationView(UtilityView, grok.View):
2583    """ Callback view
2584    """
2585    grok.context(IBedTicket)
2586    grok.name('relocate')
2587    grok.require('waeup.manageHostels')
2588
2589    # Relocate student if student parameters have changed or the bed_type
2590    # of the bed has changed
2591    def update(self):
2592        success, msg = self.context.relocateStudent()
2593        if not success:
2594            self.flash(msg, type="warning")
2595        else:
2596            self.flash(msg)
2597        self.redirect(self.url(self.context))
2598        return
2599
2600    def render(self):
2601        return
2602
2603class StudentHistoryPage(KofaPage):
2604    """ Page to display student history
2605    """
2606    grok.context(IStudent)
2607    grok.name('history')
2608    grok.require('waeup.viewStudent')
2609    grok.template('studenthistory')
2610    pnav = 4
2611
2612    @property
2613    def label(self):
2614        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2615
2616# Pages for students only
2617
2618class StudentBaseEditFormPage(KofaEditFormPage):
2619    """ View to edit student base data
2620    """
2621    grok.context(IStudent)
2622    grok.name('edit_base')
2623    grok.require('waeup.handleStudent')
2624    form_fields = grok.AutoFields(IStudentBase).select(
2625        'email', 'phone', 'parents_email')
2626    label = _('Edit base data')
2627    pnav = 4
2628
2629    @action(_('Save'), style='primary')
2630    def save(self, **data):
2631        msave(self, **data)
2632        return
2633
2634class StudentChangePasswordPage(KofaEditFormPage):
2635    """ View to edit student passwords
2636    """
2637    grok.context(IStudent)
2638    grok.name('change_password')
2639    grok.require('waeup.handleStudent')
2640    grok.template('change_password')
2641    label = _('Change password')
2642    pnav = 4
2643
2644    @action(_('Save'), style='primary')
2645    def save(self, **data):
2646        form = self.request.form
2647        password = form.get('change_password', None)
2648        password_ctl = form.get('change_password_repeat', None)
2649        if password:
2650            validator = getUtility(IPasswordValidator)
2651            errors = validator.validate_password(password, password_ctl)
2652            if not errors:
2653                IUserAccount(self.context).setPassword(password)
2654                # Unset temporary password
2655                self.context.temp_password = None
2656                self.context.writeLogMessage(self, 'saved: password')
2657                self.flash(_('Password changed.'))
2658            else:
2659                self.flash( ' '.join(errors), type="warning")
2660        return
2661
2662class StudentFilesUploadPage(KofaPage):
2663    """ View to upload files by student
2664    """
2665    grok.context(IStudent)
2666    grok.name('change_portrait')
2667    grok.require('waeup.uploadStudentFile')
2668    grok.template('filesuploadpage')
2669    label = _('Upload portrait')
2670    pnav = 4
2671
2672    def update(self):
2673        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2674        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2675            emit_lock_message(self)
2676            return
2677        super(StudentFilesUploadPage, self).update()
2678        return
2679
2680class StartClearancePage(KofaPage):
2681    grok.context(IStudent)
2682    grok.name('start_clearance')
2683    grok.require('waeup.handleStudent')
2684    grok.template('enterpin')
2685    label = _('Start clearance')
2686    ac_prefix = 'CLR'
2687    notice = ''
2688    pnav = 4
2689    buttonname = _('Start clearance now')
2690    with_ac = True
2691
2692    @property
2693    def all_required_fields_filled(self):
2694        if not self.context.email:
2695            return _("Email address is missing."), 'edit_base'
2696        if not self.context.phone:
2697            return _("Phone number is missing."), 'edit_base'
2698        return
2699
2700    @property
2701    def portrait_uploaded(self):
2702        store = getUtility(IExtFileStore)
2703        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2704            return True
2705        return False
2706
2707    def update(self, SUBMIT=None):
2708        if not self.context.state == ADMITTED:
2709            self.flash(_("Wrong state"), type="warning")
2710            self.redirect(self.url(self.context))
2711            return
2712        if not self.portrait_uploaded:
2713            self.flash(_("No portrait uploaded."), type="warning")
2714            self.redirect(self.url(self.context, 'change_portrait'))
2715            return
2716        if self.all_required_fields_filled:
2717            arf_warning = self.all_required_fields_filled[0]
2718            arf_redirect = self.all_required_fields_filled[1]
2719            self.flash(arf_warning, type="warning")
2720            self.redirect(self.url(self.context, arf_redirect))
2721            return
2722        if self.with_ac:
2723            self.ac_series = self.request.form.get('ac_series', None)
2724            self.ac_number = self.request.form.get('ac_number', None)
2725        if SUBMIT is None:
2726            return
2727        if self.with_ac:
2728            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2729            code = get_access_code(pin)
2730            if not code:
2731                self.flash(_('Activation code is invalid.'), type="warning")
2732                return
2733            if code.state == USED:
2734                self.flash(_('Activation code has already been used.'),
2735                           type="warning")
2736                return
2737            # Mark pin as used (this also fires a pin related transition)
2738            # and fire transition start_clearance
2739            comment = _(u"invalidated")
2740            # Here we know that the ac is in state initialized so we do not
2741            # expect an exception, but the owner might be different
2742            if not invalidate_accesscode(pin, comment, self.context.student_id):
2743                self.flash(_('You are not the owner of this access code.'),
2744                           type="warning")
2745                return
2746            self.context.clr_code = pin
2747        IWorkflowInfo(self.context).fireTransition('start_clearance')
2748        self.flash(_('Clearance process has been started.'))
2749        self.redirect(self.url(self.context,'cedit'))
2750        return
2751
2752class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2753    """ View to edit student clearance data by student
2754    """
2755    grok.context(IStudent)
2756    grok.name('cedit')
2757    grok.require('waeup.handleStudent')
2758    label = _('Edit clearance data')
2759
2760    @property
2761    def form_fields(self):
2762        if self.context.is_postgrad:
2763            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2764                'clr_code', 'officer_comment')
2765        else:
2766            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2767                'clr_code', 'officer_comment')
2768        return form_fields
2769
2770    def update(self):
2771        if self.context.clearance_locked:
2772            emit_lock_message(self)
2773            return
2774        return super(StudentClearanceEditFormPage, self).update()
2775
2776    @action(_('Save'), style='primary')
2777    def save(self, **data):
2778        self.applyData(self.context, **data)
2779        self.flash(_('Clearance form has been saved.'))
2780        return
2781
2782    def dataNotComplete(self):
2783        """To be implemented in the customization package.
2784        """
2785        return False
2786
2787    @action(_('Save and request clearance'), style='primary',
2788            warning=_('You can not edit your data after '
2789            'requesting clearance. You really want to request clearance now?'))
2790    def requestClearance(self, **data):
2791        self.applyData(self.context, **data)
2792        if self.dataNotComplete():
2793            self.flash(self.dataNotComplete(), type="warning")
2794            return
2795        self.flash(_('Clearance form has been saved.'))
2796        if self.context.clr_code:
2797            self.redirect(self.url(self.context, 'request_clearance'))
2798        else:
2799            # We bypass the request_clearance page if student
2800            # has been imported in state 'clearance started' and
2801            # no clr_code was entered before.
2802            state = IWorkflowState(self.context).getState()
2803            if state != CLEARANCE:
2804                # This shouldn't happen, but the application officer
2805                # might have forgotten to lock the form after changing the state
2806                self.flash(_('This form cannot be submitted. Wrong state!'),
2807                           type="danger")
2808                return
2809            IWorkflowInfo(self.context).fireTransition('request_clearance')
2810            self.flash(_('Clearance has been requested.'))
2811            self.redirect(self.url(self.context))
2812        return
2813
2814class RequestClearancePage(KofaPage):
2815    grok.context(IStudent)
2816    grok.name('request_clearance')
2817    grok.require('waeup.handleStudent')
2818    grok.template('enterpin')
2819    label = _('Request clearance')
2820    notice = _('Enter the CLR access code used for starting clearance.')
2821    ac_prefix = 'CLR'
2822    pnav = 4
2823    buttonname = _('Request clearance now')
2824    with_ac = True
2825
2826    def update(self, SUBMIT=None):
2827        if self.with_ac:
2828            self.ac_series = self.request.form.get('ac_series', None)
2829            self.ac_number = self.request.form.get('ac_number', None)
2830        if SUBMIT is None:
2831            return
2832        if self.with_ac:
2833            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2834            if self.context.clr_code and self.context.clr_code != pin:
2835                self.flash(_("This isn't your CLR access code."), type="danger")
2836                return
2837        state = IWorkflowState(self.context).getState()
2838        if state != CLEARANCE:
2839            # This shouldn't happen, but the application officer
2840            # might have forgotten to lock the form after changing the state
2841            self.flash(_('This form cannot be submitted. Wrong state!'),
2842                       type="danger")
2843            return
2844        IWorkflowInfo(self.context).fireTransition('request_clearance')
2845        self.flash(_('Clearance has been requested.'))
2846        self.redirect(self.url(self.context))
2847        return
2848
2849class StartSessionPage(KofaPage):
2850    grok.context(IStudentStudyCourse)
2851    grok.name('start_session')
2852    grok.require('waeup.handleStudent')
2853    grok.template('enterpin')
2854    label = _('Start session')
2855    ac_prefix = 'SFE'
2856    notice = ''
2857    pnav = 4
2858    buttonname = _('Start now')
2859    with_ac = True
2860
2861    def update(self, SUBMIT=None):
2862        if not self.context.is_current:
2863            emit_lock_message(self)
2864            return
2865        super(StartSessionPage, self).update()
2866        if not self.context.next_session_allowed:
2867            self.flash(_("You are not entitled to start session."),
2868                       type="warning")
2869            self.redirect(self.url(self.context))
2870            return
2871        if self.with_ac:
2872            self.ac_series = self.request.form.get('ac_series', None)
2873            self.ac_number = self.request.form.get('ac_number', None)
2874        if SUBMIT is None:
2875            return
2876        if self.with_ac:
2877            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2878            code = get_access_code(pin)
2879            if not code:
2880                self.flash(_('Activation code is invalid.'), type="warning")
2881                return
2882            # Mark pin as used (this also fires a pin related transition)
2883            if code.state == USED:
2884                self.flash(_('Activation code has already been used.'),
2885                           type="warning")
2886                return
2887            else:
2888                comment = _(u"invalidated")
2889                # Here we know that the ac is in state initialized so we do not
2890                # expect an error, but the owner might be different
2891                if not invalidate_accesscode(
2892                    pin,comment,self.context.student.student_id):
2893                    self.flash(_('You are not the owner of this access code.'),
2894                               type="warning")
2895                    return
2896        try:
2897            if self.context.student.state == CLEARED:
2898                IWorkflowInfo(self.context.student).fireTransition(
2899                    'pay_first_school_fee')
2900            elif self.context.student.state == RETURNING:
2901                IWorkflowInfo(self.context.student).fireTransition(
2902                    'pay_school_fee')
2903            elif self.context.student.state == PAID:
2904                IWorkflowInfo(self.context.student).fireTransition(
2905                    'pay_pg_fee')
2906        except ConstraintNotSatisfied:
2907            self.flash(_('An error occurred, please contact the system administrator.'),
2908                       type="danger")
2909            return
2910        self.flash(_('Session started.'))
2911        self.redirect(self.url(self.context))
2912        return
2913
2914class AddStudyLevelFormPage(KofaEditFormPage):
2915    """ Page for students to add current study levels
2916    """
2917    grok.context(IStudentStudyCourse)
2918    grok.name('add')
2919    grok.require('waeup.handleStudent')
2920    grok.template('studyleveladdpage')
2921    form_fields = grok.AutoFields(IStudentStudyCourse)
2922    pnav = 4
2923
2924    @property
2925    def label(self):
2926        studylevelsource = StudyLevelSource().factory
2927        code = self.context.current_level
2928        title = studylevelsource.getTitle(self.context, code)
2929        return _('Add current level ${a}', mapping = {'a':title})
2930
2931    def update(self):
2932        if not self.context.is_current \
2933            or self.context.student.studycourse_locked:
2934            emit_lock_message(self)
2935            return
2936        if self.context.student.state != PAID:
2937            emit_lock_message(self)
2938            return
2939        code = self.context.current_level
2940        if code is None:
2941            self.flash(_('Your data are incomplete'), type="danger")
2942            self.redirect(self.url(self.context))
2943            return
2944        super(AddStudyLevelFormPage, self).update()
2945        return
2946
2947    @action(_('Create course list now'), style='primary')
2948    def addStudyLevel(self, **data):
2949        studylevel = createObject(u'waeup.StudentStudyLevel')
2950        studylevel.level = self.context.current_level
2951        studylevel.level_session = self.context.current_session
2952        try:
2953            self.context.addStudentStudyLevel(
2954                self.context.certificate,studylevel)
2955        except KeyError:
2956            self.flash(_('This level exists.'), type="warning")
2957            self.redirect(self.url(self.context))
2958            return
2959        except RequiredMissing:
2960            self.flash(_('Your data are incomplete.'), type="danger")
2961            self.redirect(self.url(self.context))
2962            return
2963        self.flash(_('You successfully created a new course list.'))
2964        self.redirect(self.url(self.context, str(studylevel.level)))
2965        return
2966
2967class StudyLevelEditFormPage(KofaEditFormPage):
2968    """ Page to edit the student study level data by students
2969    """
2970    grok.context(IStudentStudyLevel)
2971    grok.name('edit')
2972    grok.require('waeup.editStudyLevel')
2973    grok.template('studyleveleditpage')
2974    pnav = 4
2975    placeholder = _('Enter valid course code')
2976
2977    def update(self, ADD=None, course=None):
2978        if not self.context.__parent__.is_current:
2979            emit_lock_message(self)
2980            return
2981        if self.context.student.state != PAID or \
2982            not self.context.is_current_level:
2983            emit_lock_message(self)
2984            return
2985        super(StudyLevelEditFormPage, self).update()
2986        if ADD is not None:
2987            if not course:
2988                self.flash(_('No valid course code entered.'), type="warning")
2989                return
2990            cat = queryUtility(ICatalog, name='courses_catalog')
2991            result = cat.searchResults(code=(course, course))
2992            if len(result) != 1:
2993                self.flash(_('Course not found.'), type="warning")
2994                return
2995            course = list(result)[0]
2996            if course.former_course:
2997                self.flash(_('Former courses can\'t be added.'), type="warning")
2998                return
2999            addCourseTicket(self, course)
3000        return
3001
3002    @property
3003    def label(self):
3004        # Here we know that the cookie has been set
3005        lang = self.request.cookies.get('kofa.language')
3006        level_title = translate(self.context.level_title, 'waeup.kofa',
3007            target_language=lang)
3008        return _('Edit course list of ${a}',
3009            mapping = {'a':level_title})
3010
3011    @property
3012    def translated_values(self):
3013        return translated_values(self)
3014
3015    def _delCourseTicket(self, **data):
3016        form = self.request.form
3017        if 'val_id' in form:
3018            child_id = form['val_id']
3019        else:
3020            self.flash(_('No ticket selected.'), type="warning")
3021            self.redirect(self.url(self.context, '@@edit'))
3022            return
3023        if not isinstance(child_id, list):
3024            child_id = [child_id]
3025        deleted = []
3026        for id in child_id:
3027            # Students are not allowed to remove core tickets
3028            if id in self.context and \
3029                self.context[id].removable_by_student:
3030                del self.context[id]
3031                deleted.append(id)
3032        if len(deleted):
3033            self.flash(_('Successfully removed: ${a}',
3034                mapping = {'a':', '.join(deleted)}))
3035            self.context.writeLogMessage(
3036                self,'removed: %s at %s' %
3037                (', '.join(deleted), self.context.level))
3038        self.redirect(self.url(self.context, u'@@edit'))
3039        return
3040
3041    @jsaction(_('Remove selected tickets'))
3042    def delCourseTicket(self, **data):
3043        self._delCourseTicket(**data)
3044        return
3045
3046    def _updateTickets(self, **data):
3047        cat = queryUtility(ICatalog, name='courses_catalog')
3048        invalidated = list()
3049        for value in self.context.values():
3050            result = cat.searchResults(code=(value.code, value.code))
3051            if len(result) != 1:
3052                course = None
3053            else:
3054                course = list(result)[0]
3055            invalid = self.context.updateCourseTicket(value, course)
3056            if invalid:
3057                invalidated.append(invalid)
3058        if invalidated:
3059            invalidated_string = ', '.join(invalidated)
3060            self.context.writeLogMessage(
3061                self, 'course tickets invalidated: %s' % invalidated_string)
3062        self.flash(_('All course tickets updated.'))
3063        return
3064
3065    @action(_('Update all tickets'),
3066        tooltip=_('Update all course parameters including course titles.'))
3067    def updateTickets(self, **data):
3068        self._updateTickets(**data)
3069        return
3070
3071    def _registerCourses(self, **data):
3072        if self.context.student.is_postgrad and \
3073            not self.context.student.is_special_postgrad:
3074            self.flash(_(
3075                "You are a postgraduate student, "
3076                "your course list can't bee registered."), type="warning")
3077            self.redirect(self.url(self.context))
3078            return
3079        students_utils = getUtility(IStudentsUtils)
3080        warning = students_utils.warnCreditsOOR(self.context)
3081        if warning:
3082            self.flash(warning, type="warning")
3083            return
3084        msg = self.context.course_registration_forbidden
3085        if msg:
3086            self.flash(msg, type="warning")
3087            return
3088        IWorkflowInfo(self.context.student).fireTransition(
3089            'register_courses')
3090        self.flash(_('Course list has been registered.'))
3091        self.redirect(self.url(self.context))
3092        return
3093
3094    @action(_('Register course list'), style='primary',
3095        warning=_('You can not edit your course list after registration.'
3096            ' You really want to register?'))
3097    def registerCourses(self, **data):
3098        self._registerCourses(**data)
3099        return
3100
3101class CourseTicketAddFormPage2(CourseTicketAddFormPage):
3102    """Add a course ticket by student.
3103    """
3104    grok.name('ctadd')
3105    grok.require('waeup.handleStudent')
3106    form_fields = grok.AutoFields(ICourseTicketAdd)
3107
3108    def update(self):
3109        if self.context.student.state != PAID or \
3110            not self.context.is_current_level:
3111            emit_lock_message(self)
3112            return
3113        super(CourseTicketAddFormPage2, self).update()
3114        return
3115
3116    @action(_('Add course ticket'))
3117    def addCourseTicket(self, **data):
3118        # Safety belt
3119        if self.context.student.state != PAID:
3120            return
3121        course = data['course']
3122        if course.former_course:
3123            self.flash(_('Former courses can\'t be added.'), type="warning")
3124            return
3125        success = addCourseTicket(self, course)
3126        if success:
3127            self.redirect(self.url(self.context, u'@@edit'))
3128        return
3129
3130class SetPasswordPage(KofaPage):
3131    grok.context(IKofaObject)
3132    grok.name('setpassword')
3133    grok.require('waeup.Anonymous')
3134    grok.template('setpassword')
3135    label = _('Set password for first-time login')
3136    ac_prefix = 'PWD'
3137    pnav = 0
3138    set_button = _('Set')
3139
3140    def update(self, SUBMIT=None):
3141        self.reg_number = self.request.form.get('reg_number', None)
3142        self.ac_series = self.request.form.get('ac_series', None)
3143        self.ac_number = self.request.form.get('ac_number', None)
3144
3145        if SUBMIT is None:
3146            return
3147        hitlist = search(query=self.reg_number,
3148            searchtype='reg_number', view=self)
3149        if not hitlist:
3150            self.flash(_('No student found.'), type="warning")
3151            return
3152        if len(hitlist) != 1:   # Cannot happen but anyway
3153            self.flash(_('More than one student found.'), type="warning")
3154            return
3155        student = hitlist[0].context
3156        self.student_id = student.student_id
3157        student_pw = student.password
3158        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
3159        code = get_access_code(pin)
3160        if not code:
3161            self.flash(_('Access code is invalid.'), type="warning")
3162            return
3163        if student_pw and pin == student.adm_code:
3164            self.flash(_(
3165                'Password has already been set. Your Student Id is ${a}',
3166                mapping = {'a':self.student_id}))
3167            return
3168        elif student_pw:
3169            self.flash(
3170                _('Password has already been set. You are using the ' +
3171                'wrong Access Code.'), type="warning")
3172            return
3173        # Mark pin as used (this also fires a pin related transition)
3174        # and set student password
3175        if code.state == USED:
3176            self.flash(_('Access code has already been used.'), type="warning")
3177            return
3178        else:
3179            comment = _(u"invalidated")
3180            # Here we know that the ac is in state initialized so we do not
3181            # expect an exception
3182            invalidate_accesscode(pin,comment)
3183            IUserAccount(student).setPassword(self.ac_number)
3184            student.adm_code = pin
3185        self.flash(_('Password has been set. Your Student Id is ${a}',
3186            mapping = {'a':self.student_id}))
3187        return
3188
3189class StudentRequestPasswordPage(KofaAddFormPage):
3190    """Captcha'd request password page for students.
3191    """
3192    grok.name('requestpw')
3193    grok.require('waeup.Anonymous')
3194    grok.template('requestpw')
3195    form_fields = grok.AutoFields(IStudentRequestPW).select(
3196        'lastname','number','email')
3197    label = _('Request password for first-time login')
3198
3199    def update(self):
3200        blocker = grok.getSite()['configuration'].maintmode_enabled_by
3201        if blocker:
3202            self.flash(_('The portal is in maintenance mode. '
3203                        'Password request forms are temporarily disabled.'),
3204                       type='warning')
3205            self.redirect(self.url(self.context))
3206            return
3207        # Handle captcha
3208        self.captcha = getUtility(ICaptchaManager).getCaptcha()
3209        self.captcha_result = self.captcha.verify(self.request)
3210        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
3211        return
3212
3213    def _redirect(self, email, password, student_id):
3214        # Forward only email to landing page in base package.
3215        self.redirect(self.url(self.context, 'requestpw_complete',
3216            data = dict(email=email)))
3217        return
3218
3219    def _redirect_no_student(self):
3220        # No record found, this is the truth. We do not redirect here.
3221        # We are using this method in custom packages
3222        # for redirecting alumni to the application section.
3223        self.flash(_('No student record found.'), type="warning")
3224        return
3225
3226    def _pw_used(self):
3227        # XXX: False if password has not been used. We need an extra
3228        #      attribute which remembers if student logged in.
3229        return True
3230
3231    @action(_('Send login credentials to email address'), style='primary')
3232    def get_credentials(self, **data):
3233        if not self.captcha_result.is_valid:
3234            # Captcha will display error messages automatically.
3235            # No need to flash something.
3236            return
3237        number = data.get('number','')
3238        lastname = data.get('lastname','')
3239        cat = getUtility(ICatalog, name='students_catalog')
3240        results = list(
3241            cat.searchResults(reg_number=(number, number)))
3242        if not results:
3243            results = list(
3244                cat.searchResults(matric_number=(number, number)))
3245        if results:
3246            student = results[0]
3247            if getattr(student,'lastname',None) is None:
3248                self.flash(_('An error occurred.'), type="danger")
3249                return
3250            elif student.lastname.lower() != lastname.lower():
3251                # Don't tell the truth here. Anonymous must not
3252                # know that a record was found and only the lastname
3253                # verification failed.
3254                self.flash(_('No student record found.'), type="warning")
3255                return
3256            elif student.password is not None and self._pw_used:
3257                self.flash(_('Your password has already been set and used. '
3258                             'Please proceed to the login page.'),
3259                           type="warning")
3260                return
3261            # Store email address but nothing else.
3262            student.email = data['email']
3263            notify(grok.ObjectModifiedEvent(student))
3264        else:
3265            self._redirect_no_student()
3266            return
3267
3268        kofa_utils = getUtility(IKofaUtils)
3269        password = kofa_utils.genPassword()
3270        mandate = PasswordMandate()
3271        mandate.params['password'] = password
3272        mandate.params['user'] = student
3273        site = grok.getSite()
3274        site['mandates'].addMandate(mandate)
3275        # Send email with credentials
3276        args = {'mandate_id':mandate.mandate_id}
3277        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3278        url_info = u'Confirmation link: %s' % mandate_url
3279        msg = _('You have successfully requested a password for the')
3280        if kofa_utils.sendCredentials(IUserAccount(student),
3281            password, url_info, msg):
3282            email_sent = student.email
3283        else:
3284            email_sent = None
3285        self._redirect(email=email_sent, password=password,
3286            student_id=student.student_id)
3287        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3288        self.context.logger.info(
3289            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3290        return
3291
3292class ParentsUser:
3293    pass
3294
3295class RequestParentsPasswordPage(StudentRequestPasswordPage):
3296    """Captcha'd request password page for parents.
3297    """
3298    grok.name('requestppw')
3299    grok.template('requestppw')
3300    label = _('Request password for parents access')
3301
3302    def update(self):
3303        super(RequestParentsPasswordPage, self).update()
3304        kofa_utils = getUtility(IKofaUtils)
3305        self.temp_password_minutes = kofa_utils.TEMP_PASSWORD_MINUTES
3306        return
3307
3308    @action(_('Send temporary login credentials to email address'), style='primary')
3309    def get_credentials(self, **data):
3310        if not self.captcha_result.is_valid:
3311            # Captcha will display error messages automatically.
3312            # No need to flash something.
3313            return
3314        number = data.get('number','')
3315        lastname = data.get('lastname','')
3316        email = data['email']
3317        cat = getUtility(ICatalog, name='students_catalog')
3318        results = list(
3319            cat.searchResults(reg_number=(number, number)))
3320        if not results:
3321            results = list(
3322                cat.searchResults(matric_number=(number, number)))
3323        if results:
3324            student = results[0]
3325            if getattr(student,'lastname',None) is None:
3326                self.flash(_('An error occurred.'), type="danger")
3327                return
3328            elif student.lastname.lower() != lastname.lower():
3329                # Don't tell the truth here. Anonymous must not
3330                # know that a record was found and only the lastname
3331                # verification failed.
3332                self.flash(_('No student record found.'), type="warning")
3333                return
3334            elif email != student.parents_email:
3335                self.flash(_('Wrong email address.'), type="warning")
3336                return
3337        else:
3338            self._redirect_no_student()
3339            return
3340        kofa_utils = getUtility(IKofaUtils)
3341        password = kofa_utils.genPassword()
3342        mandate = ParentsPasswordMandate()
3343        mandate.params['password'] = password
3344        mandate.params['student'] = student
3345        site = grok.getSite()
3346        site['mandates'].addMandate(mandate)
3347        # Send email with credentials
3348        args = {'mandate_id':mandate.mandate_id}
3349        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
3350        url_info = u'Confirmation link: %s' % mandate_url
3351        msg = _('You have successfully requested a parents password for the')
3352        # Create a fake user
3353        user = ParentsUser()
3354        user.name = student.student_id
3355        user.title = "Parents of %s" % student.display_fullname
3356        user.email = student.parents_email
3357        if kofa_utils.sendCredentials(user, password, url_info, msg):
3358            email_sent = user.email
3359        else:
3360            email_sent = None
3361        self._redirect(email=email_sent, password=password,
3362            student_id=student.student_id)
3363        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3364        self.context.logger.info(
3365            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
3366        return
3367
3368class StudentRequestPasswordEmailSent(KofaPage):
3369    """Landing page after successful password request.
3370
3371    """
3372    grok.name('requestpw_complete')
3373    grok.require('waeup.Public')
3374    grok.template('requestpwmailsent')
3375    label = _('Your password request was successful.')
3376
3377    def update(self, email=None, student_id=None, password=None):
3378        self.email = email
3379        self.password = password
3380        self.student_id = student_id
3381        return
3382
3383class FilterStudentsInDepartmentPage(KofaPage):
3384    """Page that filters and lists students.
3385    """
3386    grok.context(IDepartment)
3387    grok.require('waeup.showStudents')
3388    grok.name('students')
3389    grok.template('filterstudentspage')
3390    pnav = 1
3391    session_label = _('Current Session')
3392    level_label = _('Current Level')
3393
3394    def label(self):
3395        return 'Students in %s' % self.context.longtitle
3396
3397    def _set_session_values(self):
3398        vocab_terms = academic_sessions_vocab.by_value.values()
3399        self.sessions = sorted(
3400            [(x.title, x.token) for x in vocab_terms], reverse=True)
3401        self.sessions += [('All Sessions', 'all')]
3402        return
3403
3404    def _set_level_values(self):
3405        vocab_terms = course_levels.by_value.values()
3406        self.levels = sorted(
3407            [(x.title, x.token) for x in vocab_terms])
3408        self.levels += [('All Levels', 'all')]
3409        return
3410
3411    def _searchCatalog(self, session, level):
3412        if level not in (10, 999, None):
3413            start_level = 100 * (level // 100)
3414            end_level = start_level + 90
3415        else:
3416            start_level = end_level = level
3417        cat = queryUtility(ICatalog, name='students_catalog')
3418        students = cat.searchResults(
3419            current_session=(session, session),
3420            current_level=(start_level, end_level),
3421            depcode=(self.context.code, self.context.code)
3422            )
3423        hitlist = []
3424        for student in students:
3425            hitlist.append(StudentQueryResultItem(student, view=self))
3426        return hitlist
3427
3428    def update(self, SHOW=None, session=None, level=None):
3429        self.parent_url = self.url(self.context.__parent__)
3430        self._set_session_values()
3431        self._set_level_values()
3432        self.hitlist = []
3433        self.session_default = session
3434        self.level_default = level
3435        if SHOW is not None:
3436            if session != 'all':
3437                self.session = int(session)
3438                self.session_string = '%s %s/%s' % (
3439                    self.session_label, self.session, self.session+1)
3440            else:
3441                self.session = None
3442                self.session_string = _('in any session')
3443            if level != 'all':
3444                self.level = int(level)
3445                self.level_string = '%s %s' % (self.level_label, self.level)
3446            else:
3447                self.level = None
3448                self.level_string = _('at any level')
3449            self.hitlist = self._searchCatalog(self.session, self.level)
3450            if not self.hitlist:
3451                self.flash(_('No student found.'), type="warning")
3452        return
3453
3454class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3455    """Page that filters and lists students.
3456    """
3457    grok.context(ICertificate)
3458
3459    def label(self):
3460        return 'Students studying %s' % self.context.longtitle
3461
3462    def _searchCatalog(self, session, level):
3463        if level not in (10, 999, None):
3464            start_level = 100 * (level // 100)
3465            end_level = start_level + 90
3466        else:
3467            start_level = end_level = level
3468        cat = queryUtility(ICatalog, name='students_catalog')
3469        students = cat.searchResults(
3470            current_session=(session, session),
3471            current_level=(start_level, end_level),
3472            certcode=(self.context.code, self.context.code)
3473            )
3474        hitlist = []
3475        for student in students:
3476            hitlist.append(StudentQueryResultItem(student, view=self))
3477        return hitlist
3478
3479class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3480    """Page that filters and lists students.
3481    """
3482    grok.context(ICourse)
3483    grok.require('waeup.viewStudent')
3484
3485    session_label = _('Session')
3486    level_label = _('Level')
3487
3488    def label(self):
3489        return 'Students registered for %s' % self.context.longtitle
3490
3491    def _searchCatalog(self, session, level):
3492        if level not in (10, 999, None):
3493            start_level = 100 * (level // 100)
3494            end_level = start_level + 90
3495        else:
3496            start_level = end_level = level
3497        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3498        coursetickets = cat.searchResults(
3499            session=(session, session),
3500            level=(start_level, end_level),
3501            code=(self.context.code, self.context.code)
3502            )
3503        hitlist = []
3504        for ticket in coursetickets:
3505            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3506        return list(set(hitlist))
3507
3508class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3509    """ Clear all students of a department in state 'clearance requested'.
3510    """
3511    grok.context(IDepartment)
3512    grok.name('clearallstudents')
3513    grok.require('waeup.clearAllStudents')
3514
3515    def update(self):
3516        cat = queryUtility(ICatalog, name='students_catalog')
3517        students = cat.searchResults(
3518            depcode=(self.context.code, self.context.code),
3519            state=(REQUESTED, REQUESTED)
3520            )
3521        num = 0
3522        for student in students:
3523            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3524                continue
3525            IWorkflowInfo(student).fireTransition('clear')
3526            num += 1
3527        self.flash(_('%d students have been cleared.' % num))
3528        self.redirect(self.url(self.context))
3529        return
3530
3531    def render(self):
3532        return
3533
3534class EditScoresPage(KofaPage):
3535    """Page that allows to edit batches of scores.
3536    """
3537    grok.context(ICourse)
3538    grok.require('waeup.editScores')
3539    grok.name('edit_scores')
3540    grok.template('editscorespage')
3541    pnav = 1
3542    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3543
3544    def label(self):
3545        return '%s tickets in academic session %s' % (
3546            self.context.code, self.session_title)
3547
3548    def _searchCatalog(self, session):
3549        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3550        # Attention: Also tickets of previous studycourses are found
3551        coursetickets = cat.searchResults(
3552            session=(session, session),
3553            code=(self.context.code, self.context.code)
3554            )
3555        return list(coursetickets)
3556
3557    def _extract_uploadfile(self, uploadfile):
3558        """Get a mapping of student-ids to scores.
3559
3560        The mapping is constructed by reading contents from `uploadfile`.
3561
3562        We expect uploadfile to be a regular CSV file with columns
3563        ``student_id`` and ``score`` (other cols are ignored).
3564        """
3565        result = dict()
3566        data = StringIO(uploadfile.read())  # ensure we have something seekable
3567        reader = csv.DictReader(data)
3568        for row in reader:
3569            if not 'student_id' in row or not 'score' in row:
3570                continue
3571            result[row['student_id']] = row['score']
3572        return result
3573
3574    def _update_scores(self, form):
3575        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3576        error = ''
3577        if 'UPDATE_FILE' in form:
3578            if form['uploadfile']:
3579                try:
3580                    formvals = self._extract_uploadfile(form['uploadfile'])
3581                except:
3582                    self.flash(
3583                        _('Uploaded file contains illegal data. Ignored'),
3584                        type="danger")
3585                    return False
3586            else:
3587                self.flash(
3588                    _('No file provided.'), type="danger")
3589                return False
3590        else:
3591            formvals = dict(zip(form['sids'], form['scores']))
3592        for ticket in self.editable_tickets:
3593            score = ticket.score
3594            sid = ticket.student.student_id
3595            if sid not in formvals:
3596                continue
3597            if formvals[sid] == '':
3598                score = None
3599            else:
3600                try:
3601                    score = int(formvals[sid])
3602                except ValueError:
3603                    error += '%s, ' % ticket.student.display_fullname
3604            if ticket.score != score:
3605                ticket.score = score
3606                ticket.student.__parent__.logger.info(
3607                    '%s - %s %s/%s score updated (%s)' % (
3608                        ob_class, ticket.student.student_id,
3609                        ticket.level, ticket.code, score)
3610                    )
3611        if error:
3612            self.flash(
3613                _('Error: Score(s) of following students have not been '
3614                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3615                type="danger")
3616        return True
3617
3618    def _validate_results(self, form):
3619        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3620        user = get_current_principal()
3621        if user is None:
3622            usertitle = 'system'
3623        else:
3624            usertitle = getattr(user, 'public_name', None)
3625            if not usertitle:
3626                usertitle = user.title
3627        self.context.results_validated_by = usertitle
3628        self.context.results_validation_date = datetime.utcnow()
3629        self.context.results_validation_session = self.current_academic_session
3630        return
3631
3632    def _results_editable(self, results_validation_session,
3633                         current_academic_session):
3634        user = get_current_principal()
3635        prm = IPrincipalRoleManager(self.context)
3636        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3637        if 'waeup.local.LocalStudentsManager' in roles:
3638            return True
3639        if results_validation_session \
3640            and results_validation_session >= current_academic_session:
3641            return False
3642        return True
3643
3644    def update(self,  *args, **kw):
3645        form = self.request.form
3646        self.current_academic_session = grok.getSite()[
3647            'configuration'].current_academic_session
3648        if self.context.__parent__.__parent__.score_editing_disabled \
3649            or self.context.score_editing_disabled:
3650            self.flash(_('Score editing disabled.'), type="warning")
3651            self.redirect(self.url(self.context))
3652            return
3653        if not self.current_academic_session:
3654            self.flash(_('Current academic session not set.'), type="warning")
3655            self.redirect(self.url(self.context))
3656            return
3657        vs = self.context.results_validation_session
3658        if not self._results_editable(vs, self.current_academic_session):
3659            self.flash(
3660                _('Course results have already been '
3661                  'validated and can no longer be changed.'),
3662                type="danger")
3663            self.redirect(self.url(self.context))
3664            return
3665        self.session_title = academic_sessions_vocab.getTerm(
3666            self.current_academic_session).title
3667        self.tickets = self._searchCatalog(self.current_academic_session)
3668        if not self.tickets:
3669            self.flash(_('No student found.'), type="warning")
3670            self.redirect(self.url(self.context))
3671            return
3672        self.editable_tickets = [
3673            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3674        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form\
3675            and not 'VALIDATE_RESULTS' in form:
3676            return
3677        if 'VALIDATE_RESULTS' in form:
3678            if vs and vs >= self.current_academic_session:
3679                self.flash(
3680                    _('Course results have already been validated.'),
3681                    type="danger")
3682            for ticket in self.tickets:
3683                if ticket.total_score is not None:
3684                    break
3685                self.flash(
3686                    _('No score has been entered.'),
3687                    type="danger")
3688                return
3689            self._validate_results(form)
3690            self.flash(_('You successfully validated the course results.'))
3691            self.redirect(self.url(self.context))
3692            return
3693        if not self.editable_tickets:
3694            return
3695        success = self._update_scores(form)
3696        if success:
3697            self.flash(_('You successfully updated course results.'))
3698        return
3699
3700class DownloadScoresView(UtilityView, grok.View):
3701    """View that exports scores.
3702    """
3703    grok.context(ICourse)
3704    grok.require('waeup.editScores')
3705    grok.name('download_scores')
3706
3707    def _results_editable(self, results_validation_session,
3708                         current_academic_session):
3709        user = get_current_principal()
3710        prm = IPrincipalRoleManager(self.context)
3711        roles = [x[0] for x in prm.getRolesForPrincipal(user.id)]
3712        if 'waeup.local.LocalStudentsManager' in roles:
3713            return True
3714        if results_validation_session \
3715            and results_validation_session >= current_academic_session:
3716            return False
3717        return True
3718
3719    def update(self):
3720        self.current_academic_session = grok.getSite()[
3721            'configuration'].current_academic_session
3722        if self.context.__parent__.__parent__.score_editing_disabled \
3723            or self.context.score_editing_disabled:
3724            self.flash(_('Score editing disabled.'), type="warning")
3725            self.redirect(self.url(self.context))
3726            return
3727        if not self.current_academic_session:
3728            self.flash(_('Current academic session not set.'), type="warning")
3729            self.redirect(self.url(self.context))
3730            return
3731        vs = self.context.results_validation_session
3732        if not self._results_editable(vs, self.current_academic_session):
3733            self.flash(
3734                _('Course results have already been '
3735                  'validated and can no longer be changed.'),
3736                type="danger")
3737            self.redirect(self.url(self.context))
3738            return
3739        site = grok.getSite()
3740        exporter = getUtility(ICSVExporter, name='lecturer')
3741        self.csv = exporter.export_filtered(site, filepath=None,
3742                                 catalog='coursetickets',
3743                                 session=self.current_academic_session,
3744                                 level=None,
3745                                 code=self.context.code)
3746        return
3747
3748    def render(self):
3749        filename = 'results_%s_%s.csv' % (
3750            self.context.code, self.current_academic_session)
3751        self.response.setHeader(
3752            'Content-Type', 'text/csv; charset=UTF-8')
3753        self.response.setHeader(
3754            'Content-Disposition:', 'attachment; filename="%s' % filename)
3755        return self.csv
3756
3757class ExportPDFScoresSlip(UtilityView, grok.View,
3758    LocalRoleAssignmentUtilityView):
3759    """Deliver a PDF slip of course tickets for a lecturer.
3760    """
3761    grok.context(ICourse)
3762    grok.name('coursetickets.pdf')
3763    grok.require('waeup.showStudents')
3764
3765    def update(self):
3766        self.current_academic_session = grok.getSite()[
3767            'configuration'].current_academic_session
3768        if not self.current_academic_session:
3769            self.flash(_('Current academic session not set.'), type="danger")
3770            self.redirect(self.url(self.context))
3771            return
3772
3773    @property
3774    def note(self):
3775        return
3776
3777    def data(self, session):
3778        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3779        # Attention: Also tickets of previous studycourses are found
3780        coursetickets = cat.searchResults(
3781            session=(session, session),
3782            code=(self.context.code, self.context.code)
3783            )
3784        header = [[_('Matric No.'),
3785                   _('Reg. No.'),
3786                   _('Fullname'),
3787                   _('Status'),
3788                   _('Course of Studies'),
3789                   _('Level'),
3790                   _('Score') ],]
3791        tickets = []
3792        for ticket in list(coursetickets):
3793            row = [ticket.student.matric_number,
3794                  ticket.student.reg_number,
3795                  ticket.student.display_fullname,
3796                  ticket.student.translated_state,
3797                  ticket.student.certcode,
3798                  ticket.level,
3799                  ticket.score]
3800            tickets.append(row)
3801        return header + sorted(tickets, key=lambda value: value[0]), None
3802
3803    def render(self):
3804        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3805                     if i['local_role'] == 'waeup.local.Lecturer']
3806        lecturers = sorted(lecturers)
3807        lecturers =  ', '.join(lecturers)
3808        students_utils = getUtility(IStudentsUtils)
3809        return students_utils.renderPDFCourseticketsOverview(
3810            self, 'coursetickets', self.current_academic_session,
3811            self.data(self.current_academic_session), lecturers,
3812            'landscape', 90, self.note)
3813
3814class ExportAttendanceSlip(UtilityView, grok.View,
3815    LocalRoleAssignmentUtilityView):
3816    """Deliver a PDF slip of course tickets in attendance sheet format.
3817    """
3818    grok.context(ICourse)
3819    grok.name('attendance.pdf')
3820    grok.require('waeup.showStudents')
3821
3822    def update(self):
3823        self.current_academic_session = grok.getSite()[
3824            'configuration'].current_academic_session
3825        if not self.current_academic_session:
3826            self.flash(_('Current academic session not set.'), type="danger")
3827            self.redirect(self.url(self.context))
3828            return
3829
3830    @property
3831    def note(self):
3832        return
3833
3834    def data(self, session):
3835        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3836        # Attention: Also tickets of previous studycourses are found
3837        coursetickets = cat.searchResults(
3838            session=(session, session),
3839            code=(self.context.code, self.context.code)
3840            )
3841        header = [[_('S/N'),
3842                   _('Matric No.'),
3843                   _('Name'),
3844                   _('Level'),
3845                   _('Course of\nStudies'),
3846                   _('Booklet No.'),
3847                   _('Signature'),
3848                   ],]
3849        tickets = []
3850        sn = 1
3851        ctlist = sorted(list(coursetickets),
3852                        key=lambda value: str(value.student.certcode) +
3853                                          str(value.student.matric_number))
3854        # In AAUE only editable appear on the attendance sheet. Hopefully
3855        # this holds for other universities too.
3856        editable_tickets = [ticket for ticket in ctlist
3857            if ticket.editable_by_lecturer]
3858        for ticket in editable_tickets:
3859            name = textwrap.fill(ticket.student.display_fullname, 20)
3860            row = [sn,
3861                  ticket.student.matric_number,
3862                  name,
3863                  ticket.level,
3864                  ticket.student.certcode,
3865                  20 * ' ',
3866                  27 * ' ',
3867                  ]
3868            tickets.append(row)
3869            sn += 1
3870        return header + tickets, None
3871
3872    def render(self):
3873        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3874                     if i['local_role'] == 'waeup.local.Lecturer']
3875        lecturers =  ', '.join(lecturers)
3876        students_utils = getUtility(IStudentsUtils)
3877        return students_utils.renderPDFCourseticketsOverview(
3878            self, 'attendance', self.current_academic_session,
3879            self.data(self.current_academic_session),
3880            lecturers, '', 65, self.note)
3881
3882class ExportJobContainerOverview(KofaPage):
3883    """Page that lists active student data export jobs and provides links
3884    to discard or download CSV files.
3885
3886    """
3887    grok.context(VirtualExportJobContainer)
3888    grok.require('waeup.showStudents')
3889    grok.name('index.html')
3890    grok.template('exportjobsindex')
3891    label = _('Student Data Exports')
3892    pnav = 1
3893    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3894
3895    def update(self, CREATE1=None, CREATE2=None, DISCARD=None, job_id=None):
3896        if CREATE1:
3897            self.redirect(self.url('@@exportconfig'))
3898            return
3899        if CREATE2:
3900            self.redirect(self.url('@@exportselected'))
3901            return
3902        if DISCARD and job_id:
3903            entry = self.context.entry_from_job_id(job_id)
3904            self.context.delete_export_entry(entry)
3905            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3906            self.context.logger.info(
3907                '%s - discarded: job_id=%s' % (ob_class, job_id))
3908            self.flash(_('Discarded export') + ' %s' % job_id)
3909        self.entries = doll_up(self, user=self.request.principal.id)
3910        return
3911
3912class ExportJobContainerJobConfig(KofaPage):
3913    """Page that configures a students export job.
3914
3915    This is a baseclass.
3916    """
3917    grok.baseclass()
3918    grok.require('waeup.showStudents')
3919    grok.template('exportconfig')
3920    label = _('Configure student data export')
3921    pnav = 1
3922    redirect_target = ''
3923    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3924
3925    def _set_session_values(self):
3926        vocab_terms = academic_sessions_vocab.by_value.values()
3927        self.sessions = [(_('All Sessions'), 'all')]
3928        self.sessions += sorted(
3929            [(x.title, x.token) for x in vocab_terms], reverse=True)
3930        return
3931
3932    def _set_level_values(self):
3933        vocab_terms = course_levels.by_value.values()
3934        self.levels = [(_('All Levels'), 'all')]
3935        self.levels += sorted(
3936            [(x.title, x.token) for x in vocab_terms])
3937        return
3938
3939    def _set_semesters_values(self):
3940        utils = getUtility(IKofaUtils)
3941        self.semesters =[(_('All Semesters'), 'all')]
3942        self.semesters += sorted([(value, key) for key, value in
3943                      utils.SEMESTER_DICT.items()])
3944        return
3945
3946    def _set_mode_values(self):
3947        utils = getUtility(IKofaUtils)
3948        self.modes =[(_('All Modes'), 'all')]
3949        self.modes += sorted([(value, key) for key, value in
3950                      utils.STUDY_MODES_DICT.items()])
3951        return
3952
3953    def _set_paycat_values(self):
3954        utils = getUtility(IKofaUtils)
3955        self.paycats =[(_('All Payment Categories'), 'all')]
3956        self.paycats += sorted([(value, key) for key, value in
3957                      utils.PAYMENT_CATEGORIES.items()])
3958        return
3959
3960    def _set_exporter_values(self):
3961        # We provide all student exporters, nothing else, yet.
3962        # Bursary, Department or Accommodation Officers don't
3963        # have the general exportData
3964        # permission and are only allowed to export bursary, payments
3965        # overview or accommodation data respectively.
3966        # This is the only place where waeup.exportAccommodationData,
3967        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3968        # are used.
3969        exporters = []
3970        if not checkPermission('waeup.exportData', self.context):
3971            if checkPermission('waeup.exportBursaryData', self.context):
3972                exporters += [('Bursary Data', 'bursary')]
3973            if checkPermission('waeup.exportPaymentsOverview', self.context):
3974                exporters += [('School Fee Payments Overview',
3975                               'sfpaymentsoverview'),
3976                              ('Session Payments Overview',
3977                               'sessionpaymentsoverview')]
3978            if checkPermission('waeup.exportAccommodationData', self.context):
3979                exporters += [('Bed Tickets', 'bedtickets'),
3980                              ('Accommodation Payments',
3981                               'accommodationpayments')]
3982            self.exporters = exporters
3983            return
3984        STUDENT_EXPORTER_NAMES = getUtility(
3985            IStudentsUtils).STUDENT_EXPORTER_NAMES
3986        for name in STUDENT_EXPORTER_NAMES:
3987            util = getUtility(ICSVExporter, name=name)
3988            exporters.append((util.title, name),)
3989        self.exporters = exporters
3990        return
3991
3992    @property
3993    def faccode(self):
3994        return None
3995
3996    @property
3997    def depcode(self):
3998        return None
3999
4000    @property
4001    def certcode(self):
4002        return None
4003
4004    def update(self, START=None, session=None, level=None, mode=None,
4005               payments_start=None, payments_end=None, ct_level=None,
4006               ct_session=None, ct_semester=None, paycat=None,
4007               paysession=None, level_session=None, exporter=None):
4008        self._set_session_values()
4009        self._set_level_values()
4010        self._set_mode_values()
4011        self._set_paycat_values()
4012        self._set_exporter_values()
4013        self._set_semesters_values()
4014        if START is None:
4015            return
4016        ena = exports_not_allowed(self)
4017        if ena:
4018            self.flash(ena, type='danger')
4019            return
4020        if payments_start or payments_end:
4021            date_format = '%d/%m/%Y'
4022            try:
4023                datetime.strptime(payments_start, date_format)
4024                datetime.strptime(payments_end, date_format)
4025            except ValueError:
4026                self.flash(_('Payment dates do not match format d/m/Y.'),
4027                           type="danger")
4028                return
4029        if session == 'all':
4030            session=None
4031        if level == 'all':
4032            level = None
4033        if mode == 'all':
4034            mode = None
4035        if (mode,
4036            level,
4037            session,
4038            self.faccode,
4039            self.depcode,
4040            self.certcode) == (None, None, None, None, None, None):
4041            # Export all students including those without certificate
4042            job_id = self.context.start_export_job(exporter,
4043                                          self.request.principal.id,
4044                                          payments_start = payments_start,
4045                                          payments_end = payments_end,
4046                                          paycat=paycat,
4047                                          paysession=paysession,
4048                                          ct_level = ct_level,
4049                                          ct_session = ct_session,
4050                                          ct_semester = ct_semester,
4051                                          level_session=level_session,
4052                                          )
4053        else:
4054            job_id = self.context.start_export_job(exporter,
4055                                          self.request.principal.id,
4056                                          current_session=session,
4057                                          current_level=level,
4058                                          current_mode=mode,
4059                                          faccode=self.faccode,
4060                                          depcode=self.depcode,
4061                                          certcode=self.certcode,
4062                                          payments_start = payments_start,
4063                                          payments_end = payments_end,
4064                                          paycat=paycat,
4065                                          paysession=paysession,
4066                                          ct_level = ct_level,
4067                                          ct_session = ct_session,
4068                                          ct_semester = ct_semester,
4069                                          level_session=level_session,)
4070        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4071        self.context.logger.info(
4072            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
4073            % (ob_class, exporter, session, level, mode, self.faccode,
4074            self.depcode, self.certcode, payments_start, payments_end,
4075            ct_level, ct_session, paycat, paysession, level_session, job_id))
4076        self.flash(_('Export started for students with') +
4077                   ' current_session=%s, current_level=%s, study_mode=%s' % (
4078                   session, level, mode))
4079        self.redirect(self.url(self.redirect_target))
4080        return
4081
4082class ExportJobContainerDownload(ExportCSVView):
4083    """Page that downloads a students export csv file.
4084
4085    """
4086    grok.context(VirtualExportJobContainer)
4087    grok.require('waeup.showStudents')
4088
4089class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
4090    """Page that configures a students export job in datacenter.
4091
4092    """
4093    grok.name('exportconfig')
4094    grok.context(IDataCenter)
4095    redirect_target = '@@export'
4096
4097class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
4098    """Page that configures a students export job in datacenter.
4099
4100    """
4101    grok.name('exportselected')
4102    grok.context(IDataCenter)
4103    redirect_target = '@@export'
4104    grok.template('exportselected')
4105
4106    def update(self, START=None, students=None, exporter=None):
4107        self._set_exporter_values()
4108        if START is None:
4109            return
4110        ena = exports_not_allowed(self)
4111        if ena:
4112            self.flash(ena, type='danger')
4113            return
4114        try:
4115            ids = students.replace(',', ' ').split()
4116        except:
4117            self.flash(sys.exc_info()[1])
4118            self.redirect(self.url(self.redirect_target))
4119            return
4120        job_id = self.context.start_export_job(
4121            exporter, self.request.principal.id, selected=ids)
4122        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4123        self.context.logger.info(
4124            '%s - selected students exported: %s, job_id=%s' %
4125            (ob_class, exporter, job_id))
4126        self.flash(_('Export of selected students started.'))
4127        self.redirect(self.url(self.redirect_target))
4128        return
4129
4130class FacultiesExportJobContainerJobConfig(
4131    DatacenterExportJobContainerJobConfig):
4132    """Page that configures a students export job in facultiescontainer.
4133
4134    """
4135    grok.context(VirtualFacultiesExportJobContainer)
4136    redirect_target = ''
4137
4138class FacultiesExportJobContainerSelectStudents(
4139    DatacenterExportJobContainerSelectStudents):
4140    """Page that configures a students export job in facultiescontainer.
4141
4142    """
4143    grok.context(VirtualFacultiesExportJobContainer)
4144    redirect_target = ''
4145
4146class FacultyExportJobContainerJobConfig(DatacenterExportJobContainerJobConfig):
4147    """Page that configures a students export job in faculties.
4148
4149    """
4150    grok.context(VirtualFacultyExportJobContainer)
4151    redirect_target = ''
4152
4153    @property
4154    def faccode(self):
4155        return self.context.__parent__.code
4156
4157class DepartmentExportJobContainerJobConfig(
4158    DatacenterExportJobContainerJobConfig):
4159    """Page that configures a students export job in departments.
4160
4161    """
4162    grok.context(VirtualDepartmentExportJobContainer)
4163    redirect_target = ''
4164
4165    @property
4166    def depcode(self):
4167        return self.context.__parent__.code
4168
4169class CertificateExportJobContainerJobConfig(
4170    DatacenterExportJobContainerJobConfig):
4171    """Page that configures a students export job for certificates.
4172
4173    """
4174    grok.context(VirtualCertificateExportJobContainer)
4175    grok.template('exportconfig_certificate')
4176    redirect_target = ''
4177
4178    @property
4179    def certcode(self):
4180        return self.context.__parent__.code
4181
4182class CourseExportJobContainerJobConfig(
4183    DatacenterExportJobContainerJobConfig):
4184    """Page that configures a students export job for courses.
4185
4186    In contrast to department or certificate student data exports the
4187    coursetickets_catalog is searched here. Therefore the update
4188    method from the base class is customized.
4189    """
4190    grok.context(VirtualCourseExportJobContainer)
4191    grok.template('exportconfig_course')
4192    redirect_target = ''
4193
4194    def _set_exporter_values(self):
4195        # We provide only the 'coursetickets' and 'lecturer' exporter
4196        # but can add more.
4197        exporters = []
4198        for name in ('coursetickets', 'lecturer'):
4199            util = getUtility(ICSVExporter, name=name)
4200            exporters.append((util.title, name),)
4201        self.exporters = exporters
4202        return
4203
4204    def _set_session_values(self):
4205        # We allow only current academic session
4206        academic_session = grok.getSite()['configuration'].current_academic_session
4207        if not academic_session:
4208            self.sessions = []
4209            return
4210        x = academic_sessions_vocab.getTerm(academic_session)
4211        self.sessions = [(x.title, x.token)]
4212        return
4213
4214    def update(self, START=None, session=None, level=None, mode=None,
4215               exporter=None):
4216        if not checkPermission('waeup.exportData', self.context):
4217            self.flash(_('Not permitted.'), type='danger')
4218            self.redirect(self.url(self.context))
4219            return
4220        self._set_session_values()
4221        self._set_level_values()
4222        self._set_mode_values()
4223        self._set_exporter_values()
4224        if not self.sessions:
4225            self.flash(
4226                _('Academic session not set. '
4227                  'Please contact the administrator.'),
4228                type='danger')
4229            self.redirect(self.url(self.context))
4230            return
4231        if START is None:
4232            return
4233        ena = exports_not_allowed(self)
4234        if ena:
4235            self.flash(ena, type='danger')
4236            return
4237        if session == 'all':
4238            session = None
4239        if level == 'all':
4240            level = None
4241        job_id = self.context.start_export_job(exporter,
4242                                      self.request.principal.id,
4243                                      # Use a different catalog and
4244                                      # pass different keywords than
4245                                      # for the (default) students_catalog
4246                                      catalog='coursetickets',
4247                                      session=session,
4248                                      level=level,
4249                                      code=self.context.__parent__.code)
4250        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
4251        self.context.logger.info(
4252            '%s - exported: %s (%s, %s, %s), job_id=%s'
4253            % (ob_class, exporter, session, level,
4254            self.context.__parent__.code, job_id))
4255        self.flash(_('Export started for course tickets with') +
4256                   ' level_session=%s, level=%s' % (
4257                   session, level))
4258        self.redirect(self.url(self.redirect_target))
4259        return
Note: See TracBrowser for help on using the repository browser.