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

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

Do not show parents email on transcripts. Change permiddion for the 'Send email' button.

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