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

Last change on this file since 13935 was 13935, checked in by uli, 8 years ago

Merge changes from uli-scores-upload back into trunk.

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