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

Last change on this file since 14657 was 14649, checked in by Henrik Bettermann, 8 years ago

Do not translate course_category.

  • Property svn:keywords set to Id
File size: 134.4 KB
Line 
1## $Id: browser.py 14649 2017-03-23 05:33:13Z 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
24from cStringIO import StringIO
25from datetime import datetime
26from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
27from urllib import urlencode
28from zope.catalog.interfaces import ICatalog
29from zope.component import queryUtility, getUtility, createObject
30from zope.event import notify
31from zope.formlib.textwidgets import BytesDisplayWidget
32from zope.i18n import translate
33from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
34from zope.security import checkPermission
35from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
36from waeup.kofa.accesscodes.workflow import USED
37from waeup.kofa.browser.breadcrumbs import Breadcrumb
38from waeup.kofa.browser.interfaces import ICaptchaManager
39from waeup.kofa.browser.layout import (
40    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
41    NullValidator, jsaction, action, UtilityView)
42from waeup.kofa.browser.pages import (
43    ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
44    LocalRoleAssignmentUtilityView)
45from waeup.kofa.hostels.hostel import NOT_OCCUPIED
46from waeup.kofa.interfaces import (
47    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
48    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
49    academic_sessions_vocab, IDataCenter, DOCLINK)
50from waeup.kofa.interfaces import MessageFactory as _
51from waeup.kofa.mandates.mandate import PasswordMandate
52from waeup.kofa.university.interfaces import (
53    IDepartment, ICertificate, ICourse)
54from waeup.kofa.university.certificate import (
55    VirtualCertificateExportJobContainer)
56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer)
58from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
59from waeup.kofa.university.facultiescontainer import (
60    VirtualFacultiesExportJobContainer)
61from waeup.kofa.university.course import (
62    VirtualCourseExportJobContainer,)
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.utils.batching import VirtualExportJobContainer
65from waeup.kofa.utils.helpers import get_current_principal, now
66from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
67from waeup.kofa.students.interfaces import (
68    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
69    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
70    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
71    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
72    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
73    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
74    IStudentTranscript
75    )
76from waeup.kofa.students.catalog import search, StudentQueryResultItem
77from waeup.kofa.students.vocabularies import StudyLevelSource
78from waeup.kofa.students.workflow import (
79    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
80    VALIDATED, GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS
81    )
82
83
84grok.context(IKofaObject)  # Make IKofaObject the default context
85
86
87class TicketError(Exception):
88    """A course ticket could not be added
89    """
90    pass
91
92# Save function used for save methods in pages
93def msave(view, **data):
94    changed_fields = view.applyData(view.context, **data)
95    # Turn list of lists into single list
96    if changed_fields:
97        changed_fields = reduce(lambda x, y: x+y, changed_fields.values())
98    # Inform catalog if certificate has changed
99    # (applyData does this only for the context)
100    if 'certificate' in changed_fields:
101        notify(grok.ObjectModifiedEvent(view.context.student))
102    fields_string = ' + '.join(changed_fields)
103    view.flash(_('Form has been saved.'))
104    if fields_string:
105        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
106    return
107
108def emit_lock_message(view):
109    """Flash a lock message.
110    """
111    view.flash(_('The requested form is locked (read-only).'), type="warning")
112    view.redirect(view.url(view.context))
113    return
114
115def translated_values(view):
116    """Translate course ticket attribute values to be displayed on
117    studylevel pages.
118    """
119    lang = view.request.cookies.get('kofa.language')
120    for value in view.context.values():
121        # We have to unghostify (according to Tres Seaver) the __dict__
122        # by activating the object, otherwise value_dict will be empty
123        # when calling the first time.
124        value._p_activate()
125        value_dict = dict([i for i in value.__dict__.items()])
126        value_dict['url'] = view.url(value)
127        value_dict['removable_by_student'] = value.removable_by_student
128        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
129            target_language=lang)
130        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
131            target_language=lang)
132        value_dict['outstanding'] = translate(str(value.outstanding), 'zope',
133            target_language=lang)
134        value_dict['automatic'] = translate(str(value.automatic), 'zope',
135            target_language=lang)
136        value_dict['grade'] = value.grade
137        value_dict['weight'] = value.weight
138        value_dict['course_category'] = value.course_category
139        semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
140        value_dict['semester'] = semester_dict[
141            value.semester].replace('mester', 'm.')
142        yield value_dict
143
144def addCourseTicket(view, course=None):
145    students_utils = getUtility(IStudentsUtils)
146    ticket = createObject(u'waeup.CourseTicket')
147    ticket.automatic = False
148    ticket.carry_over = False
149    warning = students_utils.warnCreditsOOR(view.context, course)
150    if warning:
151        view.flash(warning, type="warning")
152        return False
153    try:
154        view.context.addCourseTicket(ticket, course)
155    except KeyError:
156        view.flash(_('The ticket exists.'), type="warning")
157        return False
158    except TicketError, error:
159        # Ticket errors are not being raised in the base package.
160        view.flash(error, type="warning")
161        return False
162    view.flash(_('Successfully added ${a}.',
163        mapping = {'a':ticket.code}))
164    view.context.writeLogMessage(
165        view,'added: %s|%s|%s' % (
166        ticket.code, ticket.level, ticket.level_session))
167    return True
168
169def level_dict(studycourse):
170    portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
171    level_dict = {}
172    studylevelsource = StudyLevelSource().factory
173    for code in studylevelsource.getValues(studycourse):
174        title = translate(studylevelsource.getTitle(studycourse, code),
175            'waeup.kofa', target_language=portal_language)
176        level_dict[code] = title
177    return level_dict
178
179class StudentsBreadcrumb(Breadcrumb):
180    """A breadcrumb for the students container.
181    """
182    grok.context(IStudentsContainer)
183    title = _('Students')
184
185    @property
186    def target(self):
187        user = get_current_principal()
188        if getattr(user, 'user_type', None) == 'student':
189            return None
190        return self.viewname
191
192class StudentBreadcrumb(Breadcrumb):
193    """A breadcrumb for the student container.
194    """
195    grok.context(IStudent)
196
197    def title(self):
198        return self.context.display_fullname
199
200class SudyCourseBreadcrumb(Breadcrumb):
201    """A breadcrumb for the student study course.
202    """
203    grok.context(IStudentStudyCourse)
204
205    def title(self):
206        if self.context.is_current:
207            return _('Study Course')
208        else:
209            return _('Previous Study Course')
210
211class PaymentsBreadcrumb(Breadcrumb):
212    """A breadcrumb for the student payments folder.
213    """
214    grok.context(IStudentPaymentsContainer)
215    title = _('Payments')
216
217class OnlinePaymentBreadcrumb(Breadcrumb):
218    """A breadcrumb for payments.
219    """
220    grok.context(IStudentOnlinePayment)
221
222    @property
223    def title(self):
224        return self.context.p_id
225
226class AccommodationBreadcrumb(Breadcrumb):
227    """A breadcrumb for the student accommodation folder.
228    """
229    grok.context(IStudentAccommodation)
230    title = _('Accommodation')
231
232class BedTicketBreadcrumb(Breadcrumb):
233    """A breadcrumb for bed tickets.
234    """
235    grok.context(IBedTicket)
236
237    @property
238    def title(self):
239        return _('Bed Ticket ${a}',
240            mapping = {'a':self.context.getSessionString()})
241
242class StudyLevelBreadcrumb(Breadcrumb):
243    """A breadcrumb for course lists.
244    """
245    grok.context(IStudentStudyLevel)
246
247    @property
248    def title(self):
249        return self.context.level_title
250
251class StudentsContainerPage(KofaPage):
252    """The standard view for student containers.
253    """
254    grok.context(IStudentsContainer)
255    grok.name('index')
256    grok.require('waeup.viewStudentsContainer')
257    grok.template('containerpage')
258    label = _('Find students')
259    search_button = _('Find student(s)')
260    pnav = 4
261
262    def update(self, *args, **kw):
263        form = self.request.form
264        self.hitlist = []
265        if form.get('searchtype', None) == 'suspended':
266            self.searchtype = form['searchtype']
267            self.searchterm = None
268        elif form.get('searchtype', None) == 'transcript':
269            self.searchtype = form['searchtype']
270            self.searchterm = None
271        elif 'searchterm' in form and form['searchterm']:
272            self.searchterm = form['searchterm']
273            self.searchtype = form['searchtype']
274        elif 'old_searchterm' in form:
275            self.searchterm = form['old_searchterm']
276            self.searchtype = form['old_searchtype']
277        else:
278            if 'search' in form:
279                self.flash(_('Empty search string'), type="warning")
280            return
281        if self.searchtype == 'current_session':
282            try:
283                self.searchterm = int(self.searchterm)
284            except ValueError:
285                self.flash(_('Only year dates allowed (e.g. 2011).'),
286                           type="danger")
287                return
288        self.hitlist = search(query=self.searchterm,
289            searchtype=self.searchtype, view=self)
290        if not self.hitlist:
291            self.flash(_('No student found.'), type="warning")
292        return
293
294class StudentsContainerManagePage(KofaPage):
295    """The manage page for student containers.
296    """
297    grok.context(IStudentsContainer)
298    grok.name('manage')
299    grok.require('waeup.manageStudent')
300    grok.template('containermanagepage')
301    pnav = 4
302    label = _('Manage students section')
303    search_button = _('Find student(s)')
304    remove_button = _('Remove selected')
305    doclink = DOCLINK + '/students.html'
306
307    def update(self, *args, **kw):
308        form = self.request.form
309        self.hitlist = []
310        if form.get('searchtype', None) == 'suspended':
311            self.searchtype = form['searchtype']
312            self.searchterm = None
313        elif 'searchterm' in form and form['searchterm']:
314            self.searchterm = form['searchterm']
315            self.searchtype = form['searchtype']
316        elif 'old_searchterm' in form:
317            self.searchterm = form['old_searchterm']
318            self.searchtype = form['old_searchtype']
319        else:
320            if 'search' in form:
321                self.flash(_('Empty search string'), type="warning")
322            return
323        if self.searchtype == 'current_session':
324            try:
325                self.searchterm = int(self.searchterm)
326            except ValueError:
327                self.flash(_('Only year dates allowed (e.g. 2011).'),
328                           type="danger")
329                return
330        if not 'entries' in form:
331            self.hitlist = search(query=self.searchterm,
332                searchtype=self.searchtype, view=self)
333            if not self.hitlist:
334                self.flash(_('No student found.'), type="warning")
335            if 'remove' in form:
336                self.flash(_('No item selected.'), type="warning")
337            return
338        entries = form['entries']
339        if isinstance(entries, basestring):
340            entries = [entries]
341        deleted = []
342        for entry in entries:
343            if 'remove' in form:
344                del self.context[entry]
345                deleted.append(entry)
346        self.hitlist = search(query=self.searchterm,
347            searchtype=self.searchtype, view=self)
348        if len(deleted):
349            self.flash(_('Successfully removed: ${a}',
350                mapping = {'a':', '.join(deleted)}))
351        return
352
353class StudentAddFormPage(KofaAddFormPage):
354    """Add-form to add a student.
355    """
356    grok.context(IStudentsContainer)
357    grok.require('waeup.manageStudent')
358    grok.name('addstudent')
359    form_fields = grok.AutoFields(IStudent).select(
360        'firstname', 'middlename', 'lastname', 'reg_number')
361    label = _('Add student')
362    pnav = 4
363
364    @action(_('Create student'), style='primary')
365    def addStudent(self, **data):
366        student = createObject(u'waeup.Student')
367        self.applyData(student, **data)
368        self.context.addStudent(student)
369        self.flash(_('Student record created.'))
370        self.redirect(self.url(self.context[student.student_id], 'index'))
371        return
372
373    @action(_('Create graduated student'), style='primary')
374    def addGraduatedStudent(self, **data):
375        student = createObject(u'waeup.Student')
376        self.applyData(student, **data)
377        self.context.addStudent(student)
378        IWorkflowState(student).setState(GRADUATED)
379        self.flash(_('Student record created.'))
380        self.redirect(self.url(self.context[student.student_id], 'index'))
381        return
382
383class LoginAsStudentStep1(KofaEditFormPage):
384    """ View to temporarily set a student password.
385    """
386    grok.context(IStudent)
387    grok.name('loginasstep1')
388    grok.require('waeup.loginAsStudent')
389    grok.template('loginasstep1')
390    pnav = 4
391
392    def label(self):
393        return _(u'Set temporary password for ${a}',
394            mapping = {'a':self.context.display_fullname})
395
396    @action('Set password now', style='primary')
397    def setPassword(self, *args, **data):
398        kofa_utils = getUtility(IKofaUtils)
399        password = kofa_utils.genPassword()
400        self.context.setTempPassword(self.request.principal.id, password)
401        self.context.writeLogMessage(
402            self, 'temp_password generated: %s' % password)
403        args = {'password':password}
404        self.redirect(self.url(self.context) +
405            '/loginasstep2?%s' % urlencode(args))
406        return
407
408class LoginAsStudentStep2(KofaPage):
409    """ View to temporarily login as student with a temporary password.
410    """
411    grok.context(IStudent)
412    grok.name('loginasstep2')
413    grok.require('waeup.Public')
414    grok.template('loginasstep2')
415    login_button = _('Login now')
416    pnav = 4
417
418    def label(self):
419        return _(u'Login as ${a}',
420            mapping = {'a':self.context.student_id})
421
422    def update(self, SUBMIT=None, password=None):
423        self.password = password
424        if SUBMIT is not None:
425            self.flash(_('You successfully logged in as student.'))
426            self.redirect(self.url(self.context))
427        return
428
429class StudentBaseDisplayFormPage(KofaDisplayFormPage):
430    """ Page to display student base data
431    """
432    grok.context(IStudent)
433    grok.name('index')
434    grok.require('waeup.viewStudent')
435    grok.template('basepage')
436    form_fields = grok.AutoFields(IStudentBase).omit(
437        'password', 'suspended', 'suspended_comment', 'flash_notice')
438    pnav = 4
439
440    @property
441    def label(self):
442        if self.context.suspended:
443            return _('${a}: Base Data (account deactivated)',
444                mapping = {'a':self.context.display_fullname})
445        return  _('${a}: Base Data',
446            mapping = {'a':self.context.display_fullname})
447
448    @property
449    def hasPassword(self):
450        if self.context.password:
451            return _('set')
452        return _('unset')
453
454    def update(self):
455        if self.context.flash_notice:
456            self.flash(self.context.flash_notice, type="warning")
457        super(StudentBaseDisplayFormPage, self).update()
458        return
459
460class StudentBasePDFFormPage(KofaDisplayFormPage):
461    """ Page to display student base data in pdf files.
462    """
463
464    def __init__(self, context, request, omit_fields=()):
465        self.omit_fields = omit_fields
466        super(StudentBasePDFFormPage, self).__init__(context, request)
467
468    @property
469    def form_fields(self):
470        form_fields = grok.AutoFields(IStudentBase)
471        for field in self.omit_fields:
472            form_fields = form_fields.omit(field)
473        return form_fields
474
475class ContactStudentFormPage(ContactAdminFormPage):
476    grok.context(IStudent)
477    grok.name('contactstudent')
478    grok.require('waeup.viewStudent')
479    pnav = 4
480    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
481
482    def update(self, subject=u'', body=u''):
483        super(ContactStudentFormPage, self).update()
484        self.form_fields.get('subject').field.default = subject
485        self.form_fields.get('body').field.default = body
486        return
487
488    def label(self):
489        return _(u'Send message to ${a}',
490            mapping = {'a':self.context.display_fullname})
491
492    @action('Send message now', style='primary')
493    def send(self, *args, **data):
494        try:
495            email = self.request.principal.email
496        except AttributeError:
497            email = self.config.email_admin
498        usertype = getattr(self.request.principal,
499                           'user_type', 'system').title()
500        kofa_utils = getUtility(IKofaUtils)
501        success = kofa_utils.sendContactForm(
502                self.request.principal.title,email,
503                self.context.display_fullname,self.context.email,
504                self.request.principal.id,usertype,
505                self.config.name,
506                data['body'],data['subject'])
507        if success:
508            self.flash(_('Your message has been sent.'))
509        else:
510            self.flash(_('An smtp server error occurred.'), type="danger")
511        return
512
513class ExportPDFAdmissionSlip(UtilityView, grok.View):
514    """Deliver a PDF Admission slip.
515    """
516    grok.context(IStudent)
517    grok.name('admission_slip.pdf')
518    grok.require('waeup.viewStudent')
519    prefix = 'form'
520
521    omit_fields = ('date_of_birth', 'current_level', 'flash_notice')
522
523    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
524
525    @property
526    def label(self):
527        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
528        return translate(_('Admission Letter of'),
529            'waeup.kofa', target_language=portal_language) \
530            + ' %s' % self.context.display_fullname
531
532    def render(self):
533        students_utils = getUtility(IStudentsUtils)
534        return students_utils.renderPDFAdmissionLetter(self,
535            self.context.student, omit_fields=self.omit_fields)
536
537class StudentBaseManageFormPage(KofaEditFormPage):
538    """ View to manage student base data
539    """
540    grok.context(IStudent)
541    grok.name('manage_base')
542    grok.require('waeup.manageStudent')
543    form_fields = grok.AutoFields(IStudentBase).omit(
544        'student_id', 'adm_code', 'suspended')
545    grok.template('basemanagepage')
546    label = _('Manage base data')
547    pnav = 4
548
549    def update(self):
550        super(StudentBaseManageFormPage, self).update()
551        self.wf_info = IWorkflowInfo(self.context)
552        return
553
554    @action(_('Save'), style='primary')
555    def save(self, **data):
556        form = self.request.form
557        password = form.get('password', None)
558        password_ctl = form.get('control_password', None)
559        if password:
560            validator = getUtility(IPasswordValidator)
561            errors = validator.validate_password(password, password_ctl)
562            if errors:
563                self.flash( ' '.join(errors), type="danger")
564                return
565        changed_fields = self.applyData(self.context, **data)
566        # Turn list of lists into single list
567        if changed_fields:
568            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
569        else:
570            changed_fields = []
571        if password:
572            # Now we know that the form has no errors and can set password
573            IUserAccount(self.context).setPassword(password)
574            changed_fields.append('password')
575        fields_string = ' + '.join(changed_fields)
576        self.flash(_('Form has been saved.'))
577        if fields_string:
578            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
579        return
580
581class StudentTriggerTransitionFormPage(KofaEditFormPage):
582    """ View to trigger student workflow transitions
583    """
584    grok.context(IStudent)
585    grok.name('trigtrans')
586    grok.require('waeup.triggerTransition')
587    grok.template('trigtrans')
588    label = _('Trigger registration transition')
589    pnav = 4
590
591    def getTransitions(self):
592        """Return a list of dicts of allowed transition ids and titles.
593
594        Each list entry provides keys ``name`` and ``title`` for
595        internal name and (human readable) title of a single
596        transition.
597        """
598        wf_info = IWorkflowInfo(self.context)
599        allowed_transitions = [t for t in wf_info.getManualTransitions()
600            if not t[0].startswith('pay')]
601        if self.context.is_postgrad and not self.context.is_special_postgrad:
602            allowed_transitions = [t for t in allowed_transitions
603                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
604        return [dict(name='', title=_('No transition'))] +[
605            dict(name=x, title=y) for x, y in allowed_transitions]
606
607    @action(_('Save'), style='primary')
608    def save(self, **data):
609        form = self.request.form
610        if 'transition' in form and form['transition']:
611            transition_id = form['transition']
612            wf_info = IWorkflowInfo(self.context)
613            wf_info.fireTransition(transition_id)
614        return
615
616class StudentActivateView(UtilityView, grok.View):
617    """ Activate student account
618    """
619    grok.context(IStudent)
620    grok.name('activate')
621    grok.require('waeup.manageStudent')
622
623    def update(self):
624        self.context.suspended = False
625        self.context.writeLogMessage(self, 'account activated')
626        history = IObjectHistory(self.context)
627        history.addMessage('Student account activated')
628        self.flash(_('Student account has been activated.'))
629        self.redirect(self.url(self.context))
630        return
631
632    def render(self):
633        return
634
635class StudentDeactivateView(UtilityView, grok.View):
636    """ Deactivate student account
637    """
638    grok.context(IStudent)
639    grok.name('deactivate')
640    grok.require('waeup.manageStudent')
641
642    def update(self):
643        self.context.suspended = True
644        self.context.writeLogMessage(self, 'account deactivated')
645        history = IObjectHistory(self.context)
646        history.addMessage('Student account deactivated')
647        self.flash(_('Student account has been deactivated.'))
648        self.redirect(self.url(self.context))
649        return
650
651    def render(self):
652        return
653
654class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
655    """ Page to display student clearance data
656    """
657    grok.context(IStudent)
658    grok.name('view_clearance')
659    grok.require('waeup.viewStudent')
660    pnav = 4
661
662    @property
663    def separators(self):
664        return getUtility(IStudentsUtils).SEPARATORS_DICT
665
666    @property
667    def form_fields(self):
668        if self.context.is_postgrad:
669            form_fields = grok.AutoFields(IPGStudentClearance)
670        else:
671            form_fields = grok.AutoFields(IUGStudentClearance)
672        if not getattr(self.context, 'officer_comment'):
673            form_fields = form_fields.omit('officer_comment')
674        else:
675            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
676        return form_fields
677
678    @property
679    def label(self):
680        return _('${a}: Clearance Data',
681            mapping = {'a':self.context.display_fullname})
682
683class ExportPDFClearanceSlip(grok.View):
684    """Deliver a PDF slip of the context.
685    """
686    grok.context(IStudent)
687    grok.name('clearance_slip.pdf')
688    grok.require('waeup.viewStudent')
689    prefix = 'form'
690    omit_fields = (
691        'suspended', 'phone',
692        'adm_code', 'suspended_comment',
693        'date_of_birth', 'current_level',
694        'flash_notice')
695
696    @property
697    def form_fields(self):
698        if self.context.is_postgrad:
699            form_fields = grok.AutoFields(IPGStudentClearance)
700        else:
701            form_fields = grok.AutoFields(IUGStudentClearance)
702        if not getattr(self.context, 'officer_comment'):
703            form_fields = form_fields.omit('officer_comment')
704        return form_fields
705
706    @property
707    def title(self):
708        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
709        return translate(_('Clearance Data'), 'waeup.kofa',
710            target_language=portal_language)
711
712    @property
713    def label(self):
714        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
715        return translate(_('Clearance Slip of'),
716            'waeup.kofa', target_language=portal_language) \
717            + ' %s' % self.context.display_fullname
718
719    # XXX: not used in waeup.kofa and thus not tested
720    def _signatures(self):
721        isStudent = getattr(
722            self.request.principal, 'user_type', None) == 'student'
723        if not isStudent and self.context.state in (CLEARED, ):
724            return ([_('Student Signature')],
725                    [_('Clearance Officer Signature')])
726        return
727
728    def _sigsInFooter(self):
729        isStudent = getattr(
730            self.request.principal, 'user_type', None) == 'student'
731        if not isStudent and self.context.state in (CLEARED, ):
732            return (_('Date, Student Signature'),
733                    _('Date, Clearance Officer Signature'),
734                    )
735        return ()
736
737    def render(self):
738        studentview = StudentBasePDFFormPage(self.context.student,
739            self.request, self.omit_fields)
740        students_utils = getUtility(IStudentsUtils)
741        return students_utils.renderPDF(
742            self, 'clearance_slip.pdf',
743            self.context.student, studentview, signatures=self._signatures(),
744            sigs_in_footer=self._sigsInFooter(),
745            omit_fields=self.omit_fields)
746
747class StudentClearanceManageFormPage(KofaEditFormPage):
748    """ Page to manage student clearance data
749    """
750    grok.context(IStudent)
751    grok.name('manage_clearance')
752    grok.require('waeup.manageStudent')
753    grok.template('clearanceeditpage')
754    label = _('Manage clearance data')
755    deletion_warning = _('Are you sure?')
756    pnav = 4
757
758    @property
759    def separators(self):
760        return getUtility(IStudentsUtils).SEPARATORS_DICT
761
762    @property
763    def form_fields(self):
764        if self.context.is_postgrad:
765            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
766        else:
767            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
768        return form_fields
769
770    @action(_('Save'), style='primary')
771    def save(self, **data):
772        msave(self, **data)
773        return
774
775class StudentClearView(UtilityView, grok.View):
776    """ Clear student by clearance officer
777    """
778    grok.context(IStudent)
779    grok.name('clear')
780    grok.require('waeup.clearStudent')
781
782    def update(self):
783        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
784            self.context)
785        if cdm:
786            self.flash(cdm)
787            self.redirect(self.url(self.context,'view_clearance'))
788            return
789        if self.context.state == REQUESTED:
790            IWorkflowInfo(self.context).fireTransition('clear')
791            self.flash(_('Student has been cleared.'))
792        else:
793            self.flash(_('Student is in wrong state.'), type="warning")
794        self.redirect(self.url(self.context,'view_clearance'))
795        return
796
797    def render(self):
798        return
799
800class StudentRejectClearancePage(KofaEditFormPage):
801    """ Reject clearance by clearance officers.
802    """
803    grok.context(IStudent)
804    grok.name('reject_clearance')
805    label = _('Reject clearance')
806    grok.require('waeup.clearStudent')
807    form_fields = grok.AutoFields(
808        IUGStudentClearance).select('officer_comment')
809
810    def update(self):
811        cdm = getUtility(IStudentsUtils).clearance_disabled_message(
812            self.context)
813        if cdm:
814            self.flash(cdm, type="warning")
815            self.redirect(self.url(self.context,'view_clearance'))
816            return
817        return super(StudentRejectClearancePage, self).update()
818
819    @action(_('Save comment and reject clearance now'), style='primary')
820    def reject(self, **data):
821        if self.context.state == CLEARED:
822            IWorkflowInfo(self.context).fireTransition('reset4')
823            message = _('Clearance has been annulled.')
824            self.flash(message, type="warning")
825        elif self.context.state == REQUESTED:
826            IWorkflowInfo(self.context).fireTransition('reset3')
827            message = _('Clearance request has been rejected.')
828            self.flash(message, type="warning")
829        else:
830            self.flash(_('Student is in wrong state.'), type="warning")
831            self.redirect(self.url(self.context,'view_clearance'))
832            return
833        self.applyData(self.context, **data)
834        comment = data['officer_comment']
835        if comment:
836            self.context.writeLogMessage(
837                self, 'comment: %s' % comment.replace('\n', '<br>'))
838            args = {'subject':message, 'body':comment}
839        else:
840            args = {'subject':message,}
841        self.redirect(self.url(self.context) +
842            '/contactstudent?%s' % urlencode(args))
843        return
844
845
846class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
847    """ Page to display student personal data
848    """
849    grok.context(IStudent)
850    grok.name('view_personal')
851    grok.require('waeup.viewStudent')
852    form_fields = grok.AutoFields(IStudentPersonal)
853    form_fields['perm_address'].custom_widget = BytesDisplayWidget
854    form_fields[
855        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
856    pnav = 4
857
858    @property
859    def label(self):
860        return _('${a}: Personal Data',
861            mapping = {'a':self.context.display_fullname})
862
863class StudentPersonalManageFormPage(KofaEditFormPage):
864    """ Page to manage personal data
865    """
866    grok.context(IStudent)
867    grok.name('manage_personal')
868    grok.require('waeup.manageStudent')
869    form_fields = grok.AutoFields(IStudentPersonal)
870    form_fields['personal_updated'].for_display = True
871    form_fields[
872        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
873    label = _('Manage personal data')
874    pnav = 4
875
876    @action(_('Save'), style='primary')
877    def save(self, **data):
878        msave(self, **data)
879        return
880
881class StudentPersonalEditFormPage(KofaEditFormPage):
882    """ Page to edit personal data
883    """
884    grok.context(IStudent)
885    grok.name('edit_personal')
886    grok.require('waeup.handleStudent')
887    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
888    label = _('Edit personal data')
889    pnav = 4
890
891    @action(_('Save/Confirm'), style='primary')
892    def save(self, **data):
893        msave(self, **data)
894        self.context.personal_updated = datetime.utcnow()
895        return
896
897class StudyCourseDisplayFormPage(KofaDisplayFormPage):
898    """ Page to display the student study course data
899    """
900    grok.context(IStudentStudyCourse)
901    grok.name('index')
902    grok.require('waeup.viewStudent')
903    grok.template('studycoursepage')
904    pnav = 4
905
906    @property
907    def form_fields(self):
908        if self.context.is_postgrad:
909            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
910                'previous_verdict')
911        else:
912            form_fields = grok.AutoFields(IStudentStudyCourse)
913        return form_fields
914
915    @property
916    def label(self):
917        if self.context.is_current:
918            return _('${a}: Study Course',
919                mapping = {'a':self.context.__parent__.display_fullname})
920        else:
921            return _('${a}: Previous Study Course',
922                mapping = {'a':self.context.__parent__.display_fullname})
923
924    @property
925    def current_mode(self):
926        if self.context.certificate is not None:
927            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
928            return studymodes_dict[self.context.certificate.study_mode]
929        return
930
931    @property
932    def department(self):
933        if self.context.certificate is not None:
934            return self.context.certificate.__parent__.__parent__
935        return
936
937    @property
938    def faculty(self):
939        if self.context.certificate is not None:
940            return self.context.certificate.__parent__.__parent__.__parent__
941        return
942
943    @property
944    def prev_studycourses(self):
945        if self.context.is_current:
946            if self.context.__parent__.get('studycourse_2', None) is not None:
947                return (
948                        {'href':self.url(self.context.student) + '/studycourse_1',
949                        'title':_('First Study Course, ')},
950                        {'href':self.url(self.context.student) + '/studycourse_2',
951                        'title':_('Second Study Course')}
952                        )
953            if self.context.__parent__.get('studycourse_1', None) is not None:
954                return (
955                        {'href':self.url(self.context.student) + '/studycourse_1',
956                        'title':_('First Study Course')},
957                        )
958        return
959
960class StudyCourseManageFormPage(KofaEditFormPage):
961    """ Page to edit the student study course data
962    """
963    grok.context(IStudentStudyCourse)
964    grok.name('manage')
965    grok.require('waeup.manageStudent')
966    grok.template('studycoursemanagepage')
967    label = _('Manage study course')
968    pnav = 4
969    taboneactions = [_('Save'),_('Cancel')]
970    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
971    tabthreeactions = [_('Add study level')]
972
973    @property
974    def form_fields(self):
975        if self.context.is_postgrad:
976            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
977                'previous_verdict')
978        else:
979            form_fields = grok.AutoFields(IStudentStudyCourse)
980        return form_fields
981
982    def update(self):
983        if not self.context.is_current:
984            emit_lock_message(self)
985            return
986        super(StudyCourseManageFormPage, self).update()
987        return
988
989    @action(_('Save'), style='primary')
990    def save(self, **data):
991        try:
992            msave(self, **data)
993        except ConstraintNotSatisfied:
994            # The selected level might not exist in certificate
995            self.flash(_('Current level not available for certificate.'),
996                       type="warning")
997            return
998        notify(grok.ObjectModifiedEvent(self.context.__parent__))
999        return
1000
1001    @property
1002    def level_dicts(self):
1003        studylevelsource = StudyLevelSource().factory
1004        for code in studylevelsource.getValues(self.context):
1005            title = studylevelsource.getTitle(self.context, code)
1006            yield(dict(code=code, title=title))
1007
1008    @property
1009    def session_dicts(self):
1010        yield(dict(code='', title='--'))
1011        for item in academic_sessions():
1012            code = item[1]
1013            title = item[0]
1014            yield(dict(code=code, title=title))
1015
1016    @action(_('Add study level'), style='primary')
1017    def addStudyLevel(self, **data):
1018        level_code = self.request.form.get('addlevel', None)
1019        level_session = self.request.form.get('level_session', None)
1020        if not level_session:
1021            self.flash(_('You must select a session for the level.'),
1022                       type="warning")
1023            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1024            return
1025        studylevel = createObject(u'waeup.StudentStudyLevel')
1026        studylevel.level = int(level_code)
1027        studylevel.level_session = int(level_session)
1028        try:
1029            self.context.addStudentStudyLevel(
1030                self.context.certificate,studylevel)
1031            self.flash(_('Study level has been added.'))
1032        except KeyError:
1033            self.flash(_('This level exists.'), type="warning")
1034        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1035        return
1036
1037    @jsaction(_('Remove selected levels'))
1038    def delStudyLevels(self, **data):
1039        form = self.request.form
1040        if 'val_id' in form:
1041            child_id = form['val_id']
1042        else:
1043            self.flash(_('No study level selected.'), type="warning")
1044            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1045            return
1046        if not isinstance(child_id, list):
1047            child_id = [child_id]
1048        deleted = []
1049        for id in child_id:
1050            del self.context[id]
1051            deleted.append(id)
1052        if len(deleted):
1053            self.flash(_('Successfully removed: ${a}',
1054                mapping = {'a':', '.join(deleted)}))
1055            self.context.writeLogMessage(
1056                self,'removed: %s' % ', '.join(deleted))
1057        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1058        return
1059
1060class StudentTranscriptRequestPage(KofaPage):
1061    """ Page to request transcript by student
1062    """
1063    grok.context(IStudent)
1064    grok.name('request_transcript')
1065    grok.require('waeup.handleStudent')
1066    grok.template('transcriptrequest')
1067    label = _('Request transcript')
1068    ac_prefix = 'TSC'
1069    notice = ''
1070    pnav = 4
1071    buttonname = _('Submit')
1072    with_ac = True
1073
1074    def update(self, SUBMIT=None):
1075        super(StudentTranscriptRequestPage, self).update()
1076        if not self.context.state == GRADUATED:
1077            self.flash(_("Wrong state"), type="danger")
1078            self.redirect(self.url(self.context))
1079            return
1080        if self.with_ac:
1081            self.ac_series = self.request.form.get('ac_series', None)
1082            self.ac_number = self.request.form.get('ac_number', None)
1083        if self.context.transcript_comment is not None:
1084            self.correspondence = self.context.transcript_comment.replace(
1085                '\n', '<br>')
1086        else:
1087            self.correspondence = ''
1088        if SUBMIT is None:
1089            return
1090        if self.with_ac:
1091            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1092            code = get_access_code(pin)
1093            if not code:
1094                self.flash(_('Activation code is invalid.'), type="warning")
1095                return
1096            if code.state == USED:
1097                self.flash(_('Activation code has already been used.'),
1098                           type="warning")
1099                return
1100            # Mark pin as used (this also fires a pin related transition)
1101            # and fire transition request_transcript
1102            comment = _(u"invalidated")
1103            # Here we know that the ac is in state initialized so we do not
1104            # expect an exception, but the owner might be different
1105            if not invalidate_accesscode(pin, comment, self.context.student_id):
1106                self.flash(_('You are not the owner of this access code.'),
1107                           type="warning")
1108                return
1109            self.context.clr_code = pin
1110        IWorkflowInfo(self.context).fireTransition('request_transcript')
1111        comment = self.request.form.get('comment', '').replace('\r', '')
1112        address = self.request.form.get('address', '').replace('\r', '')
1113        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1114        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1115        old_transcript_comment = self.context.transcript_comment
1116        if old_transcript_comment == None:
1117            old_transcript_comment = ''
1118        self.context.transcript_comment = '''On %s %s wrote:
1119
1120%s
1121
1122Dispatch Address:
1123%s
1124
1125%s''' % (today, self.request.principal.id, comment, address,
1126         old_transcript_comment)
1127        self.context.writeLogMessage(
1128            self, 'comment: %s' % comment.replace('\n', '<br>'))
1129        self.flash(_('Transcript processing has been started.'))
1130        self.redirect(self.url(self.context))
1131        return
1132
1133class StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
1134    """ Page to process transcript requests
1135    """
1136    grok.context(IStudent)
1137    grok.name('process_transcript_request')
1138    grok.require('waeup.viewTranscript')
1139    grok.template('transcriptprocess')
1140    form_fields = grok.AutoFields(IStudentTranscript)
1141    label = _('Process transcript request')
1142    buttonname = _('Save comment and mark as processed')
1143    pnav = 4
1144
1145    def update(self, SUBMIT=None):
1146        super(StudentTranscriptRequestProcessFormPage, self).update()
1147        if self.context.state != TRANSCRIPT:
1148            self.flash(_('Student is in wrong state.'), type="warning")
1149            self.redirect(self.url(self.context))
1150            return
1151        if self.context.transcript_comment is not None:
1152            self.correspondence = self.context.transcript_comment.replace(
1153                '\n', '<br>')
1154        else:
1155            self.correspondence = ''
1156        if SUBMIT is None:
1157            return
1158        IWorkflowInfo(self.context).fireTransition('process_transcript')
1159        self.flash(_('Transcript request processed.'))
1160        comment = self.request.form.get('comment', '').replace('\r', '')
1161        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1162        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1163        old_transcript_comment = self.context.transcript_comment
1164        if old_transcript_comment == None:
1165            old_transcript_comment = ''
1166        self.context.transcript_comment = '''On %s %s wrote:
1167
1168%s
1169
1170%s''' % (today, self.request.principal.id, comment,
1171         old_transcript_comment)
1172        self.context.writeLogMessage(
1173            self, 'comment: %s' % comment.replace('\n', '<br>'))
1174        subject = _('Transcript processed')
1175        args = {'subject':subject, 'body':comment}
1176        self.redirect(self.url(self.context) +
1177            '/contactstudent?%s' % urlencode(args))
1178        return
1179
1180class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1181    """ Page to edit personal data by student
1182    """
1183    grok.context(IStudent)
1184    grok.name('manage_transcript_request')
1185    grok.require('waeup.manageStudent')
1186    form_fields = grok.AutoFields(IStudentTranscript)
1187    label = _('Manage transcript request')
1188    pnav = 4
1189
1190    @action(_('Save'), style='primary')
1191    def save(self, **data):
1192        msave(self, **data)
1193        return
1194
1195class StudyCourseTranscriptPage(KofaDisplayFormPage):
1196    """ Page to display the student's transcript.
1197    """
1198    grok.context(IStudentStudyCourseTranscript)
1199    grok.name('transcript')
1200    grok.require('waeup.viewTranscript')
1201    grok.template('transcript')
1202    pnav = 4
1203
1204    def update(self):
1205        if not self.context.student.transcript_enabled:
1206            self.flash(_('You are not allowed to view the transcript.'),
1207                       type="warning")
1208            self.redirect(self.url(self.context))
1209            return
1210        super(StudyCourseTranscriptPage, self).update()
1211        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1212        self.level_dict = level_dict(self.context)
1213        self.session_dict = dict(
1214            [(item[1], item[0]) for item in academic_sessions()])
1215        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1216        return
1217
1218    @property
1219    def label(self):
1220        # Here we know that the cookie has been set
1221        return _('${a}: Transcript Data', mapping = {
1222            'a':self.context.student.display_fullname})
1223
1224class ExportPDFTranscriptSlip(UtilityView, grok.View):
1225    """Deliver a PDF slip of the context.
1226    """
1227    grok.context(IStudentStudyCourse)
1228    grok.name('transcript.pdf')
1229    grok.require('waeup.viewTranscript')
1230    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1231    prefix = 'form'
1232    omit_fields = (
1233        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1234        'password', 'suspended', 'phone', 'email',
1235        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1236
1237    def update(self):
1238        if not self.context.student.transcript_enabled:
1239            self.flash(_('You are not allowed to download the transcript.'),
1240                       type="warning")
1241            self.redirect(self.url(self.context))
1242            return
1243        super(ExportPDFTranscriptSlip, self).update()
1244        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1245        self.level_dict = level_dict(self.context)
1246        self.session_dict = dict(
1247            [(item[1], item[0]) for item in academic_sessions()])
1248        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1249        return
1250
1251    @property
1252    def label(self):
1253        # Here we know that the cookie has been set
1254        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1255        return translate(_('Academic Transcript'),
1256            'waeup.kofa', target_language=portal_language)
1257
1258    def _sigsInFooter(self):
1259        return (_('CERTIFIED TRUE COPY'),)
1260
1261    def _signatures(self):
1262        return None
1263
1264    def render(self):
1265        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1266        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1267        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1268        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1269        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1270        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1271        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1272        studentview = StudentBasePDFFormPage(self.context.student,
1273            self.request, self.omit_fields)
1274        students_utils = getUtility(IStudentsUtils)
1275
1276        tableheader = [(Code,'code', 2.5),
1277                         (Title,'title', 7),
1278                         (Term, 'semester', 1.5),
1279                         (Cred, 'credits', 1.5),
1280                         (Score, 'total_score', 1.5),
1281                         (Grade, 'grade', 1.5),
1282                         ]
1283
1284        return students_utils.renderPDFTranscript(
1285            self, 'transcript.pdf',
1286            self.context.student, studentview,
1287            omit_fields=self.omit_fields,
1288            tableheader=tableheader,
1289            signatures=self._signatures(),
1290            sigs_in_footer=self._sigsInFooter(),
1291            )
1292
1293class StudentTransferFormPage(KofaAddFormPage):
1294    """Page to transfer the student.
1295    """
1296    grok.context(IStudent)
1297    grok.name('transfer')
1298    grok.require('waeup.manageStudent')
1299    label = _('Transfer student')
1300    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1301        'entry_mode', 'entry_session')
1302    pnav = 4
1303
1304    @jsaction(_('Transfer'))
1305    def transferStudent(self, **data):
1306        error = self.context.transfer(**data)
1307        if error == -1:
1308            self.flash(_('Current level does not match certificate levels.'),
1309                       type="warning")
1310        elif error == -2:
1311            self.flash(_('Former study course record incomplete.'),
1312                       type="warning")
1313        elif error == -3:
1314            self.flash(_('Maximum number of transfers exceeded.'),
1315                       type="warning")
1316        else:
1317            self.flash(_('Successfully transferred.'))
1318        return
1319
1320class RevertTransferFormPage(KofaEditFormPage):
1321    """View that reverts the previous transfer.
1322    """
1323    grok.context(IStudent)
1324    grok.name('revert_transfer')
1325    grok.require('waeup.manageStudent')
1326    grok.template('reverttransfer')
1327    label = _('Revert previous transfer')
1328
1329    def update(self):
1330        if not self.context.has_key('studycourse_1'):
1331            self.flash(_('No previous transfer.'), type="warning")
1332            self.redirect(self.url(self.context))
1333            return
1334        return
1335
1336    @jsaction(_('Revert now'))
1337    def transferStudent(self, **data):
1338        self.context.revert_transfer()
1339        self.flash(_('Previous transfer reverted.'))
1340        self.redirect(self.url(self.context, 'studycourse'))
1341        return
1342
1343class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1344    """ Page to display student study levels
1345    """
1346    grok.context(IStudentStudyLevel)
1347    grok.name('index')
1348    grok.require('waeup.viewStudent')
1349    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1350    form_fields[
1351        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1352    grok.template('studylevelpage')
1353    pnav = 4
1354
1355    def update(self):
1356        super(StudyLevelDisplayFormPage, self).update()
1357        return
1358
1359    @property
1360    def translated_values(self):
1361        return translated_values(self)
1362
1363    @property
1364    def label(self):
1365        # Here we know that the cookie has been set
1366        lang = self.request.cookies.get('kofa.language')
1367        level_title = translate(self.context.level_title, 'waeup.kofa',
1368            target_language=lang)
1369        return _('${a}: Study Level ${b}', mapping = {
1370            'a':self.context.student.display_fullname,
1371            'b':level_title})
1372
1373class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1374    """Deliver a PDF slip of the context.
1375    """
1376    grok.context(IStudentStudyLevel)
1377    grok.name('course_registration_slip.pdf')
1378    grok.require('waeup.viewStudent')
1379    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1380    form_fields[
1381        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1382    prefix = 'form'
1383    omit_fields = (
1384        'password', 'suspended', 'phone', 'date_of_birth',
1385        'adm_code', 'sex', 'suspended_comment', 'current_level',
1386        'flash_notice')
1387
1388    @property
1389    def title(self):
1390        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1391        return translate(_('Level Data'), 'waeup.kofa',
1392            target_language=portal_language)
1393
1394    @property
1395    def label(self):
1396        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1397        lang = self.request.cookies.get('kofa.language', portal_language)
1398        level_title = translate(self.context.level_title, 'waeup.kofa',
1399            target_language=lang)
1400        return translate(_('Course Registration Slip'),
1401            'waeup.kofa', target_language=portal_language) \
1402            + ' %s' % level_title
1403
1404    @property
1405    def tabletitle(self):
1406        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1407        tabletitle = []
1408        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1409            target_language=portal_language))
1410        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1411            target_language=portal_language))
1412        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1413            target_language=portal_language))
1414        return tabletitle
1415
1416    def render(self):
1417        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1418        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1419        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1420        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1421        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1422        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1423        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1424        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1425        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1426        studentview = StudentBasePDFFormPage(self.context.student,
1427            self.request, self.omit_fields)
1428        students_utils = getUtility(IStudentsUtils)
1429
1430        tabledata = []
1431        tableheader = []
1432        for i in range(1,7):
1433            tabledata.append(sorted(
1434                [value for value in self.context.values() if value.semester == i],
1435                key=lambda value: str(value.semester) + value.code))
1436            tableheader.append([(Code,'code', 2.5),
1437                             (Title,'title', 5),
1438                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1439                             (Cred, 'credits', 1.5),
1440                             #(Mand, 'mandatory', 1.5),
1441                             (Score, 'score', 1.5),
1442                             (Grade, 'grade', 1.5),
1443                             #('Auto', 'automatic', 1.5)
1444                             ])
1445        return students_utils.renderPDF(
1446            self, 'course_registration_slip.pdf',
1447            self.context.student, studentview,
1448            tableheader=tableheader,
1449            tabledata=tabledata,
1450            omit_fields=self.omit_fields
1451            )
1452
1453class StudyLevelManageFormPage(KofaEditFormPage):
1454    """ Page to edit the student study level data
1455    """
1456    grok.context(IStudentStudyLevel)
1457    grok.name('manage')
1458    grok.require('waeup.manageStudent')
1459    grok.template('studylevelmanagepage')
1460    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1461        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1462    pnav = 4
1463    taboneactions = [_('Save'),_('Cancel')]
1464    tabtwoactions = [_('Add course ticket'),
1465        _('Remove selected tickets'),_('Cancel')]
1466    placeholder = _('Enter valid course code')
1467
1468    def update(self, ADD=None, course=None):
1469        if not self.context.__parent__.is_current:
1470            emit_lock_message(self)
1471            return
1472        super(StudyLevelManageFormPage, self).update()
1473        if ADD is not None:
1474            if not course:
1475                self.flash(_('No valid course code entered.'), type="warning")
1476                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1477                return
1478            cat = queryUtility(ICatalog, name='courses_catalog')
1479            result = cat.searchResults(code=(course, course))
1480            if len(result) != 1:
1481                self.flash(_('Course not found.'), type="warning")
1482            else:
1483                course = list(result)[0]
1484                addCourseTicket(self, course)
1485            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1486        return
1487
1488    @property
1489    def translated_values(self):
1490        return translated_values(self)
1491
1492    @property
1493    def label(self):
1494        # Here we know that the cookie has been set
1495        lang = self.request.cookies.get('kofa.language')
1496        level_title = translate(self.context.level_title, 'waeup.kofa',
1497            target_language=lang)
1498        return _('Manage study level ${a}',
1499            mapping = {'a':level_title})
1500
1501    @action(_('Save'), style='primary')
1502    def save(self, **data):
1503        msave(self, **data)
1504        return
1505
1506    @jsaction(_('Remove selected tickets'))
1507    def delCourseTicket(self, **data):
1508        form = self.request.form
1509        if 'val_id' in form:
1510            child_id = form['val_id']
1511        else:
1512            self.flash(_('No ticket selected.'), type="warning")
1513            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1514            return
1515        if not isinstance(child_id, list):
1516            child_id = [child_id]
1517        deleted = []
1518        for id in child_id:
1519            del self.context[id]
1520            deleted.append(id)
1521        if len(deleted):
1522            self.flash(_('Successfully removed: ${a}',
1523                mapping = {'a':', '.join(deleted)}))
1524            self.context.writeLogMessage(
1525                self,'removed: %s at %s' %
1526                (', '.join(deleted), self.context.level))
1527        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1528        return
1529
1530class ValidateCoursesView(UtilityView, grok.View):
1531    """ Validate course list by course adviser
1532    """
1533    grok.context(IStudentStudyLevel)
1534    grok.name('validate_courses')
1535    grok.require('waeup.validateStudent')
1536
1537    def update(self):
1538        if not self.context.__parent__.is_current:
1539            emit_lock_message(self)
1540            return
1541        if str(self.context.__parent__.current_level) != self.context.__name__:
1542            self.flash(_('This is not the student\'s current level.'),
1543                       type="danger")
1544        elif self.context.student.state == REGISTERED:
1545            IWorkflowInfo(self.context.student).fireTransition(
1546                'validate_courses')
1547            self.flash(_('Course list has been validated.'))
1548        else:
1549            self.flash(_('Student is in the wrong state.'), type="warning")
1550        self.redirect(self.url(self.context))
1551        return
1552
1553    def render(self):
1554        return
1555
1556class RejectCoursesView(UtilityView, grok.View):
1557    """ Reject course list by course adviser
1558    """
1559    grok.context(IStudentStudyLevel)
1560    grok.name('reject_courses')
1561    grok.require('waeup.validateStudent')
1562
1563    def update(self):
1564        if not self.context.__parent__.is_current:
1565            emit_lock_message(self)
1566            return
1567        if str(self.context.__parent__.current_level) != self.context.__name__:
1568            self.flash(_('This is not the student\'s current level.'),
1569                       type="danger")
1570            self.redirect(self.url(self.context))
1571            return
1572        elif self.context.student.state == VALIDATED:
1573            IWorkflowInfo(self.context.student).fireTransition('reset8')
1574            message = _('Course list request has been annulled.')
1575            self.flash(message)
1576        elif self.context.student.state == REGISTERED:
1577            IWorkflowInfo(self.context.student).fireTransition('reset7')
1578            message = _('Course list has been unregistered.')
1579            self.flash(message)
1580        else:
1581            self.flash(_('Student is in the wrong state.'), type="warning")
1582            self.redirect(self.url(self.context))
1583            return
1584        args = {'subject':message}
1585        self.redirect(self.url(self.context.student) +
1586            '/contactstudent?%s' % urlencode(args))
1587        return
1588
1589    def render(self):
1590        return
1591
1592class UnregisterCoursesView(UtilityView, grok.View):
1593    """Unegister course list by student
1594    """
1595    grok.context(IStudentStudyLevel)
1596    grok.name('unregister_courses')
1597    grok.require('waeup.handleStudent')
1598
1599    def update(self):
1600        if not self.context.__parent__.is_current:
1601            emit_lock_message(self)
1602            return
1603        try:
1604            deadline = grok.getSite()['configuration'][
1605                str(self.context.level_session)].coursereg_deadline
1606        except (TypeError, KeyError):
1607            deadline = None
1608        if deadline and deadline < datetime.now(pytz.utc):
1609            self.flash(_(
1610                "Course registration has ended. "
1611                "Unregistration is disabled."), type="warning")
1612        elif str(self.context.__parent__.current_level) != self.context.__name__:
1613            self.flash(_('This is not your current level.'), type="danger")
1614        elif self.context.student.state == REGISTERED:
1615            IWorkflowInfo(self.context.student).fireTransition('reset7')
1616            message = _('Course list has been unregistered.')
1617            self.flash(message)
1618        else:
1619            self.flash(_('You are in the wrong state.'), type="warning")
1620        self.redirect(self.url(self.context))
1621        return
1622
1623    def render(self):
1624        return
1625
1626class CourseTicketAddFormPage(KofaAddFormPage):
1627    """Add a course ticket.
1628    """
1629    grok.context(IStudentStudyLevel)
1630    grok.name('add')
1631    grok.require('waeup.manageStudent')
1632    label = _('Add course ticket')
1633    form_fields = grok.AutoFields(ICourseTicketAdd)
1634    pnav = 4
1635
1636    def update(self):
1637        if not self.context.__parent__.is_current:
1638            emit_lock_message(self)
1639            return
1640        super(CourseTicketAddFormPage, self).update()
1641        return
1642
1643    @action(_('Add course ticket'), style='primary')
1644    def addCourseTicket(self, **data):
1645        course = data['course']
1646        success = addCourseTicket(self, course)
1647        if success:
1648            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1649        return
1650
1651    @action(_('Cancel'), validator=NullValidator)
1652    def cancel(self, **data):
1653        self.redirect(self.url(self.context))
1654
1655class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1656    """ Page to display course tickets
1657    """
1658    grok.context(ICourseTicket)
1659    grok.name('index')
1660    grok.require('waeup.viewStudent')
1661    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1662    grok.template('courseticketpage')
1663    pnav = 4
1664
1665    @property
1666    def label(self):
1667        return _('${a}: Course Ticket ${b}', mapping = {
1668            'a':self.context.student.display_fullname,
1669            'b':self.context.code})
1670
1671class CourseTicketManageFormPage(KofaEditFormPage):
1672    """ Page to manage course tickets
1673    """
1674    grok.context(ICourseTicket)
1675    grok.name('manage')
1676    grok.require('waeup.manageStudent')
1677    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1678    form_fields['title'].for_display = True
1679    form_fields['fcode'].for_display = True
1680    form_fields['dcode'].for_display = True
1681    form_fields['semester'].for_display = True
1682    form_fields['passmark'].for_display = True
1683    form_fields['credits'].for_display = True
1684    form_fields['mandatory'].for_display = False
1685    form_fields['automatic'].for_display = True
1686    form_fields['carry_over'].for_display = True
1687    pnav = 4
1688    grok.template('courseticketmanagepage')
1689
1690    @property
1691    def label(self):
1692        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1693
1694    @action('Save', style='primary')
1695    def save(self, **data):
1696        msave(self, **data)
1697        return
1698
1699class PaymentsManageFormPage(KofaEditFormPage):
1700    """ Page to manage the student payments
1701
1702    This manage form page is for both students and students officers.
1703    """
1704    grok.context(IStudentPaymentsContainer)
1705    grok.name('index')
1706    grok.require('waeup.viewStudent')
1707    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1708    grok.template('paymentsmanagepage')
1709    pnav = 4
1710
1711    @property
1712    def manage_payments_allowed(self):
1713        return checkPermission('waeup.payStudent', self.context)
1714
1715    def unremovable(self, ticket):
1716        usertype = getattr(self.request.principal, 'user_type', None)
1717        if not usertype:
1718            return False
1719        if not self.manage_payments_allowed:
1720            return True
1721        return (self.request.principal.user_type == 'student' and ticket.r_code)
1722
1723    @property
1724    def label(self):
1725        return _('${a}: Payments',
1726            mapping = {'a':self.context.__parent__.display_fullname})
1727
1728    @jsaction(_('Remove selected tickets'))
1729    def delPaymentTicket(self, **data):
1730        form = self.request.form
1731        if 'val_id' in form:
1732            child_id = form['val_id']
1733        else:
1734            self.flash(_('No payment selected.'), type="warning")
1735            self.redirect(self.url(self.context))
1736            return
1737        if not isinstance(child_id, list):
1738            child_id = [child_id]
1739        deleted = []
1740        for id in child_id:
1741            # Students are not allowed to remove used payment tickets
1742            ticket = self.context.get(id, None)
1743            if ticket is not None and not self.unremovable(ticket):
1744                del self.context[id]
1745                deleted.append(id)
1746        if len(deleted):
1747            self.flash(_('Successfully removed: ${a}',
1748                mapping = {'a': ', '.join(deleted)}))
1749            self.context.writeLogMessage(
1750                self,'removed: %s' % ', '.join(deleted))
1751        self.redirect(self.url(self.context))
1752        return
1753
1754    #@action(_('Add online payment ticket'))
1755    #def addPaymentTicket(self, **data):
1756    #    self.redirect(self.url(self.context, '@@addop'))
1757
1758class OnlinePaymentAddFormPage(KofaAddFormPage):
1759    """ Page to add an online payment ticket
1760    """
1761    grok.context(IStudentPaymentsContainer)
1762    grok.name('addop')
1763    grok.template('onlinepaymentaddform')
1764    grok.require('waeup.payStudent')
1765    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1766        'p_category')
1767    label = _('Add online payment')
1768    pnav = 4
1769
1770    @property
1771    def selectable_categories(self):
1772        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1773        return sorted(categories.items())
1774
1775    @action(_('Create ticket'), style='primary')
1776    def createTicket(self, **data):
1777        p_category = data['p_category']
1778        previous_session = data.get('p_session', None)
1779        previous_level = data.get('p_level', None)
1780        student = self.context.__parent__
1781        # The hostel_application payment category is temporarily used
1782        # by Uniben.
1783        if p_category in ('bed_allocation', 'hostel_application') and student[
1784            'studycourse'].current_session != grok.getSite()[
1785            'hostels'].accommodation_session:
1786                self.flash(
1787                    _('Your current session does not match ' + \
1788                    'accommodation session.'), type="danger")
1789                return
1790        if 'maintenance' in p_category:
1791            current_session = str(student['studycourse'].current_session)
1792            if not current_session in student['accommodation']:
1793                self.flash(_('You have not yet booked accommodation.'),
1794                           type="warning")
1795                return
1796        students_utils = getUtility(IStudentsUtils)
1797        error, payment = students_utils.setPaymentDetails(
1798            p_category, student, previous_session, previous_level)
1799        if error is not None:
1800            self.flash(error, type="danger")
1801            return
1802        if p_category == 'transfer':
1803            payment.p_item = self.request.form['new_programme']
1804        self.context[payment.p_id] = payment
1805        self.flash(_('Payment ticket created.'))
1806        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1807        self.redirect(self.url(self.context))
1808        return
1809
1810    @action(_('Cancel'), validator=NullValidator)
1811    def cancel(self, **data):
1812        self.redirect(self.url(self.context))
1813
1814class PreviousPaymentAddFormPage(KofaAddFormPage):
1815    """ Page to add an online payment ticket for previous sessions.
1816    """
1817    grok.context(IStudentPaymentsContainer)
1818    grok.name('addpp')
1819    grok.require('waeup.payStudent')
1820    form_fields = grok.AutoFields(IStudentPreviousPayment)
1821    label = _('Add previous session online payment')
1822    pnav = 4
1823
1824    def update(self):
1825        if self.context.student.before_payment:
1826            self.flash(_("No previous payment to be made."), type="warning")
1827            self.redirect(self.url(self.context))
1828        super(PreviousPaymentAddFormPage, self).update()
1829        return
1830
1831    @action(_('Create ticket'), style='primary')
1832    def createTicket(self, **data):
1833        p_category = data['p_category']
1834        previous_session = data.get('p_session', None)
1835        previous_level = data.get('p_level', None)
1836        student = self.context.__parent__
1837        students_utils = getUtility(IStudentsUtils)
1838        error, payment = students_utils.setPaymentDetails(
1839            p_category, student, previous_session, previous_level)
1840        if error is not None:
1841            self.flash(error, type="danger")
1842            return
1843        self.context[payment.p_id] = payment
1844        self.flash(_('Payment ticket created.'))
1845        self.redirect(self.url(self.context))
1846        return
1847
1848    @action(_('Cancel'), validator=NullValidator)
1849    def cancel(self, **data):
1850        self.redirect(self.url(self.context))
1851
1852class BalancePaymentAddFormPage(KofaAddFormPage):
1853    """ Page to add an online payment which can balance s previous session
1854    payment.
1855    """
1856    grok.context(IStudentPaymentsContainer)
1857    grok.name('addbp')
1858    grok.require('waeup.manageStudent')
1859    form_fields = grok.AutoFields(IStudentBalancePayment)
1860    label = _('Add balance')
1861    pnav = 4
1862
1863    @action(_('Create ticket'), style='primary')
1864    def createTicket(self, **data):
1865        p_category = data['p_category']
1866        balance_session = data.get('balance_session', None)
1867        balance_level = data.get('balance_level', None)
1868        balance_amount = data.get('balance_amount', None)
1869        student = self.context.__parent__
1870        students_utils = getUtility(IStudentsUtils)
1871        error, payment = students_utils.setBalanceDetails(
1872            p_category, student, balance_session,
1873            balance_level, balance_amount)
1874        if error is not None:
1875            self.flash(error, type="danger")
1876            return
1877        self.context[payment.p_id] = payment
1878        self.flash(_('Payment ticket created.'))
1879        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1880        self.redirect(self.url(self.context))
1881        return
1882
1883    @action(_('Cancel'), validator=NullValidator)
1884    def cancel(self, **data):
1885        self.redirect(self.url(self.context))
1886
1887class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1888    """ Page to view an online payment ticket
1889    """
1890    grok.context(IStudentOnlinePayment)
1891    grok.name('index')
1892    grok.require('waeup.viewStudent')
1893    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1894    form_fields[
1895        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1896    form_fields[
1897        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1898    pnav = 4
1899
1900    @property
1901    def label(self):
1902        return _('${a}: Online Payment Ticket ${b}', mapping = {
1903            'a':self.context.student.display_fullname,
1904            'b':self.context.p_id})
1905
1906class OnlinePaymentApproveView(UtilityView, grok.View):
1907    """ Callback view
1908    """
1909    grok.context(IStudentOnlinePayment)
1910    grok.name('approve')
1911    grok.require('waeup.managePortal')
1912
1913    def update(self):
1914        flashtype, msg, log = self.context.approveStudentPayment()
1915        if log is not None:
1916            # Add log message to students.log
1917            self.context.writeLogMessage(self,log)
1918            # Add log message to payments.log
1919            self.context.logger.info(
1920                '%s,%s,%s,%s,%s,,,,,,' % (
1921                self.context.student.student_id,
1922                self.context.p_id, self.context.p_category,
1923                self.context.amount_auth, self.context.r_code))
1924        self.flash(msg, type=flashtype)
1925        return
1926
1927    def render(self):
1928        self.redirect(self.url(self.context, '@@index'))
1929        return
1930
1931class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
1932    """ Approval view for students.
1933
1934    This view is used for browser tests only and
1935    must be neutralized in custom pages!
1936    """
1937    grok.name('fake_approve')
1938    grok.require('waeup.payStudent')
1939
1940class ExportPDFPaymentSlip(UtilityView, grok.View):
1941    """Deliver a PDF slip of the context.
1942    """
1943    grok.context(IStudentOnlinePayment)
1944    grok.name('payment_slip.pdf')
1945    grok.require('waeup.viewStudent')
1946    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1947    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1948    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1949    prefix = 'form'
1950    note = None
1951    omit_fields = (
1952        'password', 'suspended', 'phone', 'date_of_birth',
1953        'adm_code', 'sex', 'suspended_comment', 'current_level',
1954        'flash_notice')
1955
1956    @property
1957    def title(self):
1958        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1959        return translate(_('Payment Data'), 'waeup.kofa',
1960            target_language=portal_language)
1961
1962    @property
1963    def label(self):
1964        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1965        return translate(_('Online Payment Slip'),
1966            'waeup.kofa', target_language=portal_language) \
1967            + ' %s' % self.context.p_id
1968
1969    def render(self):
1970        #if self.context.p_state != 'paid':
1971        #    self.flash('Ticket not yet paid.')
1972        #    self.redirect(self.url(self.context))
1973        #    return
1974        studentview = StudentBasePDFFormPage(self.context.student,
1975            self.request, self.omit_fields)
1976        students_utils = getUtility(IStudentsUtils)
1977        return students_utils.renderPDF(self, 'payment_slip.pdf',
1978            self.context.student, studentview, note=self.note,
1979            omit_fields=self.omit_fields)
1980
1981
1982class AccommodationManageFormPage(KofaEditFormPage):
1983    """ Page to manage bed tickets.
1984
1985    This manage form page is for both students and students officers.
1986    """
1987    grok.context(IStudentAccommodation)
1988    grok.name('index')
1989    grok.require('waeup.handleAccommodation')
1990    form_fields = grok.AutoFields(IStudentAccommodation)
1991    grok.template('accommodationmanagepage')
1992    pnav = 4
1993    with_hostel_selection = True
1994
1995    @property
1996    def actionsgroup1(self):
1997        if not self.with_hostel_selection:
1998            return []
1999        students_utils = getUtility(IStudentsUtils)
2000        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2001        error_message = students_utils.checkAccommodationRequirements(
2002            self.context.student, acc_details)
2003        if error_message:
2004            return []
2005        return [_('Save')]
2006
2007    @property
2008    def actionsgroup2(self):
2009        if getattr(self.request.principal, 'user_type', None) == 'student':
2010            return [_('Book accommodation')]
2011        return [_('Book accommodation'), _('Remove selected')]
2012
2013    @property
2014    def label(self):
2015        return _('${a}: Accommodation',
2016            mapping = {'a':self.context.__parent__.display_fullname})
2017
2018    @property
2019    def desired_hostel(self):
2020        if self.context.desired_hostel:
2021            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2022            if hostel is not None:
2023                return hostel.hostel_name
2024        return
2025
2026    def getHostels(self):
2027        """Get a list of all stored hostels.
2028        """
2029        yield(dict(name=None, title='--', selected=''))
2030        for val in grok.getSite()['hostels'].values():
2031            selected = ''
2032            if val.hostel_id == self.context.desired_hostel:
2033                selected = 'selected'
2034            yield(dict(name=val.hostel_id, title=val.hostel_name,
2035                       selected=selected))
2036
2037    @action(_('Save'), style='primary')
2038    def save(self):
2039        hostel = self.request.form.get('hostel', None)
2040        self.context.desired_hostel = hostel
2041        self.flash(_('Your selection has been saved.'))
2042        return
2043
2044    @action(_('Book accommodation'), style='primary')
2045    def bookAccommodation(self, **data):
2046        self.redirect(self.url(self.context, 'add'))
2047        return
2048
2049    @jsaction(_('Remove selected'))
2050    def delBedTickets(self, **data):
2051        if getattr(self.request.principal, 'user_type', None) == 'student':
2052            self.flash(_('You are not allowed to remove bed tickets.'),
2053                       type="warning")
2054            self.redirect(self.url(self.context))
2055            return
2056        form = self.request.form
2057        if 'val_id' in form:
2058            child_id = form['val_id']
2059        else:
2060            self.flash(_('No bed ticket selected.'), type="warning")
2061            self.redirect(self.url(self.context))
2062            return
2063        if not isinstance(child_id, list):
2064            child_id = [child_id]
2065        deleted = []
2066        for id in child_id:
2067            del self.context[id]
2068            deleted.append(id)
2069        if len(deleted):
2070            self.flash(_('Successfully removed: ${a}',
2071                mapping = {'a':', '.join(deleted)}))
2072            self.context.writeLogMessage(
2073                self,'removed: % s' % ', '.join(deleted))
2074        self.redirect(self.url(self.context))
2075        return
2076
2077class BedTicketAddPage(KofaPage):
2078    """ Page to add an online payment ticket
2079    """
2080    grok.context(IStudentAccommodation)
2081    grok.name('add')
2082    grok.require('waeup.handleAccommodation')
2083    grok.template('enterpin')
2084    ac_prefix = 'HOS'
2085    label = _('Add bed ticket')
2086    pnav = 4
2087    buttonname = _('Create bed ticket')
2088    notice = ''
2089    with_ac = True
2090
2091    def update(self, SUBMIT=None):
2092        student = self.context.student
2093        students_utils = getUtility(IStudentsUtils)
2094        acc_details  = students_utils.getAccommodationDetails(student)
2095        error_message = students_utils.checkAccommodationRequirements(
2096            student, acc_details)
2097        if error_message:
2098            self.flash(error_message, type="warning")
2099            self.redirect(self.url(self.context))
2100            return
2101        if self.with_ac:
2102            self.ac_series = self.request.form.get('ac_series', None)
2103            self.ac_number = self.request.form.get('ac_number', None)
2104        if SUBMIT is None:
2105            return
2106        if self.with_ac:
2107            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2108            code = get_access_code(pin)
2109            if not code:
2110                self.flash(_('Activation code is invalid.'), type="warning")
2111                return
2112        # Search and book bed
2113        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2114        entries = cat.searchResults(
2115            owner=(student.student_id,student.student_id))
2116        if len(entries):
2117            # If bed space has been manually allocated use this bed
2118            manual = True
2119            bed = [entry for entry in entries][0]
2120            # Safety belt for paranoids: Does this bed really exist on portal?
2121            # XXX: Can be remove if nobody complains.
2122            if bed.__parent__.__parent__ is None:
2123                self.flash(_('System error: Please contact the adminsitrator.'),
2124                           type="danger")
2125                self.context.writeLogMessage(
2126                    self, 'fatal error: %s' % bed.bed_id)
2127                return
2128        else:
2129            # else search for other available beds
2130            manual = False
2131            entries = cat.searchResults(
2132                bed_type=(acc_details['bt'],acc_details['bt']))
2133            available_beds = [
2134                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2135            if available_beds:
2136                students_utils = getUtility(IStudentsUtils)
2137                bed = students_utils.selectBed(
2138                    available_beds, self.context.desired_hostel)
2139                if bed is None:
2140                    self.flash(_(
2141                        'There is no free bed in your desired hostel. '
2142                        'Please try another hostel.'),
2143                        type="warning")
2144                    self.redirect(self.url(self.context))
2145                    return
2146                # Safety belt for paranoids: Does this bed really exist
2147                # in portal?
2148                # XXX: Can be remove if nobody complains.
2149                if bed.__parent__.__parent__ is None:
2150                    self.flash(_(
2151                        'System error: Please contact the administrator.'),
2152                        type="warning")
2153                    self.context.writeLogMessage(
2154                        self, 'fatal error: %s' % bed.bed_id)
2155                    return
2156                bed.bookBed(student.student_id)
2157            else:
2158                self.flash(_('There is no free bed in your category ${a}.',
2159                    mapping = {'a':acc_details['bt']}), type="warning")
2160                self.redirect(self.url(self.context))
2161                return
2162        if self.with_ac:
2163            # Mark pin as used (this also fires a pin related transition)
2164            if code.state == USED:
2165                self.flash(_('Activation code has already been used.'),
2166                           type="warning")
2167                if not manual:
2168                    # Release the previously booked bed
2169                    bed.owner = NOT_OCCUPIED
2170                    # Catalog must be informed
2171                    notify(grok.ObjectModifiedEvent(bed))
2172                return
2173            else:
2174                comment = _(u'invalidated')
2175                # Here we know that the ac is in state initialized so we do not
2176                # expect an exception, but the owner might be different
2177                success = invalidate_accesscode(
2178                    pin, comment, self.context.student.student_id)
2179                if not success:
2180                    self.flash(_('You are not the owner of this access code.'),
2181                               type="warning")
2182                    if not manual:
2183                        # Release the previously booked bed
2184                        bed.owner = NOT_OCCUPIED
2185                        # Catalog must be informed
2186                        notify(grok.ObjectModifiedEvent(bed))
2187                    return
2188        # Create bed ticket
2189        bedticket = createObject(u'waeup.BedTicket')
2190        if self.with_ac:
2191            bedticket.booking_code = pin
2192        bedticket.booking_session = acc_details['booking_session']
2193        bedticket.bed_type = acc_details['bt']
2194        bedticket.bed = bed
2195        hall_title = bed.__parent__.hostel_name
2196        coordinates = bed.coordinates[1:]
2197        block, room_nr, bed_nr = coordinates
2198        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2199            'a':hall_title, 'b':block,
2200            'c':room_nr, 'd':bed_nr,
2201            'e':bed.bed_type})
2202        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2203        bedticket.bed_coordinates = translate(
2204            bc, 'waeup.kofa',target_language=portal_language)
2205        self.context.addBedTicket(bedticket)
2206        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2207        self.flash(_('Bed ticket created and bed booked: ${a}',
2208            mapping = {'a':bedticket.display_coordinates}))
2209        self.redirect(self.url(self.context))
2210        return
2211
2212class BedTicketDisplayFormPage(KofaDisplayFormPage):
2213    """ Page to display bed tickets
2214    """
2215    grok.context(IBedTicket)
2216    grok.name('index')
2217    grok.require('waeup.handleAccommodation')
2218    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2219    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2220    pnav = 4
2221
2222    @property
2223    def label(self):
2224        return _('Bed Ticket for Session ${a}',
2225            mapping = {'a':self.context.getSessionString()})
2226
2227class ExportPDFBedTicketSlip(UtilityView, grok.View):
2228    """Deliver a PDF slip of the context.
2229    """
2230    grok.context(IBedTicket)
2231    grok.name('bed_allocation_slip.pdf')
2232    grok.require('waeup.handleAccommodation')
2233    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2234    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2235    prefix = 'form'
2236    omit_fields = (
2237        'password', 'suspended', 'phone', 'adm_code',
2238        'suspended_comment', 'date_of_birth', 'current_level',
2239        'flash_notice')
2240
2241    @property
2242    def title(self):
2243        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2244        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2245            target_language=portal_language)
2246
2247    @property
2248    def label(self):
2249        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2250        #return translate(_('Bed Allocation: '),
2251        #    'waeup.kofa', target_language=portal_language) \
2252        #    + ' %s' % self.context.bed_coordinates
2253        return translate(_('Bed Allocation Slip'),
2254            'waeup.kofa', target_language=portal_language) \
2255            + ' %s' % self.context.getSessionString()
2256
2257    def render(self):
2258        studentview = StudentBasePDFFormPage(self.context.student,
2259            self.request, self.omit_fields)
2260        students_utils = getUtility(IStudentsUtils)
2261        return students_utils.renderPDF(
2262            self, 'bed_allocation_slip.pdf',
2263            self.context.student, studentview,
2264            omit_fields=self.omit_fields)
2265
2266class BedTicketRelocationView(UtilityView, grok.View):
2267    """ Callback view
2268    """
2269    grok.context(IBedTicket)
2270    grok.name('relocate')
2271    grok.require('waeup.manageHostels')
2272
2273    # Relocate student if student parameters have changed or the bed_type
2274    # of the bed has changed
2275    def update(self):
2276        success, msg = self.context.relocateStudent()
2277        if not success:
2278            self.flash(msg, type="warning")
2279        else:
2280            self.flash(msg)
2281        self.redirect(self.url(self.context))
2282        return
2283
2284    def render(self):
2285        return
2286
2287class StudentHistoryPage(KofaPage):
2288    """ Page to display student history
2289    """
2290    grok.context(IStudent)
2291    grok.name('history')
2292    grok.require('waeup.viewStudent')
2293    grok.template('studenthistory')
2294    pnav = 4
2295
2296    @property
2297    def label(self):
2298        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2299
2300# Pages for students only
2301
2302class StudentBaseEditFormPage(KofaEditFormPage):
2303    """ View to edit student base data
2304    """
2305    grok.context(IStudent)
2306    grok.name('edit_base')
2307    grok.require('waeup.handleStudent')
2308    form_fields = grok.AutoFields(IStudentBase).select(
2309        'email', 'phone')
2310    label = _('Edit base data')
2311    pnav = 4
2312
2313    @action(_('Save'), style='primary')
2314    def save(self, **data):
2315        msave(self, **data)
2316        return
2317
2318class StudentChangePasswordPage(KofaEditFormPage):
2319    """ View to edit student passwords
2320    """
2321    grok.context(IStudent)
2322    grok.name('change_password')
2323    grok.require('waeup.handleStudent')
2324    grok.template('change_password')
2325    label = _('Change password')
2326    pnav = 4
2327
2328    @action(_('Save'), style='primary')
2329    def save(self, **data):
2330        form = self.request.form
2331        password = form.get('change_password', None)
2332        password_ctl = form.get('change_password_repeat', None)
2333        if password:
2334            validator = getUtility(IPasswordValidator)
2335            errors = validator.validate_password(password, password_ctl)
2336            if not errors:
2337                IUserAccount(self.context).setPassword(password)
2338                # Unset temporary password
2339                self.context.temp_password = None
2340                self.context.writeLogMessage(self, 'saved: password')
2341                self.flash(_('Password changed.'))
2342            else:
2343                self.flash( ' '.join(errors), type="warning")
2344        return
2345
2346class StudentFilesUploadPage(KofaPage):
2347    """ View to upload files by student
2348    """
2349    grok.context(IStudent)
2350    grok.name('change_portrait')
2351    grok.require('waeup.uploadStudentFile')
2352    grok.template('filesuploadpage')
2353    label = _('Upload portrait')
2354    pnav = 4
2355
2356    def update(self):
2357        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2358        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2359            emit_lock_message(self)
2360            return
2361        super(StudentFilesUploadPage, self).update()
2362        return
2363
2364class StartClearancePage(KofaPage):
2365    grok.context(IStudent)
2366    grok.name('start_clearance')
2367    grok.require('waeup.handleStudent')
2368    grok.template('enterpin')
2369    label = _('Start clearance')
2370    ac_prefix = 'CLR'
2371    notice = ''
2372    pnav = 4
2373    buttonname = _('Start clearance now')
2374    with_ac = True
2375
2376    @property
2377    def all_required_fields_filled(self):
2378        if not self.context.email:
2379            return _("Email address is missing."), 'edit_base'
2380        if not self.context.phone:
2381            return _("Phone number is missing."), 'edit_base'
2382        return
2383
2384    @property
2385    def portrait_uploaded(self):
2386        store = getUtility(IExtFileStore)
2387        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2388            return True
2389        return False
2390
2391    def update(self, SUBMIT=None):
2392        if not self.context.state == ADMITTED:
2393            self.flash(_("Wrong state"), type="warning")
2394            self.redirect(self.url(self.context))
2395            return
2396        if not self.portrait_uploaded:
2397            self.flash(_("No portrait uploaded."), type="warning")
2398            self.redirect(self.url(self.context, 'change_portrait'))
2399            return
2400        if self.all_required_fields_filled:
2401            arf_warning = self.all_required_fields_filled[0]
2402            arf_redirect = self.all_required_fields_filled[1]
2403            self.flash(arf_warning, type="warning")
2404            self.redirect(self.url(self.context, arf_redirect))
2405            return
2406        if self.with_ac:
2407            self.ac_series = self.request.form.get('ac_series', None)
2408            self.ac_number = self.request.form.get('ac_number', None)
2409        if SUBMIT is None:
2410            return
2411        if self.with_ac:
2412            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2413            code = get_access_code(pin)
2414            if not code:
2415                self.flash(_('Activation code is invalid.'), type="warning")
2416                return
2417            if code.state == USED:
2418                self.flash(_('Activation code has already been used.'),
2419                           type="warning")
2420                return
2421            # Mark pin as used (this also fires a pin related transition)
2422            # and fire transition start_clearance
2423            comment = _(u"invalidated")
2424            # Here we know that the ac is in state initialized so we do not
2425            # expect an exception, but the owner might be different
2426            if not invalidate_accesscode(pin, comment, self.context.student_id):
2427                self.flash(_('You are not the owner of this access code.'),
2428                           type="warning")
2429                return
2430            self.context.clr_code = pin
2431        IWorkflowInfo(self.context).fireTransition('start_clearance')
2432        self.flash(_('Clearance process has been started.'))
2433        self.redirect(self.url(self.context,'cedit'))
2434        return
2435
2436class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2437    """ View to edit student clearance data by student
2438    """
2439    grok.context(IStudent)
2440    grok.name('cedit')
2441    grok.require('waeup.handleStudent')
2442    label = _('Edit clearance data')
2443
2444    @property
2445    def form_fields(self):
2446        if self.context.is_postgrad:
2447            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2448                'clr_code', 'officer_comment')
2449        else:
2450            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2451                'clr_code', 'officer_comment')
2452        return form_fields
2453
2454    def update(self):
2455        if self.context.clearance_locked:
2456            emit_lock_message(self)
2457            return
2458        return super(StudentClearanceEditFormPage, self).update()
2459
2460    @action(_('Save'), style='primary')
2461    def save(self, **data):
2462        self.applyData(self.context, **data)
2463        self.flash(_('Clearance form has been saved.'))
2464        return
2465
2466    def dataNotComplete(self):
2467        """To be implemented in the customization package.
2468        """
2469        return False
2470
2471    @action(_('Save and request clearance'), style='primary',
2472            warning=_('You can not edit your data after '
2473            'requesting clearance. You really want to request clearance now?'))
2474    def requestClearance(self, **data):
2475        self.applyData(self.context, **data)
2476        if self.dataNotComplete():
2477            self.flash(self.dataNotComplete(), type="warning")
2478            return
2479        self.flash(_('Clearance form has been saved.'))
2480        if self.context.clr_code:
2481            self.redirect(self.url(self.context, 'request_clearance'))
2482        else:
2483            # We bypass the request_clearance page if student
2484            # has been imported in state 'clearance started' and
2485            # no clr_code was entered before.
2486            state = IWorkflowState(self.context).getState()
2487            if state != CLEARANCE:
2488                # This shouldn't happen, but the application officer
2489                # might have forgotten to lock the form after changing the state
2490                self.flash(_('This form cannot be submitted. Wrong state!'),
2491                           type="danger")
2492                return
2493            IWorkflowInfo(self.context).fireTransition('request_clearance')
2494            self.flash(_('Clearance has been requested.'))
2495            self.redirect(self.url(self.context))
2496        return
2497
2498class RequestClearancePage(KofaPage):
2499    grok.context(IStudent)
2500    grok.name('request_clearance')
2501    grok.require('waeup.handleStudent')
2502    grok.template('enterpin')
2503    label = _('Request clearance')
2504    notice = _('Enter the CLR access code used for starting clearance.')
2505    ac_prefix = 'CLR'
2506    pnav = 4
2507    buttonname = _('Request clearance now')
2508    with_ac = True
2509
2510    def update(self, SUBMIT=None):
2511        if self.with_ac:
2512            self.ac_series = self.request.form.get('ac_series', None)
2513            self.ac_number = self.request.form.get('ac_number', None)
2514        if SUBMIT is None:
2515            return
2516        if self.with_ac:
2517            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2518            if self.context.clr_code and self.context.clr_code != pin:
2519                self.flash(_("This isn't your CLR access code."), type="danger")
2520                return
2521        state = IWorkflowState(self.context).getState()
2522        if state != CLEARANCE:
2523            # This shouldn't happen, but the application officer
2524            # might have forgotten to lock the form after changing the state
2525            self.flash(_('This form cannot be submitted. Wrong state!'),
2526                       type="danger")
2527            return
2528        IWorkflowInfo(self.context).fireTransition('request_clearance')
2529        self.flash(_('Clearance has been requested.'))
2530        self.redirect(self.url(self.context))
2531        return
2532
2533class StartSessionPage(KofaPage):
2534    grok.context(IStudentStudyCourse)
2535    grok.name('start_session')
2536    grok.require('waeup.handleStudent')
2537    grok.template('enterpin')
2538    label = _('Start session')
2539    ac_prefix = 'SFE'
2540    notice = ''
2541    pnav = 4
2542    buttonname = _('Start now')
2543    with_ac = True
2544
2545    def update(self, SUBMIT=None):
2546        if not self.context.is_current:
2547            emit_lock_message(self)
2548            return
2549        super(StartSessionPage, self).update()
2550        if not self.context.next_session_allowed:
2551            self.flash(_("You are not entitled to start session."),
2552                       type="warning")
2553            self.redirect(self.url(self.context))
2554            return
2555        if self.with_ac:
2556            self.ac_series = self.request.form.get('ac_series', None)
2557            self.ac_number = self.request.form.get('ac_number', None)
2558        if SUBMIT is None:
2559            return
2560        if self.with_ac:
2561            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2562            code = get_access_code(pin)
2563            if not code:
2564                self.flash(_('Activation code is invalid.'), type="warning")
2565                return
2566            # Mark pin as used (this also fires a pin related transition)
2567            if code.state == USED:
2568                self.flash(_('Activation code has already been used.'),
2569                           type="warning")
2570                return
2571            else:
2572                comment = _(u"invalidated")
2573                # Here we know that the ac is in state initialized so we do not
2574                # expect an error, but the owner might be different
2575                if not invalidate_accesscode(
2576                    pin,comment,self.context.student.student_id):
2577                    self.flash(_('You are not the owner of this access code.'),
2578                               type="warning")
2579                    return
2580        try:
2581            if self.context.student.state == CLEARED:
2582                IWorkflowInfo(self.context.student).fireTransition(
2583                    'pay_first_school_fee')
2584            elif self.context.student.state == RETURNING:
2585                IWorkflowInfo(self.context.student).fireTransition(
2586                    'pay_school_fee')
2587            elif self.context.student.state == PAID:
2588                IWorkflowInfo(self.context.student).fireTransition(
2589                    'pay_pg_fee')
2590        except ConstraintNotSatisfied:
2591            self.flash(_('An error occurred, please contact the system administrator.'),
2592                       type="danger")
2593            return
2594        self.flash(_('Session started.'))
2595        self.redirect(self.url(self.context))
2596        return
2597
2598class AddStudyLevelFormPage(KofaEditFormPage):
2599    """ Page for students to add current study levels
2600    """
2601    grok.context(IStudentStudyCourse)
2602    grok.name('add')
2603    grok.require('waeup.handleStudent')
2604    grok.template('studyleveladdpage')
2605    form_fields = grok.AutoFields(IStudentStudyCourse)
2606    pnav = 4
2607
2608    @property
2609    def label(self):
2610        studylevelsource = StudyLevelSource().factory
2611        code = self.context.current_level
2612        title = studylevelsource.getTitle(self.context, code)
2613        return _('Add current level ${a}', mapping = {'a':title})
2614
2615    def update(self):
2616        if not self.context.is_current:
2617            emit_lock_message(self)
2618            return
2619        if self.context.student.state != PAID:
2620            emit_lock_message(self)
2621            return
2622        code = self.context.current_level
2623        if code is None:
2624            self.flash(_('Your data are incomplete'), type="danger")
2625            self.redirect(self.url(self.context))
2626            return
2627        super(AddStudyLevelFormPage, self).update()
2628        return
2629
2630    @action(_('Create course list now'), style='primary')
2631    def addStudyLevel(self, **data):
2632        studylevel = createObject(u'waeup.StudentStudyLevel')
2633        studylevel.level = self.context.current_level
2634        studylevel.level_session = self.context.current_session
2635        try:
2636            self.context.addStudentStudyLevel(
2637                self.context.certificate,studylevel)
2638        except KeyError:
2639            self.flash(_('This level exists.'), type="warning")
2640            self.redirect(self.url(self.context))
2641            return
2642        except RequiredMissing:
2643            self.flash(_('Your data are incomplete.'), type="danger")
2644            self.redirect(self.url(self.context))
2645            return
2646        self.flash(_('You successfully created a new course list.'))
2647        self.redirect(self.url(self.context, str(studylevel.level)))
2648        return
2649
2650class StudyLevelEditFormPage(KofaEditFormPage):
2651    """ Page to edit the student study level data by students
2652    """
2653    grok.context(IStudentStudyLevel)
2654    grok.name('edit')
2655    grok.require('waeup.editStudyLevel')
2656    grok.template('studyleveleditpage')
2657    pnav = 4
2658    placeholder = _('Enter valid course code')
2659
2660    def update(self, ADD=None, course=None):
2661        if not self.context.__parent__.is_current:
2662            emit_lock_message(self)
2663            return
2664        if self.context.student.state != PAID or \
2665            not self.context.is_current_level:
2666            emit_lock_message(self)
2667            return
2668        super(StudyLevelEditFormPage, self).update()
2669        if ADD is not None:
2670            if not course:
2671                self.flash(_('No valid course code entered.'), type="warning")
2672                return
2673            cat = queryUtility(ICatalog, name='courses_catalog')
2674            result = cat.searchResults(code=(course, course))
2675            if len(result) != 1:
2676                self.flash(_('Course not found.'), type="warning")
2677                return
2678            course = list(result)[0]
2679            addCourseTicket(self, course)
2680        return
2681
2682    @property
2683    def label(self):
2684        # Here we know that the cookie has been set
2685        lang = self.request.cookies.get('kofa.language')
2686        level_title = translate(self.context.level_title, 'waeup.kofa',
2687            target_language=lang)
2688        return _('Edit course list of ${a}',
2689            mapping = {'a':level_title})
2690
2691    @property
2692    def translated_values(self):
2693        return translated_values(self)
2694
2695    def _delCourseTicket(self, **data):
2696        form = self.request.form
2697        if 'val_id' in form:
2698            child_id = form['val_id']
2699        else:
2700            self.flash(_('No ticket selected.'), type="warning")
2701            self.redirect(self.url(self.context, '@@edit'))
2702            return
2703        if not isinstance(child_id, list):
2704            child_id = [child_id]
2705        deleted = []
2706        for id in child_id:
2707            # Students are not allowed to remove core tickets
2708            if id in self.context and \
2709                self.context[id].removable_by_student:
2710                del self.context[id]
2711                deleted.append(id)
2712        if len(deleted):
2713            self.flash(_('Successfully removed: ${a}',
2714                mapping = {'a':', '.join(deleted)}))
2715            self.context.writeLogMessage(
2716                self,'removed: %s at %s' %
2717                (', '.join(deleted), self.context.level))
2718        self.redirect(self.url(self.context, u'@@edit'))
2719        return
2720
2721    @jsaction(_('Remove selected tickets'))
2722    def delCourseTicket(self, **data):
2723        self._delCourseTicket(**data)
2724        return
2725
2726    def _registerCourses(self, **data):
2727        if self.context.student.is_postgrad and \
2728            not self.context.student.is_special_postgrad:
2729            self.flash(_(
2730                "You are a postgraduate student, "
2731                "your course list can't bee registered."), type="warning")
2732            self.redirect(self.url(self.context))
2733            return
2734        students_utils = getUtility(IStudentsUtils)
2735        warning = students_utils.warnCreditsOOR(self.context)
2736        if warning:
2737            self.flash(warning, type="warning")
2738            return
2739        msg = self.context.course_registration_forbidden
2740        if msg:
2741            self.flash(msg, type="warning")
2742            return
2743        IWorkflowInfo(self.context.student).fireTransition(
2744            'register_courses')
2745        self.flash(_('Course list has been registered.'))
2746        self.redirect(self.url(self.context))
2747        return
2748
2749    @action(_('Register course list'), style='primary',
2750        warning=_('You can not edit your course list after registration.'
2751            ' You really want to register?'))
2752    def registerCourses(self, **data):
2753        self._registerCourses(**data)
2754        return
2755
2756class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2757    """Add a course ticket by student.
2758    """
2759    grok.name('ctadd')
2760    grok.require('waeup.handleStudent')
2761    form_fields = grok.AutoFields(ICourseTicketAdd)
2762
2763    def update(self):
2764        if self.context.student.state != PAID or \
2765            not self.context.is_current_level:
2766            emit_lock_message(self)
2767            return
2768        super(CourseTicketAddFormPage2, self).update()
2769        return
2770
2771    @action(_('Add course ticket'))
2772    def addCourseTicket(self, **data):
2773        # Safety belt
2774        if self.context.student.state != PAID:
2775            return
2776        course = data['course']
2777        success = addCourseTicket(self, course)
2778        if success:
2779            self.redirect(self.url(self.context, u'@@edit'))
2780        return
2781
2782class SetPasswordPage(KofaPage):
2783    grok.context(IKofaObject)
2784    grok.name('setpassword')
2785    grok.require('waeup.Anonymous')
2786    grok.template('setpassword')
2787    label = _('Set password for first-time login')
2788    ac_prefix = 'PWD'
2789    pnav = 0
2790    set_button = _('Set')
2791
2792    def update(self, SUBMIT=None):
2793        self.reg_number = self.request.form.get('reg_number', None)
2794        self.ac_series = self.request.form.get('ac_series', None)
2795        self.ac_number = self.request.form.get('ac_number', None)
2796
2797        if SUBMIT is None:
2798            return
2799        hitlist = search(query=self.reg_number,
2800            searchtype='reg_number', view=self)
2801        if not hitlist:
2802            self.flash(_('No student found.'), type="warning")
2803            return
2804        if len(hitlist) != 1:   # Cannot happen but anyway
2805            self.flash(_('More than one student found.'), type="warning")
2806            return
2807        student = hitlist[0].context
2808        self.student_id = student.student_id
2809        student_pw = student.password
2810        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2811        code = get_access_code(pin)
2812        if not code:
2813            self.flash(_('Access code is invalid.'), type="warning")
2814            return
2815        if student_pw and pin == student.adm_code:
2816            self.flash(_(
2817                'Password has already been set. Your Student Id is ${a}',
2818                mapping = {'a':self.student_id}))
2819            return
2820        elif student_pw:
2821            self.flash(
2822                _('Password has already been set. You are using the ' +
2823                'wrong Access Code.'), type="warning")
2824            return
2825        # Mark pin as used (this also fires a pin related transition)
2826        # and set student password
2827        if code.state == USED:
2828            self.flash(_('Access code has already been used.'), type="warning")
2829            return
2830        else:
2831            comment = _(u"invalidated")
2832            # Here we know that the ac is in state initialized so we do not
2833            # expect an exception
2834            invalidate_accesscode(pin,comment)
2835            IUserAccount(student).setPassword(self.ac_number)
2836            student.adm_code = pin
2837        self.flash(_('Password has been set. Your Student Id is ${a}',
2838            mapping = {'a':self.student_id}))
2839        return
2840
2841class StudentRequestPasswordPage(KofaAddFormPage):
2842    """Captcha'd request password page for students.
2843    """
2844    grok.name('requestpw')
2845    grok.require('waeup.Anonymous')
2846    grok.template('requestpw')
2847    form_fields = grok.AutoFields(IStudentRequestPW).select(
2848        'lastname','number','email')
2849    label = _('Request password for first-time login')
2850
2851    def update(self):
2852        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2853        if blocker:
2854            self.flash(_('The portal is in maintenance mode. '
2855                        'Password request forms are temporarily disabled.'),
2856                       type='warning')
2857            self.redirect(self.url(self.context))
2858            return
2859        # Handle captcha
2860        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2861        self.captcha_result = self.captcha.verify(self.request)
2862        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2863        return
2864
2865    def _redirect(self, email, password, student_id):
2866        # Forward only email to landing page in base package.
2867        self.redirect(self.url(self.context, 'requestpw_complete',
2868            data = dict(email=email)))
2869        return
2870
2871    def _redirect_no_student(self):
2872        # No record found, this is the truth. We do not redirect here.
2873        # We are using this method in custom packages
2874        # for redirecting alumni to the application section.
2875        self.flash(_('No student record found.'), type="warning")
2876        return
2877
2878    def _pw_used(self):
2879        # XXX: False if password has not been used. We need an extra
2880        #      attribute which remembers if student logged in.
2881        return True
2882
2883    @action(_('Send login credentials to email address'), style='primary')
2884    def get_credentials(self, **data):
2885        if not self.captcha_result.is_valid:
2886            # Captcha will display error messages automatically.
2887            # No need to flash something.
2888            return
2889        number = data.get('number','')
2890        lastname = data.get('lastname','')
2891        cat = getUtility(ICatalog, name='students_catalog')
2892        results = list(
2893            cat.searchResults(reg_number=(number, number)))
2894        if not results:
2895            results = list(
2896                cat.searchResults(matric_number=(number, number)))
2897        if results:
2898            student = results[0]
2899            if getattr(student,'lastname',None) is None:
2900                self.flash(_('An error occurred.'), type="danger")
2901                return
2902            elif student.lastname.lower() != lastname.lower():
2903                # Don't tell the truth here. Anonymous must not
2904                # know that a record was found and only the lastname
2905                # verification failed.
2906                self.flash(_('No student record found.'), type="warning")
2907                return
2908            elif student.password is not None and self._pw_used:
2909                self.flash(_('Your password has already been set and used. '
2910                             'Please proceed to the login page.'),
2911                           type="warning")
2912                return
2913            # Store email address but nothing else.
2914            student.email = data['email']
2915            notify(grok.ObjectModifiedEvent(student))
2916        else:
2917            self._redirect_no_student()
2918            return
2919
2920        kofa_utils = getUtility(IKofaUtils)
2921        password = kofa_utils.genPassword()
2922        mandate = PasswordMandate()
2923        mandate.params['password'] = password
2924        mandate.params['user'] = student
2925        site = grok.getSite()
2926        site['mandates'].addMandate(mandate)
2927        # Send email with credentials
2928        args = {'mandate_id':mandate.mandate_id}
2929        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2930        url_info = u'Confirmation link: %s' % mandate_url
2931        msg = _('You have successfully requested a password for the')
2932        if kofa_utils.sendCredentials(IUserAccount(student),
2933            password, url_info, msg):
2934            email_sent = student.email
2935        else:
2936            email_sent = None
2937        self._redirect(email=email_sent, password=password,
2938            student_id=student.student_id)
2939        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2940        self.context.logger.info(
2941            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2942        return
2943
2944class StudentRequestPasswordEmailSent(KofaPage):
2945    """Landing page after successful password request.
2946
2947    """
2948    grok.name('requestpw_complete')
2949    grok.require('waeup.Public')
2950    grok.template('requestpwmailsent')
2951    label = _('Your password request was successful.')
2952
2953    def update(self, email=None, student_id=None, password=None):
2954        self.email = email
2955        self.password = password
2956        self.student_id = student_id
2957        return
2958
2959class FilterStudentsInDepartmentPage(KofaPage):
2960    """Page that filters and lists students.
2961    """
2962    grok.context(IDepartment)
2963    grok.require('waeup.showStudents')
2964    grok.name('students')
2965    grok.template('filterstudentspage')
2966    pnav = 1
2967    session_label = _('Current Session')
2968    level_label = _('Current Level')
2969
2970    def label(self):
2971        return 'Students in %s' % self.context.longtitle
2972
2973    def _set_session_values(self):
2974        vocab_terms = academic_sessions_vocab.by_value.values()
2975        self.sessions = sorted(
2976            [(x.title, x.token) for x in vocab_terms], reverse=True)
2977        self.sessions += [('All Sessions', 'all')]
2978        return
2979
2980    def _set_level_values(self):
2981        vocab_terms = course_levels.by_value.values()
2982        self.levels = sorted(
2983            [(x.title, x.token) for x in vocab_terms])
2984        self.levels += [('All Levels', 'all')]
2985        return
2986
2987    def _searchCatalog(self, session, level):
2988        if level not in (10, 999, None):
2989            start_level = 100 * (level // 100)
2990            end_level = start_level + 90
2991        else:
2992            start_level = end_level = level
2993        cat = queryUtility(ICatalog, name='students_catalog')
2994        students = cat.searchResults(
2995            current_session=(session, session),
2996            current_level=(start_level, end_level),
2997            depcode=(self.context.code, self.context.code)
2998            )
2999        hitlist = []
3000        for student in students:
3001            hitlist.append(StudentQueryResultItem(student, view=self))
3002        return hitlist
3003
3004    def update(self, SHOW=None, session=None, level=None):
3005        self.parent_url = self.url(self.context.__parent__)
3006        self._set_session_values()
3007        self._set_level_values()
3008        self.hitlist = []
3009        self.session_default = session
3010        self.level_default = level
3011        if SHOW is not None:
3012            if session != 'all':
3013                self.session = int(session)
3014                self.session_string = '%s %s/%s' % (
3015                    self.session_label, self.session, self.session+1)
3016            else:
3017                self.session = None
3018                self.session_string = _('in any session')
3019            if level != 'all':
3020                self.level = int(level)
3021                self.level_string = '%s %s' % (self.level_label, self.level)
3022            else:
3023                self.level = None
3024                self.level_string = _('at any level')
3025            self.hitlist = self._searchCatalog(self.session, self.level)
3026            if not self.hitlist:
3027                self.flash(_('No student found.'), type="warning")
3028        return
3029
3030class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3031    """Page that filters and lists students.
3032    """
3033    grok.context(ICertificate)
3034
3035    def label(self):
3036        return 'Students studying %s' % self.context.longtitle
3037
3038    def _searchCatalog(self, session, level):
3039        if level not in (10, 999, None):
3040            start_level = 100 * (level // 100)
3041            end_level = start_level + 90
3042        else:
3043            start_level = end_level = level
3044        cat = queryUtility(ICatalog, name='students_catalog')
3045        students = cat.searchResults(
3046            current_session=(session, session),
3047            current_level=(start_level, end_level),
3048            certcode=(self.context.code, self.context.code)
3049            )
3050        hitlist = []
3051        for student in students:
3052            hitlist.append(StudentQueryResultItem(student, view=self))
3053        return hitlist
3054
3055class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3056    """Page that filters and lists students.
3057    """
3058    grok.context(ICourse)
3059    grok.require('waeup.viewStudent')
3060
3061    session_label = _('Session')
3062    level_label = _('Level')
3063
3064    def label(self):
3065        return 'Students registered for %s' % self.context.longtitle
3066
3067    def _searchCatalog(self, session, level):
3068        if level not in (10, 999, None):
3069            start_level = 100 * (level // 100)
3070            end_level = start_level + 90
3071        else:
3072            start_level = end_level = level
3073        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3074        coursetickets = cat.searchResults(
3075            session=(session, session),
3076            level=(start_level, end_level),
3077            code=(self.context.code, self.context.code)
3078            )
3079        hitlist = []
3080        for ticket in coursetickets:
3081            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3082        return list(set(hitlist))
3083
3084class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3085    """ Clear all students of a department in state 'clearance requested'.
3086    """
3087    grok.context(IDepartment)
3088    grok.name('clearallstudents')
3089    grok.require('waeup.clearAllStudents')
3090
3091    def update(self):
3092        cat = queryUtility(ICatalog, name='students_catalog')
3093        students = cat.searchResults(
3094            depcode=(self.context.code, self.context.code),
3095            state=(REQUESTED, REQUESTED)
3096            )
3097        num = 0
3098        for student in students:
3099            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3100                continue
3101            IWorkflowInfo(student).fireTransition('clear')
3102            num += 1
3103        self.flash(_('%d students have been cleared.' % num))
3104        self.redirect(self.url(self.context))
3105        return
3106
3107    def render(self):
3108        return
3109
3110
3111class EditScoresPage(KofaPage):
3112    """Page that allows to edit batches of scores.
3113    """
3114    grok.context(ICourse)
3115    grok.require('waeup.editScores')
3116    grok.name('edit_scores')
3117    grok.template('editscorespage')
3118    pnav = 1
3119    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3120
3121    def label(self):
3122        return '%s tickets in academic session %s' % (
3123            self.context.code, self.session_title)
3124
3125    def _searchCatalog(self, session):
3126        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3127        coursetickets = cat.searchResults(
3128            session=(session, session),
3129            code=(self.context.code, self.context.code)
3130            )
3131        return list(coursetickets)
3132
3133    def _extract_uploadfile(self, uploadfile):
3134        """Get a mapping of student-ids to scores.
3135
3136        The mapping is constructed by reading contents from `uploadfile`.
3137
3138        We expect uploadfile to be a regular CSV file with columns
3139        ``student_id`` and ``score`` (other cols are ignored).
3140        """
3141        result = dict()
3142        data = StringIO(uploadfile.read())  # ensure we have something seekable
3143        reader = csv.DictReader(data)
3144        for row in reader:
3145            if not 'student_id' in row or not 'score' in row:
3146                continue
3147            result[row['student_id']] = row['score']
3148        return result
3149
3150    def _update_scores(self, form):
3151        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3152        error = ''
3153        if 'UPDATE_FILE' in form:
3154            if form['uploadfile']:
3155                try:
3156                    formvals = self._extract_uploadfile(form['uploadfile'])
3157                except:
3158                    self.flash(
3159                        _('Uploaded file contains illegal data. Ignored'),
3160                        type="danger")
3161                    return False
3162            else:
3163                self.flash(
3164                    _('No file provided.'), type="danger")
3165                return False
3166        else:
3167            formvals = dict(zip(form['sids'], form['scores']))
3168        for ticket in self.editable_tickets:
3169            score = ticket.score
3170            sid = ticket.student.student_id
3171            if sid not in formvals:
3172                continue
3173            if formvals[sid] == '':
3174                score = None
3175            else:
3176                try:
3177                    score = int(formvals[sid])
3178                except ValueError:
3179                    error += '%s, ' % ticket.student.display_fullname
3180            if ticket.score != score:
3181                ticket.score = score
3182                ticket.student.__parent__.logger.info(
3183                    '%s - %s %s/%s score updated (%s)' % (
3184                        ob_class, ticket.student.student_id,
3185                        ticket.level, ticket.code, score)
3186                    )
3187        if error:
3188            self.flash(
3189                _('Error: Score(s) of following students have not been '
3190                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3191                type="danger")
3192        return True
3193
3194    def update(self,  *args, **kw):
3195        form = self.request.form
3196        self.current_academic_session = grok.getSite()[
3197            'configuration'].current_academic_session
3198        if self.context.__parent__.__parent__.score_editing_disabled:
3199            self.flash(_('Score editing disabled.'), type="warning")
3200            self.redirect(self.url(self.context))
3201            return
3202        if not self.current_academic_session:
3203            self.flash(_('Current academic session not set.'), type="warning")
3204            self.redirect(self.url(self.context))
3205            return
3206        self.session_title = academic_sessions_vocab.getTerm(
3207            self.current_academic_session).title
3208        self.tickets = self._searchCatalog(self.current_academic_session)
3209        if not self.tickets:
3210            self.flash(_('No student found.'), type="warning")
3211            self.redirect(self.url(self.context))
3212            return
3213        self.editable_tickets = [
3214            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3215        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3216            return
3217        if not self.editable_tickets:
3218            return
3219        success = self._update_scores(form)
3220        if success:
3221            self.flash(_('You successfully updated course results.'))
3222        return
3223
3224
3225class DownloadScoresView(UtilityView, grok.View):
3226    """View that exports scores.
3227    """
3228    grok.context(ICourse)
3229    grok.require('waeup.editScores')
3230    grok.name('download_scores')
3231
3232    def update(self):
3233        self.current_academic_session = grok.getSite()[
3234            'configuration'].current_academic_session
3235        if self.context.__parent__.__parent__.score_editing_disabled:
3236            self.flash(_('Score editing disabled.'), type="warning")
3237            self.redirect(self.url(self.context))
3238            return
3239        if not self.current_academic_session:
3240            self.flash(_('Current academic session not set.'), type="warning")
3241            self.redirect(self.url(self.context))
3242            return
3243        site = grok.getSite()
3244        exporter = getUtility(ICSVExporter, name='lecturer')
3245        self.csv = exporter.export_filtered(site, filepath=None,
3246                                 catalog='coursetickets',
3247                                 session=self.current_academic_session,
3248                                 level=None,
3249                                 code=self.context.code)
3250        return
3251
3252    def render(self):
3253        filename = 'results_%s_%s.csv' % (
3254            self.context.code, self.current_academic_session)
3255        self.response.setHeader(
3256            'Content-Type', 'text/csv; charset=UTF-8')
3257        self.response.setHeader(
3258            'Content-Disposition:', 'attachment; filename="%s' % filename)
3259        return self.csv
3260
3261class ExportPDFScoresSlip(UtilityView, grok.View,
3262    LocalRoleAssignmentUtilityView):
3263    """Deliver a PDF slip of course tickets for a lecturer.
3264    """
3265    grok.context(ICourse)
3266    grok.name('coursetickets.pdf')
3267    grok.require('waeup.editScores')
3268
3269    def data(self, session):
3270        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3271        coursetickets = cat.searchResults(
3272            session=(session, session),
3273            code=(self.context.code, self.context.code)
3274            )
3275        header = [[_('Matric No.'),
3276                   _('Reg. No.'),
3277                   _('Fullname'),
3278                   _('Status'),
3279                   _('Course of Studies'),
3280                   _('Level'),
3281                   _('Score') ],]
3282        tickets = []
3283        for ticket in list(coursetickets):
3284            row = [ticket.student.matric_number,
3285                  ticket.student.reg_number,
3286                  ticket.student.display_fullname,
3287                  ticket.student.translated_state,
3288                  ticket.student.certcode,
3289                  ticket.level,
3290                  ticket.score]
3291            tickets.append(row)
3292        return header + sorted(tickets, key=lambda value: value[0]), None
3293
3294    def render(self):
3295        session = grok.getSite()['configuration'].current_academic_session
3296        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3297                     if i['local_role'] == 'waeup.local.Lecturer']
3298        lecturers =  ', '.join(lecturers)
3299        students_utils = getUtility(IStudentsUtils)
3300        return students_utils.renderPDFCourseticketsOverview(
3301            self, session, self.data(session), lecturers)
3302
3303class ExportJobContainerOverview(KofaPage):
3304    """Page that lists active student data export jobs and provides links
3305    to discard or download CSV files.
3306
3307    """
3308    grok.context(VirtualExportJobContainer)
3309    grok.require('waeup.showStudents')
3310    grok.name('index.html')
3311    grok.template('exportjobsindex')
3312    label = _('Student Data Exports')
3313    pnav = 1
3314    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3315
3316    def update(self, CREATE=None, DISCARD=None, job_id=None):
3317        if CREATE:
3318            self.redirect(self.url('@@exportconfig'))
3319            return
3320        if DISCARD and job_id:
3321            entry = self.context.entry_from_job_id(job_id)
3322            self.context.delete_export_entry(entry)
3323            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3324            self.context.logger.info(
3325                '%s - discarded: job_id=%s' % (ob_class, job_id))
3326            self.flash(_('Discarded export') + ' %s' % job_id)
3327        self.entries = doll_up(self, user=self.request.principal.id)
3328        return
3329
3330class ExportJobContainerJobConfig(KofaPage):
3331    """Page that configures a students export job.
3332
3333    This is a baseclass.
3334    """
3335    grok.baseclass()
3336    grok.name('exportconfig')
3337    grok.require('waeup.showStudents')
3338    grok.template('exportconfig')
3339    label = _('Configure student data export')
3340    pnav = 1
3341    redirect_target = ''
3342    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3343
3344    def _set_session_values(self):
3345        vocab_terms = academic_sessions_vocab.by_value.values()
3346        self.sessions = sorted(
3347            [(x.title, x.token) for x in vocab_terms], reverse=True)
3348        self.sessions += [(_('All Sessions'), 'all')]
3349        return
3350
3351    def _set_level_values(self):
3352        vocab_terms = course_levels.by_value.values()
3353        self.levels = sorted(
3354            [(x.title, x.token) for x in vocab_terms])
3355        self.levels += [(_('All Levels'), 'all')]
3356        return
3357
3358    def _set_mode_values(self):
3359        utils = getUtility(IKofaUtils)
3360        self.modes = sorted([(value, key) for key, value in
3361                      utils.STUDY_MODES_DICT.items()])
3362        self.modes +=[(_('All Modes'), 'all')]
3363        return
3364
3365    def _set_exporter_values(self):
3366        # We provide all student exporters, nothing else, yet.
3367        # Bursary or Department Officers don't have the general exportData
3368        # permission and are only allowed to export bursary or payments
3369        # overview data respectively. This is the only place where
3370        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3371        # are used.
3372        exporters = []
3373        if not checkPermission('waeup.exportData', self.context):
3374            if checkPermission('waeup.exportBursaryData', self.context):
3375                exporters += [('Bursary Data', 'bursary')]
3376            if checkPermission('waeup.exportPaymentsOverview', self.context):
3377                exporters += [('Student Payments Overview', 'paymentsoverview')]
3378            self.exporters = exporters
3379            return
3380        STUDENT_EXPORTER_NAMES = getUtility(
3381            IStudentsUtils).STUDENT_EXPORTER_NAMES
3382        for name in STUDENT_EXPORTER_NAMES:
3383            util = getUtility(ICSVExporter, name=name)
3384            exporters.append((util.title, name),)
3385        self.exporters = exporters
3386        return
3387
3388    @property
3389    def faccode(self):
3390        return None
3391
3392    @property
3393    def depcode(self):
3394        return None
3395
3396    @property
3397    def certcode(self):
3398        return None
3399
3400    def update(self, START=None, session=None, level=None, mode=None,
3401               payments_start=None, payments_end=None,
3402               exporter=None):
3403        self._set_session_values()
3404        self._set_level_values()
3405        self._set_mode_values()
3406        self._set_exporter_values()
3407        if START is None:
3408            return
3409        ena = exports_not_allowed(self)
3410        if ena:
3411            self.flash(ena, type='danger')
3412            return
3413        if payments_start or payments_end:
3414            date_format = '%d/%m/%Y'
3415            try:
3416                datetime.strptime(payments_start, date_format)
3417                datetime.strptime(payments_end, date_format)
3418            except ValueError:
3419                self.flash(_('Payment dates do not match format d/m/Y.'),
3420                           type="danger")
3421                return
3422        if session == 'all':
3423            session=None
3424        if level == 'all':
3425            level = None
3426        if mode == 'all':
3427            mode = None
3428        if payments_start == '':
3429            payments_start = None
3430        if payments_end == '':
3431            payments_end = None
3432        if (mode,
3433            level,
3434            session,
3435            self.faccode,
3436            self.depcode,
3437            self.certcode) == (None, None, None, None, None, None):
3438            # Export all students including those without certificate
3439            if payments_start:
3440                job_id = self.context.start_export_job(exporter,
3441                                              self.request.principal.id,
3442                                              payments_start = payments_start,
3443                                              payments_end = payments_end)
3444            else:
3445                job_id = self.context.start_export_job(exporter,
3446                                              self.request.principal.id)
3447        else:
3448            if payments_start:
3449                job_id = self.context.start_export_job(exporter,
3450                                              self.request.principal.id,
3451                                              current_session=session,
3452                                              current_level=level,
3453                                              current_mode=mode,
3454                                              faccode=self.faccode,
3455                                              depcode=self.depcode,
3456                                              certcode=self.certcode,
3457                                              payments_start = payments_start,
3458                                              payments_end = payments_end)
3459            else:
3460                job_id = self.context.start_export_job(exporter,
3461                                              self.request.principal.id,
3462                                              current_session=session,
3463                                              current_level=level,
3464                                              current_mode=mode,
3465                                              faccode=self.faccode,
3466                                              depcode=self.depcode,
3467                                              certcode=self.certcode)
3468        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3469        self.context.logger.info(
3470            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3471            % (ob_class, exporter, session, level, mode, self.faccode,
3472            self.depcode, self.certcode, payments_start, payments_end, job_id))
3473        self.flash(_('Export started for students with') +
3474                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3475                   session, level, mode))
3476        self.redirect(self.url(self.redirect_target))
3477        return
3478
3479class ExportJobContainerDownload(ExportCSVView):
3480    """Page that downloads a students export csv file.
3481
3482    """
3483    grok.context(VirtualExportJobContainer)
3484    grok.require('waeup.showStudents')
3485
3486class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3487    """Page that configures a students export job in datacenter.
3488
3489    """
3490    grok.context(IDataCenter)
3491    redirect_target = '@@export'
3492
3493class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3494    """Page that configures a students export job in datacenter.
3495
3496    """
3497    grok.name('exportselected')
3498    grok.context(IDataCenter)
3499    redirect_target = '@@export'
3500    grok.template('exportselected')
3501    label = _('Configure student data export')
3502
3503    def update(self, START=None, students=None, exporter=None):
3504        self._set_exporter_values()
3505        if START is None:
3506            return
3507        ena = exports_not_allowed(self)
3508        if ena:
3509            self.flash(ena, type='danger')
3510            return
3511        try:
3512            ids = students.replace(',', ' ').split()
3513        except:
3514            self.flash(sys.exc_info()[1])
3515            self.redirect(self.url(self.redirect_target))
3516            return
3517        job_id = self.context.start_export_job(
3518            exporter, self.request.principal.id, selected=ids)
3519        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3520        self.context.logger.info(
3521            '%s - selected students exported: %s, job_id=%s' %
3522            (ob_class, exporter, job_id))
3523        self.flash(_('Export of selected students started.'))
3524        self.redirect(self.url(self.redirect_target))
3525        return
3526
3527class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3528    """Page that configures a students export job in facultiescontainer.
3529
3530    """
3531    grok.context(VirtualFacultiesExportJobContainer)
3532
3533
3534class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3535    """Page that configures a students export job in faculties.
3536
3537    """
3538    grok.context(VirtualFacultyExportJobContainer)
3539
3540    @property
3541    def faccode(self):
3542        return self.context.__parent__.code
3543
3544class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3545    """Page that configures a students export job in departments.
3546
3547    """
3548    grok.context(VirtualDepartmentExportJobContainer)
3549
3550    @property
3551    def depcode(self):
3552        return self.context.__parent__.code
3553
3554class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3555    """Page that configures a students export job for certificates.
3556
3557    """
3558    grok.context(VirtualCertificateExportJobContainer)
3559    grok.template('exportconfig_certificate')
3560
3561    @property
3562    def certcode(self):
3563        return self.context.__parent__.code
3564
3565class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3566    """Page that configures a students export job for courses.
3567
3568    In contrast to department or certificate student data exports the
3569    coursetickets_catalog is searched here. Therefore the update
3570    method from the base class is customized.
3571    """
3572    grok.context(VirtualCourseExportJobContainer)
3573    grok.template('exportconfig_course')
3574
3575    def _set_exporter_values(self):
3576        # We provide only the 'coursetickets' and 'lecturer' exporter
3577        # but can add more.
3578        exporters = []
3579        for name in ('coursetickets', 'lecturer'):
3580            util = getUtility(ICSVExporter, name=name)
3581            exporters.append((util.title, name),)
3582        self.exporters = exporters
3583
3584    def _set_session_values(self):
3585        # We allow only current academic session
3586        academic_session = grok.getSite()['configuration'].current_academic_session
3587        if not academic_session:
3588            self.sessions = []
3589            return
3590        x = academic_sessions_vocab.getTerm(academic_session)
3591        self.sessions = [(x.title, x.token)]
3592        return
3593
3594    def update(self, START=None, session=None, level=None, mode=None,
3595               exporter=None):
3596        self._set_session_values()
3597        self._set_level_values()
3598        self._set_mode_values()
3599        self._set_exporter_values()
3600        if not self.sessions:
3601            self.flash(
3602                _('Academic session not set. '
3603                  'Please contact the administrator.'),
3604                type='danger')
3605            self.redirect(self.url(self.context))
3606            return
3607        if START is None:
3608            return
3609        ena = exports_not_allowed(self)
3610        if ena:
3611            self.flash(ena, type='danger')
3612            return
3613        if session == 'all':
3614            session = None
3615        if level == 'all':
3616            level = None
3617        job_id = self.context.start_export_job(exporter,
3618                                      self.request.principal.id,
3619                                      # Use a different catalog and
3620                                      # pass different keywords than
3621                                      # for the (default) students_catalog
3622                                      catalog='coursetickets',
3623                                      session=session,
3624                                      level=level,
3625                                      code=self.context.__parent__.code)
3626        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3627        self.context.logger.info(
3628            '%s - exported: %s (%s, %s, %s), job_id=%s'
3629            % (ob_class, exporter, session, level,
3630            self.context.__parent__.code, job_id))
3631        self.flash(_('Export started for course tickets with') +
3632                   ' level_session=%s, level=%s' % (
3633                   session, level))
3634        self.redirect(self.url(self.redirect_target))
3635        return
Note: See TracBrowser for help on using the repository browser.