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

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

Show warning before requesting clearance.

  • Property svn:keywords set to Id
File size: 133.3 KB
Line 
1## $Id: browser.py 14102 2016-08-22 09:04:28Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""UI components for students and related components.
19"""
20import csv
21import grok
22import pytz
23import sys
24from cStringIO import StringIO
25from datetime import datetime
26from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
27from urllib import urlencode
28from zope.catalog.interfaces import ICatalog
29from zope.component import queryUtility, getUtility, createObject
30from zope.event import notify
31from zope.formlib.textwidgets import BytesDisplayWidget
32from zope.i18n import translate
33from zope.schema.interfaces import ConstraintNotSatisfied, RequiredMissing
34from zope.security import checkPermission
35from waeup.kofa.accesscodes import invalidate_accesscode, get_access_code
36from waeup.kofa.accesscodes.workflow import USED
37from waeup.kofa.browser.breadcrumbs import Breadcrumb
38from waeup.kofa.browser.interfaces import ICaptchaManager
39from waeup.kofa.browser.layout import (
40    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
41    NullValidator, jsaction, action, UtilityView)
42from waeup.kofa.browser.pages import (
43    ContactAdminFormPage, ExportCSVView, doll_up, exports_not_allowed,
44    LocalRoleAssignmentUtilityView)
45from waeup.kofa.hostels.hostel import NOT_OCCUPIED
46from waeup.kofa.interfaces import (
47    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
48    IKofaUtils, IObjectHistory, academic_sessions, ICSVExporter,
49    academic_sessions_vocab, IDataCenter, DOCLINK)
50from waeup.kofa.interfaces import MessageFactory as _
51from waeup.kofa.mandates.mandate import PasswordMandate
52from waeup.kofa.university.interfaces import (
53    IDepartment, ICertificate, ICourse)
54from waeup.kofa.university.certificate import (
55    VirtualCertificateExportJobContainer)
56from waeup.kofa.university.department import (
57    VirtualDepartmentExportJobContainer)
58from waeup.kofa.university.faculty import VirtualFacultyExportJobContainer
59from waeup.kofa.university.facultiescontainer import (
60    VirtualFacultiesExportJobContainer)
61from waeup.kofa.university.course import (
62    VirtualCourseExportJobContainer,)
63from waeup.kofa.university.vocabularies import course_levels
64from waeup.kofa.utils.batching import VirtualExportJobContainer
65from waeup.kofa.utils.helpers import get_current_principal, now
66from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
67from waeup.kofa.students.interfaces import (
68    IStudentsContainer, IStudent, IUGStudentClearance, IPGStudentClearance,
69    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
70    IStudentStudyCourseTransfer, IStudentStudyCourseTranscript,
71    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, ICourseTicketAdd,
72    IStudentPaymentsContainer, IStudentOnlinePayment, IStudentPreviousPayment,
73    IStudentBalancePayment, IBedTicket, IStudentsUtils, IStudentRequestPW,
74    IStudentTranscript
75    )
76from waeup.kofa.students.catalog import search, StudentQueryResultItem
77from waeup.kofa.students.vocabularies import StudyLevelSource
78from waeup.kofa.students.workflow import (
79    ADMITTED, PAID, CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED,
80    VALIDATED, GRADUATED, TRANSCRIPT, FORBIDDEN_POSTGRAD_TRANS
81    )
82
83
84grok.context(IKofaObject)  # Make IKofaObject the default context
85
86
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            warning=_('You can not edit your data after '
2448            'requesting clearance. You really want to request clearance now?'))
2449    def requestClearance(self, **data):
2450        self.applyData(self.context, **data)
2451        if self.dataNotComplete():
2452            self.flash(self.dataNotComplete(), type="warning")
2453            return
2454        self.flash(_('Clearance form has been saved.'))
2455        if self.context.clr_code:
2456            self.redirect(self.url(self.context, 'request_clearance'))
2457        else:
2458            # We bypass the request_clearance page if student
2459            # has been imported in state 'clearance started' and
2460            # no clr_code was entered before.
2461            state = IWorkflowState(self.context).getState()
2462            if state != CLEARANCE:
2463                # This shouldn't happen, but the application officer
2464                # might have forgotten to lock the form after changing the state
2465                self.flash(_('This form cannot be submitted. Wrong state!'),
2466                           type="danger")
2467                return
2468            IWorkflowInfo(self.context).fireTransition('request_clearance')
2469            self.flash(_('Clearance has been requested.'))
2470            self.redirect(self.url(self.context))
2471        return
2472
2473class RequestClearancePage(KofaPage):
2474    grok.context(IStudent)
2475    grok.name('request_clearance')
2476    grok.require('waeup.handleStudent')
2477    grok.template('enterpin')
2478    label = _('Request clearance')
2479    notice = _('Enter the CLR access code used for starting clearance.')
2480    ac_prefix = 'CLR'
2481    pnav = 4
2482    buttonname = _('Request clearance now')
2483    with_ac = True
2484
2485    def update(self, SUBMIT=None):
2486        if self.with_ac:
2487            self.ac_series = self.request.form.get('ac_series', None)
2488            self.ac_number = self.request.form.get('ac_number', None)
2489        if SUBMIT is None:
2490            return
2491        if self.with_ac:
2492            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2493            if self.context.clr_code and self.context.clr_code != pin:
2494                self.flash(_("This isn't your CLR access code."), type="danger")
2495                return
2496        state = IWorkflowState(self.context).getState()
2497        if state != CLEARANCE:
2498            # This shouldn't happen, but the application officer
2499            # might have forgotten to lock the form after changing the state
2500            self.flash(_('This form cannot be submitted. Wrong state!'),
2501                       type="danger")
2502            return
2503        IWorkflowInfo(self.context).fireTransition('request_clearance')
2504        self.flash(_('Clearance has been requested.'))
2505        self.redirect(self.url(self.context))
2506        return
2507
2508class StartSessionPage(KofaPage):
2509    grok.context(IStudentStudyCourse)
2510    grok.name('start_session')
2511    grok.require('waeup.handleStudent')
2512    grok.template('enterpin')
2513    label = _('Start session')
2514    ac_prefix = 'SFE'
2515    notice = ''
2516    pnav = 4
2517    buttonname = _('Start now')
2518    with_ac = True
2519
2520    def update(self, SUBMIT=None):
2521        if not self.context.is_current:
2522            emit_lock_message(self)
2523            return
2524        super(StartSessionPage, self).update()
2525        if not self.context.next_session_allowed:
2526            self.flash(_("You are not entitled to start session."),
2527                       type="warning")
2528            self.redirect(self.url(self.context))
2529            return
2530        if self.with_ac:
2531            self.ac_series = self.request.form.get('ac_series', None)
2532            self.ac_number = self.request.form.get('ac_number', None)
2533        if SUBMIT is None:
2534            return
2535        if self.with_ac:
2536            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2537            code = get_access_code(pin)
2538            if not code:
2539                self.flash(_('Activation code is invalid.'), type="warning")
2540                return
2541            # Mark pin as used (this also fires a pin related transition)
2542            if code.state == USED:
2543                self.flash(_('Activation code has already been used.'),
2544                           type="warning")
2545                return
2546            else:
2547                comment = _(u"invalidated")
2548                # Here we know that the ac is in state initialized so we do not
2549                # expect an error, but the owner might be different
2550                if not invalidate_accesscode(
2551                    pin,comment,self.context.student.student_id):
2552                    self.flash(_('You are not the owner of this access code.'),
2553                               type="warning")
2554                    return
2555        try:
2556            if self.context.student.state == CLEARED:
2557                IWorkflowInfo(self.context.student).fireTransition(
2558                    'pay_first_school_fee')
2559            elif self.context.student.state == RETURNING:
2560                IWorkflowInfo(self.context.student).fireTransition(
2561                    'pay_school_fee')
2562            elif self.context.student.state == PAID:
2563                IWorkflowInfo(self.context.student).fireTransition(
2564                    'pay_pg_fee')
2565        except ConstraintNotSatisfied:
2566            self.flash(_('An error occurred, please contact the system administrator.'),
2567                       type="danger")
2568            return
2569        self.flash(_('Session started.'))
2570        self.redirect(self.url(self.context))
2571        return
2572
2573class AddStudyLevelFormPage(KofaEditFormPage):
2574    """ Page for students to add current study levels
2575    """
2576    grok.context(IStudentStudyCourse)
2577    grok.name('add')
2578    grok.require('waeup.handleStudent')
2579    grok.template('studyleveladdpage')
2580    form_fields = grok.AutoFields(IStudentStudyCourse)
2581    pnav = 4
2582
2583    @property
2584    def label(self):
2585        studylevelsource = StudyLevelSource().factory
2586        code = self.context.current_level
2587        title = studylevelsource.getTitle(self.context, code)
2588        return _('Add current level ${a}', mapping = {'a':title})
2589
2590    def update(self):
2591        if not self.context.is_current:
2592            emit_lock_message(self)
2593            return
2594        if self.context.student.state != PAID:
2595            emit_lock_message(self)
2596            return
2597        code = self.context.current_level
2598        if code is None:
2599            self.flash(_('Your data are incomplete'), type="danger")
2600            self.redirect(self.url(self.context))
2601            return
2602        super(AddStudyLevelFormPage, self).update()
2603        return
2604
2605    @action(_('Create course list now'), style='primary')
2606    def addStudyLevel(self, **data):
2607        studylevel = createObject(u'waeup.StudentStudyLevel')
2608        studylevel.level = self.context.current_level
2609        studylevel.level_session = self.context.current_session
2610        try:
2611            self.context.addStudentStudyLevel(
2612                self.context.certificate,studylevel)
2613        except KeyError:
2614            self.flash(_('This level exists.'), type="warning")
2615            self.redirect(self.url(self.context))
2616            return
2617        except RequiredMissing:
2618            self.flash(_('Your data are incomplete.'), type="danger")
2619            self.redirect(self.url(self.context))
2620            return
2621        self.flash(_('You successfully created a new course list.'))
2622        self.redirect(self.url(self.context, str(studylevel.level)))
2623        return
2624
2625class StudyLevelEditFormPage(KofaEditFormPage):
2626    """ Page to edit the student study level data by students
2627    """
2628    grok.context(IStudentStudyLevel)
2629    grok.name('edit')
2630    grok.require('waeup.editStudyLevel')
2631    grok.template('studyleveleditpage')
2632    pnav = 4
2633    placeholder = _('Enter valid course code')
2634
2635    def update(self, ADD=None, course=None):
2636        if not self.context.__parent__.is_current:
2637            emit_lock_message(self)
2638            return
2639        if self.context.student.state != PAID or \
2640            not self.context.is_current_level:
2641            emit_lock_message(self)
2642            return
2643        super(StudyLevelEditFormPage, self).update()
2644        if ADD is not None:
2645            if not course:
2646                self.flash(_('No valid course code entered.'), type="warning")
2647                return
2648            cat = queryUtility(ICatalog, name='courses_catalog')
2649            result = cat.searchResults(code=(course, course))
2650            if len(result) != 1:
2651                self.flash(_('Course not found.'), type="warning")
2652                return
2653            course = list(result)[0]
2654            addCourseTicket(self, course)
2655        return
2656
2657    @property
2658    def label(self):
2659        # Here we know that the cookie has been set
2660        lang = self.request.cookies.get('kofa.language')
2661        level_title = translate(self.context.level_title, 'waeup.kofa',
2662            target_language=lang)
2663        return _('Edit course list of ${a}',
2664            mapping = {'a':level_title})
2665
2666    @property
2667    def translated_values(self):
2668        return translated_values(self)
2669
2670    def _delCourseTicket(self, **data):
2671        form = self.request.form
2672        if 'val_id' in form:
2673            child_id = form['val_id']
2674        else:
2675            self.flash(_('No ticket selected.'), type="warning")
2676            self.redirect(self.url(self.context, '@@edit'))
2677            return
2678        if not isinstance(child_id, list):
2679            child_id = [child_id]
2680        deleted = []
2681        for id in child_id:
2682            # Students are not allowed to remove core tickets
2683            if id in self.context and \
2684                self.context[id].removable_by_student:
2685                del self.context[id]
2686                deleted.append(id)
2687        if len(deleted):
2688            self.flash(_('Successfully removed: ${a}',
2689                mapping = {'a':', '.join(deleted)}))
2690            self.context.writeLogMessage(
2691                self,'removed: %s at %s' %
2692                (', '.join(deleted), self.context.level))
2693        self.redirect(self.url(self.context, u'@@edit'))
2694        return
2695
2696    @jsaction(_('Remove selected tickets'))
2697    def delCourseTicket(self, **data):
2698        self._delCourseTicket(**data)
2699        return
2700
2701    def _registerCourses(self, **data):
2702        if self.context.student.is_postgrad and \
2703            not self.context.student.is_special_postgrad:
2704            self.flash(_(
2705                "You are a postgraduate student, "
2706                "your course list can't bee registered."), type="warning")
2707            self.redirect(self.url(self.context))
2708            return
2709        students_utils = getUtility(IStudentsUtils)
2710        max_credits = students_utils.maxCredits(self.context)
2711        if max_credits and self.context.total_credits > max_credits:
2712            self.flash(_('Maximum credits of ${a} exceeded.',
2713                mapping = {'a':max_credits}), type="warning")
2714            return
2715        if not self.context.course_registration_allowed:
2716            self.flash(_(
2717                "Course registration has ended. "
2718                "Please pay the late registration fee."), type="warning")
2719            #self.redirect(self.url(self.context))
2720            return
2721        IWorkflowInfo(self.context.student).fireTransition(
2722            'register_courses')
2723        self.flash(_('Course list has been registered.'))
2724        self.redirect(self.url(self.context))
2725        return
2726
2727    @action(_('Register course list'), style='primary',
2728        warning=_('You can not edit your course list after registration.'
2729            ' You really want to register?'))
2730    def registerCourses(self, **data):
2731        self._registerCourses(**data)
2732        return
2733
2734class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2735    """Add a course ticket by student.
2736    """
2737    grok.name('ctadd')
2738    grok.require('waeup.handleStudent')
2739    form_fields = grok.AutoFields(ICourseTicketAdd)
2740
2741    def update(self):
2742        if self.context.student.state != PAID or \
2743            not self.context.is_current_level:
2744            emit_lock_message(self)
2745            return
2746        super(CourseTicketAddFormPage2, self).update()
2747        return
2748
2749    @action(_('Add course ticket'))
2750    def addCourseTicket(self, **data):
2751        # Safety belt
2752        if self.context.student.state != PAID:
2753            return
2754        course = data['course']
2755        success = addCourseTicket(self, course)
2756        if success:
2757            self.redirect(self.url(self.context, u'@@edit'))
2758        return
2759
2760class SetPasswordPage(KofaPage):
2761    grok.context(IKofaObject)
2762    grok.name('setpassword')
2763    grok.require('waeup.Anonymous')
2764    grok.template('setpassword')
2765    label = _('Set password for first-time login')
2766    ac_prefix = 'PWD'
2767    pnav = 0
2768    set_button = _('Set')
2769
2770    def update(self, SUBMIT=None):
2771        self.reg_number = self.request.form.get('reg_number', None)
2772        self.ac_series = self.request.form.get('ac_series', None)
2773        self.ac_number = self.request.form.get('ac_number', None)
2774
2775        if SUBMIT is None:
2776            return
2777        hitlist = search(query=self.reg_number,
2778            searchtype='reg_number', view=self)
2779        if not hitlist:
2780            self.flash(_('No student found.'), type="warning")
2781            return
2782        if len(hitlist) != 1:   # Cannot happen but anyway
2783            self.flash(_('More than one student found.'), type="warning")
2784            return
2785        student = hitlist[0].context
2786        self.student_id = student.student_id
2787        student_pw = student.password
2788        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2789        code = get_access_code(pin)
2790        if not code:
2791            self.flash(_('Access code is invalid.'), type="warning")
2792            return
2793        if student_pw and pin == student.adm_code:
2794            self.flash(_(
2795                'Password has already been set. Your Student Id is ${a}',
2796                mapping = {'a':self.student_id}))
2797            return
2798        elif student_pw:
2799            self.flash(
2800                _('Password has already been set. You are using the ' +
2801                'wrong Access Code.'), type="warning")
2802            return
2803        # Mark pin as used (this also fires a pin related transition)
2804        # and set student password
2805        if code.state == USED:
2806            self.flash(_('Access code has already been used.'), type="warning")
2807            return
2808        else:
2809            comment = _(u"invalidated")
2810            # Here we know that the ac is in state initialized so we do not
2811            # expect an exception
2812            invalidate_accesscode(pin,comment)
2813            IUserAccount(student).setPassword(self.ac_number)
2814            student.adm_code = pin
2815        self.flash(_('Password has been set. Your Student Id is ${a}',
2816            mapping = {'a':self.student_id}))
2817        return
2818
2819class StudentRequestPasswordPage(KofaAddFormPage):
2820    """Captcha'd request password page for students.
2821    """
2822    grok.name('requestpw')
2823    grok.require('waeup.Anonymous')
2824    grok.template('requestpw')
2825    form_fields = grok.AutoFields(IStudentRequestPW).select(
2826        'lastname','number','email')
2827    label = _('Request password for first-time login')
2828
2829    def update(self):
2830        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2831        if blocker:
2832            self.flash(_('The portal is in maintenance mode. '
2833                        'Password request forms are temporarily disabled.'),
2834                       type='warning')
2835            self.redirect(self.url(self.context))
2836            return
2837        # Handle captcha
2838        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2839        self.captcha_result = self.captcha.verify(self.request)
2840        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2841        return
2842
2843    def _redirect(self, email, password, student_id):
2844        # Forward only email to landing page in base package.
2845        self.redirect(self.url(self.context, 'requestpw_complete',
2846            data = dict(email=email)))
2847        return
2848
2849    def _pw_used(self):
2850        # XXX: False if password has not been used. We need an extra
2851        #      attribute which remembers if student logged in.
2852        return True
2853
2854    @action(_('Send login credentials to email address'), style='primary')
2855    def get_credentials(self, **data):
2856        if not self.captcha_result.is_valid:
2857            # Captcha will display error messages automatically.
2858            # No need to flash something.
2859            return
2860        number = data.get('number','')
2861        lastname = data.get('lastname','')
2862        cat = getUtility(ICatalog, name='students_catalog')
2863        results = list(
2864            cat.searchResults(reg_number=(number, number)))
2865        if not results:
2866            results = list(
2867                cat.searchResults(matric_number=(number, number)))
2868        if results:
2869            student = results[0]
2870            if getattr(student,'lastname',None) is None:
2871                self.flash(_('An error occurred.'), type="danger")
2872                return
2873            elif student.lastname.lower() != lastname.lower():
2874                # Don't tell the truth here. Anonymous must not
2875                # know that a record was found and only the lastname
2876                # verification failed.
2877                self.flash(_('No student record found.'), type="warning")
2878                return
2879            elif student.password is not None and self._pw_used:
2880                self.flash(_('Your password has already been set and used. '
2881                             'Please proceed to the login page.'),
2882                           type="warning")
2883                return
2884            # Store email address but nothing else.
2885            student.email = data['email']
2886            notify(grok.ObjectModifiedEvent(student))
2887        else:
2888            # No record found, this is the truth.
2889            self.flash(_('No student record found.'), type="warning")
2890            return
2891
2892        kofa_utils = getUtility(IKofaUtils)
2893        password = kofa_utils.genPassword()
2894        mandate = PasswordMandate()
2895        mandate.params['password'] = password
2896        mandate.params['user'] = student
2897        site = grok.getSite()
2898        site['mandates'].addMandate(mandate)
2899        # Send email with credentials
2900        args = {'mandate_id':mandate.mandate_id}
2901        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2902        url_info = u'Confirmation link: %s' % mandate_url
2903        msg = _('You have successfully requested a password for the')
2904        if kofa_utils.sendCredentials(IUserAccount(student),
2905            password, url_info, msg):
2906            email_sent = student.email
2907        else:
2908            email_sent = None
2909        self._redirect(email=email_sent, password=password,
2910            student_id=student.student_id)
2911        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2912        self.context.logger.info(
2913            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2914        return
2915
2916class StudentRequestPasswordEmailSent(KofaPage):
2917    """Landing page after successful password request.
2918
2919    """
2920    grok.name('requestpw_complete')
2921    grok.require('waeup.Public')
2922    grok.template('requestpwmailsent')
2923    label = _('Your password request was successful.')
2924
2925    def update(self, email=None, student_id=None, password=None):
2926        self.email = email
2927        self.password = password
2928        self.student_id = student_id
2929        return
2930
2931class FilterStudentsInDepartmentPage(KofaPage):
2932    """Page that filters and lists students.
2933    """
2934    grok.context(IDepartment)
2935    grok.require('waeup.showStudents')
2936    grok.name('students')
2937    grok.template('filterstudentspage')
2938    pnav = 1
2939    session_label = _('Current Session')
2940    level_label = _('Current Level')
2941
2942    def label(self):
2943        return 'Students in %s' % self.context.longtitle
2944
2945    def _set_session_values(self):
2946        vocab_terms = academic_sessions_vocab.by_value.values()
2947        self.sessions = sorted(
2948            [(x.title, x.token) for x in vocab_terms], reverse=True)
2949        self.sessions += [('All Sessions', 'all')]
2950        return
2951
2952    def _set_level_values(self):
2953        vocab_terms = course_levels.by_value.values()
2954        self.levels = sorted(
2955            [(x.title, x.token) for x in vocab_terms])
2956        self.levels += [('All Levels', 'all')]
2957        return
2958
2959    def _searchCatalog(self, session, level):
2960        if level not in (10, 999, None):
2961            start_level = 100 * (level // 100)
2962            end_level = start_level + 90
2963        else:
2964            start_level = end_level = level
2965        cat = queryUtility(ICatalog, name='students_catalog')
2966        students = cat.searchResults(
2967            current_session=(session, session),
2968            current_level=(start_level, end_level),
2969            depcode=(self.context.code, self.context.code)
2970            )
2971        hitlist = []
2972        for student in students:
2973            hitlist.append(StudentQueryResultItem(student, view=self))
2974        return hitlist
2975
2976    def update(self, SHOW=None, session=None, level=None):
2977        self.parent_url = self.url(self.context.__parent__)
2978        self._set_session_values()
2979        self._set_level_values()
2980        self.hitlist = []
2981        self.session_default = session
2982        self.level_default = level
2983        if SHOW is not None:
2984            if session != 'all':
2985                self.session = int(session)
2986                self.session_string = '%s %s/%s' % (
2987                    self.session_label, self.session, self.session+1)
2988            else:
2989                self.session = None
2990                self.session_string = _('in any session')
2991            if level != 'all':
2992                self.level = int(level)
2993                self.level_string = '%s %s' % (self.level_label, self.level)
2994            else:
2995                self.level = None
2996                self.level_string = _('at any level')
2997            self.hitlist = self._searchCatalog(self.session, self.level)
2998            if not self.hitlist:
2999                self.flash(_('No student found.'), type="warning")
3000        return
3001
3002class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3003    """Page that filters and lists students.
3004    """
3005    grok.context(ICertificate)
3006
3007    def label(self):
3008        return 'Students studying %s' % self.context.longtitle
3009
3010    def _searchCatalog(self, session, level):
3011        if level not in (10, 999, None):
3012            start_level = 100 * (level // 100)
3013            end_level = start_level + 90
3014        else:
3015            start_level = end_level = level
3016        cat = queryUtility(ICatalog, name='students_catalog')
3017        students = cat.searchResults(
3018            current_session=(session, session),
3019            current_level=(start_level, end_level),
3020            certcode=(self.context.code, self.context.code)
3021            )
3022        hitlist = []
3023        for student in students:
3024            hitlist.append(StudentQueryResultItem(student, view=self))
3025        return hitlist
3026
3027class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3028    """Page that filters and lists students.
3029    """
3030    grok.context(ICourse)
3031    grok.require('waeup.viewStudent')
3032
3033    session_label = _('Session')
3034    level_label = _('Level')
3035
3036    def label(self):
3037        return 'Students registered for %s' % self.context.longtitle
3038
3039    def _searchCatalog(self, session, level):
3040        if level not in (10, 999, None):
3041            start_level = 100 * (level // 100)
3042            end_level = start_level + 90
3043        else:
3044            start_level = end_level = level
3045        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3046        coursetickets = cat.searchResults(
3047            session=(session, session),
3048            level=(start_level, end_level),
3049            code=(self.context.code, self.context.code)
3050            )
3051        hitlist = []
3052        for ticket in coursetickets:
3053            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3054        return list(set(hitlist))
3055
3056class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3057    """ Clear all students of a department in state 'clearance requested'.
3058    """
3059    grok.context(IDepartment)
3060    grok.name('clearallstudents')
3061    grok.require('waeup.clearAllStudents')
3062
3063    def update(self):
3064        cat = queryUtility(ICatalog, name='students_catalog')
3065        students = cat.searchResults(
3066            depcode=(self.context.code, self.context.code),
3067            state=(REQUESTED, REQUESTED)
3068            )
3069        num = 0
3070        for student in students:
3071            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3072                continue
3073            IWorkflowInfo(student).fireTransition('clear')
3074            num += 1
3075        self.flash(_('%d students have been cleared.' % num))
3076        self.redirect(self.url(self.context))
3077        return
3078
3079    def render(self):
3080        return
3081
3082
3083class EditScoresPage(KofaPage):
3084    """Page that allows to edit batches of scores.
3085    """
3086    grok.context(ICourse)
3087    grok.require('waeup.editScores')
3088    grok.name('edit_scores')
3089    grok.template('editscorespage')
3090    pnav = 1
3091    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3092
3093    def label(self):
3094        return '%s tickets in academic session %s' % (
3095            self.context.code, self.session_title)
3096
3097    def _searchCatalog(self, session):
3098        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3099        coursetickets = cat.searchResults(
3100            session=(session, session),
3101            code=(self.context.code, self.context.code)
3102            )
3103        return list(coursetickets)
3104
3105    def _extract_uploadfile(self, uploadfile):
3106        """Get a mapping of student-ids to scores.
3107
3108        The mapping is constructed by reading contents from `uploadfile`.
3109
3110        We expect uploadfile to be a regular CSV file with columns
3111        ``student_id`` and ``score`` (other cols are ignored).
3112        """
3113        result = dict()
3114        data = StringIO(uploadfile.read())  # ensure we have something seekable
3115        reader = csv.DictReader(data)
3116        for row in reader:
3117            if not 'student_id' in row or not 'score' in row:
3118                continue
3119            result[row['student_id']] = row['score']
3120        return result
3121
3122    def update(self,  *args, **kw):
3123        form = self.request.form
3124        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3125        self.current_academic_session = grok.getSite()[
3126            'configuration'].current_academic_session
3127        if self.context.__parent__.__parent__.score_editing_disabled:
3128            self.flash(_('Score editing disabled.'), type="warning")
3129            self.redirect(self.url(self.context))
3130            return
3131        if not self.current_academic_session:
3132            self.flash(_('Current academic session not set.'), type="warning")
3133            self.redirect(self.url(self.context))
3134            return
3135        self.session_title = academic_sessions_vocab.getTerm(
3136            self.current_academic_session).title
3137        self.tickets = self._searchCatalog(self.current_academic_session)
3138        editable_tickets = [
3139            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3140        if not self.tickets:
3141            self.flash(_('No student found.'), type="warning")
3142            self.redirect(self.url(self.context))
3143            return
3144        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3145            return
3146        error = ''
3147        if not editable_tickets:
3148            return
3149        if 'UPDATE_FILE' in form:
3150            if form['uploadfile']:
3151                try:
3152                    formvals = self._extract_uploadfile(form['uploadfile'])
3153                except:
3154                    self.flash(
3155                        _('Uploaded file contains illegal data. Ignored'),
3156                        type="danger")
3157                    return
3158            else:
3159                self.flash(
3160                    _('No file provided.'), type="danger")
3161                return
3162        else:
3163            formvals = dict(zip(form['sids'], form['scores']))
3164        for ticket in editable_tickets:
3165            score = ticket.score
3166            sid = ticket.student.student_id
3167            if sid not in formvals:
3168                continue
3169            if formvals[sid] == '':
3170                score = None
3171            else:
3172                try:
3173                    score = int(formvals[sid])
3174                except ValueError:
3175                    error += '%s, ' % ticket.student.display_fullname
3176            if ticket.score != score:
3177                ticket.score = score
3178                ticket.student.__parent__.logger.info(
3179                    '%s - %s %s/%s score updated (%s)' % (
3180                        ob_class, ticket.student.student_id,
3181                        ticket.level, ticket.code, score)
3182                    )
3183        if error:
3184            self.flash(
3185                _('Error: Score(s) of following students have not been '
3186                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3187                type="danger")
3188            return
3189        self.flash(_('You successfully updated course results.'))
3190        return
3191
3192
3193class DownloadScoresView(UtilityView, grok.View):
3194    """View that exports scores.
3195    """
3196    grok.context(ICourse)
3197    grok.require('waeup.editScores')
3198    grok.name('download_scores')
3199
3200    def update(self):
3201        self.current_academic_session = grok.getSite()[
3202            'configuration'].current_academic_session
3203        if self.context.__parent__.__parent__.score_editing_disabled:
3204            self.flash(_('Score editing disabled.'), type="warning")
3205            self.redirect(self.url(self.context))
3206            return
3207        if not self.current_academic_session:
3208            self.flash(_('Current academic session not set.'), type="warning")
3209            self.redirect(self.url(self.context))
3210            return
3211        site = grok.getSite()
3212        exporter = getUtility(ICSVExporter, name='lecturer')
3213        self.csv = exporter.export_filtered(site, filepath=None,
3214                                 catalog='coursetickets',
3215                                 session=self.current_academic_session,
3216                                 level=None,
3217                                 code=self.context.code)
3218        return
3219
3220    def render(self):
3221        filename = 'results_%s_%s.csv' % (
3222            self.context.code, self.current_academic_session)
3223        self.response.setHeader(
3224            'Content-Type', 'text/csv; charset=UTF-8')
3225        self.response.setHeader(
3226            'Content-Disposition:', 'attachment; filename="%s' % filename)
3227        return self.csv
3228
3229class ExportPDFScoresSlip(UtilityView, grok.View,
3230    LocalRoleAssignmentUtilityView):
3231    """Deliver a PDF slip of course tickets for a lecturer.
3232    """
3233    grok.context(ICourse)
3234    grok.name('coursetickets.pdf')
3235    grok.require('waeup.editScores')
3236
3237    def table_data(self, session):
3238        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3239        coursetickets = cat.searchResults(
3240            session=(session, session),
3241            code=(self.context.code, self.context.code)
3242            )
3243        header = [[_('Matric No.'),
3244                   _('Reg. No.'),
3245                   _('Fullname'),
3246                   _('Status'),
3247                   _('Course of Studies'),
3248                   _('Level'),
3249                   _('Score') ],]
3250        tickets = []
3251        for ticket in list(coursetickets):
3252            row = [ticket.student.matric_number,
3253                  ticket.student.reg_number,
3254                  ticket.student.display_fullname,
3255                  ticket.student.translated_state,
3256                  ticket.student.certcode,
3257                  ticket.level,
3258                  ticket.score]
3259            tickets.append(row)
3260        return header + sorted(tickets, key=lambda value: value[0])
3261
3262    def render(self):
3263        session = grok.getSite()['configuration'].current_academic_session
3264        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3265                     if i['local_role'] == 'waeup.local.Lecturer']
3266        lecturers =  ', '.join(lecturers)
3267        students_utils = getUtility(IStudentsUtils)
3268        return students_utils.renderPDFCourseticketsOverview(
3269            self, session, self.table_data(session), lecturers)
3270
3271class ExportJobContainerOverview(KofaPage):
3272    """Page that lists active student data export jobs and provides links
3273    to discard or download CSV files.
3274
3275    """
3276    grok.context(VirtualExportJobContainer)
3277    grok.require('waeup.showStudents')
3278    grok.name('index.html')
3279    grok.template('exportjobsindex')
3280    label = _('Student Data Exports')
3281    pnav = 1
3282    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3283
3284    def update(self, CREATE=None, DISCARD=None, job_id=None):
3285        if CREATE:
3286            self.redirect(self.url('@@exportconfig'))
3287            return
3288        if DISCARD and job_id:
3289            entry = self.context.entry_from_job_id(job_id)
3290            self.context.delete_export_entry(entry)
3291            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3292            self.context.logger.info(
3293                '%s - discarded: job_id=%s' % (ob_class, job_id))
3294            self.flash(_('Discarded export') + ' %s' % job_id)
3295        self.entries = doll_up(self, user=self.request.principal.id)
3296        return
3297
3298class ExportJobContainerJobConfig(KofaPage):
3299    """Page that configures a students export job.
3300
3301    This is a baseclass.
3302    """
3303    grok.baseclass()
3304    grok.name('exportconfig')
3305    grok.require('waeup.showStudents')
3306    grok.template('exportconfig')
3307    label = _('Configure student data export')
3308    pnav = 1
3309    redirect_target = ''
3310    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3311
3312    def _set_session_values(self):
3313        vocab_terms = academic_sessions_vocab.by_value.values()
3314        self.sessions = sorted(
3315            [(x.title, x.token) for x in vocab_terms], reverse=True)
3316        self.sessions += [(_('All Sessions'), 'all')]
3317        return
3318
3319    def _set_level_values(self):
3320        vocab_terms = course_levels.by_value.values()
3321        self.levels = sorted(
3322            [(x.title, x.token) for x in vocab_terms])
3323        self.levels += [(_('All Levels'), 'all')]
3324        return
3325
3326    def _set_mode_values(self):
3327        utils = getUtility(IKofaUtils)
3328        self.modes = sorted([(value, key) for key, value in
3329                      utils.STUDY_MODES_DICT.items()])
3330        self.modes +=[(_('All Modes'), 'all')]
3331        return
3332
3333    def _set_exporter_values(self):
3334        # We provide all student exporters, nothing else, yet.
3335        # Bursary or Department Officers don't have the general exportData
3336        # permission and are only allowed to export bursary or payments
3337        # overview data respectively. This is the only place where
3338        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3339        # are used.
3340        exporters = []
3341        if not checkPermission('waeup.exportData', self.context):
3342            if checkPermission('waeup.exportBursaryData', self.context):
3343                exporters += [('Bursary Data', 'bursary')]
3344            if checkPermission('waeup.exportPaymentsOverview', self.context):
3345                exporters += [('Student Payments Overview', 'paymentsoverview')]
3346            self.exporters = exporters
3347            return
3348        STUDENT_EXPORTER_NAMES = getUtility(
3349            IStudentsUtils).STUDENT_EXPORTER_NAMES
3350        for name in STUDENT_EXPORTER_NAMES:
3351            util = getUtility(ICSVExporter, name=name)
3352            exporters.append((util.title, name),)
3353        self.exporters = exporters
3354        return
3355
3356    @property
3357    def faccode(self):
3358        return None
3359
3360    @property
3361    def depcode(self):
3362        return None
3363
3364    @property
3365    def certcode(self):
3366        return None
3367
3368    def update(self, START=None, session=None, level=None, mode=None,
3369               payments_start=None, payments_end=None,
3370               exporter=None):
3371        self._set_session_values()
3372        self._set_level_values()
3373        self._set_mode_values()
3374        self._set_exporter_values()
3375        if START is None:
3376            return
3377        ena = exports_not_allowed(self)
3378        if ena:
3379            self.flash(ena, type='danger')
3380            return
3381        if payments_start or payments_end:
3382            date_format = '%d/%m/%Y'
3383            try:
3384                datetime.strptime(payments_start, date_format)
3385                datetime.strptime(payments_end, date_format)
3386            except ValueError:
3387                self.flash(_('Payment dates do not match format d/m/Y.'),
3388                           type="danger")
3389                return
3390        if session == 'all':
3391            session=None
3392        if level == 'all':
3393            level = None
3394        if mode == 'all':
3395            mode = None
3396        if payments_start == '':
3397            payments_start = None
3398        if payments_end == '':
3399            payments_end = None
3400        if (mode,
3401            level,
3402            session,
3403            self.faccode,
3404            self.depcode,
3405            self.certcode) == (None, None, None, None, None, None):
3406            # Export all students including those without certificate
3407            if payments_start:
3408                job_id = self.context.start_export_job(exporter,
3409                                              self.request.principal.id,
3410                                              payments_start = payments_start,
3411                                              payments_end = payments_end)
3412            else:
3413                job_id = self.context.start_export_job(exporter,
3414                                              self.request.principal.id)
3415        else:
3416            if payments_start:
3417                job_id = self.context.start_export_job(exporter,
3418                                              self.request.principal.id,
3419                                              current_session=session,
3420                                              current_level=level,
3421                                              current_mode=mode,
3422                                              faccode=self.faccode,
3423                                              depcode=self.depcode,
3424                                              certcode=self.certcode,
3425                                              payments_start = payments_start,
3426                                              payments_end = payments_end)
3427            else:
3428                job_id = self.context.start_export_job(exporter,
3429                                              self.request.principal.id,
3430                                              current_session=session,
3431                                              current_level=level,
3432                                              current_mode=mode,
3433                                              faccode=self.faccode,
3434                                              depcode=self.depcode,
3435                                              certcode=self.certcode)
3436        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3437        self.context.logger.info(
3438            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3439            % (ob_class, exporter, session, level, mode, self.faccode,
3440            self.depcode, self.certcode, payments_start, payments_end, job_id))
3441        self.flash(_('Export started for students with') +
3442                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3443                   session, level, mode))
3444        self.redirect(self.url(self.redirect_target))
3445        return
3446
3447class ExportJobContainerDownload(ExportCSVView):
3448    """Page that downloads a students export csv file.
3449
3450    """
3451    grok.context(VirtualExportJobContainer)
3452    grok.require('waeup.showStudents')
3453
3454class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3455    """Page that configures a students export job in datacenter.
3456
3457    """
3458    grok.context(IDataCenter)
3459    redirect_target = '@@export'
3460
3461class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3462    """Page that configures a students export job in datacenter.
3463
3464    """
3465    grok.name('exportselected')
3466    grok.context(IDataCenter)
3467    redirect_target = '@@export'
3468    grok.template('exportselected')
3469    label = _('Configure student data export')
3470
3471    def update(self, START=None, students=None, exporter=None):
3472        self._set_exporter_values()
3473        if START is None:
3474            return
3475        ena = exports_not_allowed(self)
3476        if ena:
3477            self.flash(ena, type='danger')
3478            return
3479        try:
3480            ids = students.replace(',', ' ').split()
3481        except:
3482            self.flash(sys.exc_info()[1])
3483            self.redirect(self.url(self.redirect_target))
3484            return
3485        job_id = self.context.start_export_job(
3486            exporter, self.request.principal.id, selected=ids)
3487        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3488        self.context.logger.info(
3489            '%s - selected students exported: %s, job_id=%s' %
3490            (ob_class, exporter, job_id))
3491        self.flash(_('Export of selected students started.'))
3492        self.redirect(self.url(self.redirect_target))
3493        return
3494
3495class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3496    """Page that configures a students export job in facultiescontainer.
3497
3498    """
3499    grok.context(VirtualFacultiesExportJobContainer)
3500
3501
3502class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3503    """Page that configures a students export job in faculties.
3504
3505    """
3506    grok.context(VirtualFacultyExportJobContainer)
3507
3508    @property
3509    def faccode(self):
3510        return self.context.__parent__.code
3511
3512class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3513    """Page that configures a students export job in departments.
3514
3515    """
3516    grok.context(VirtualDepartmentExportJobContainer)
3517
3518    @property
3519    def depcode(self):
3520        return self.context.__parent__.code
3521
3522class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3523    """Page that configures a students export job for certificates.
3524
3525    """
3526    grok.context(VirtualCertificateExportJobContainer)
3527    grok.template('exportconfig_certificate')
3528
3529    @property
3530    def certcode(self):
3531        return self.context.__parent__.code
3532
3533class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3534    """Page that configures a students export job for courses.
3535
3536    In contrast to department or certificate student data exports the
3537    coursetickets_catalog is searched here. Therefore the update
3538    method from the base class is customized.
3539    """
3540    grok.context(VirtualCourseExportJobContainer)
3541    grok.template('exportconfig_course')
3542
3543    def _set_exporter_values(self):
3544        # We provide only the 'coursetickets' and 'lecturer' exporter
3545        # but can add more.
3546        exporters = []
3547        for name in ('coursetickets', 'lecturer'):
3548            util = getUtility(ICSVExporter, name=name)
3549            exporters.append((util.title, name),)
3550        self.exporters = exporters
3551
3552    def _set_session_values(self):
3553        # We allow only current academic session
3554        academic_session = grok.getSite()['configuration'].current_academic_session
3555        if not academic_session:
3556            self.sessions = []
3557            return
3558        x = academic_sessions_vocab.getTerm(academic_session)
3559        self.sessions = [(x.title, x.token)]
3560        return
3561
3562    def update(self, START=None, session=None, level=None, mode=None,
3563               exporter=None):
3564        self._set_session_values()
3565        self._set_level_values()
3566        self._set_mode_values()
3567        self._set_exporter_values()
3568        if not self.sessions:
3569            self.flash(
3570                _('Academic session not set. '
3571                  'Please contact the administrator.'),
3572                type='danger')
3573            self.redirect(self.url(self.context))
3574            return
3575        if START is None:
3576            return
3577        ena = exports_not_allowed(self)
3578        if ena:
3579            self.flash(ena, type='danger')
3580            return
3581        if session == 'all':
3582            session = None
3583        if level == 'all':
3584            level = None
3585        job_id = self.context.start_export_job(exporter,
3586                                      self.request.principal.id,
3587                                      # Use a different catalog and
3588                                      # pass different keywords than
3589                                      # for the (default) students_catalog
3590                                      catalog='coursetickets',
3591                                      session=session,
3592                                      level=level,
3593                                      code=self.context.__parent__.code)
3594        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3595        self.context.logger.info(
3596            '%s - exported: %s (%s, %s, %s), job_id=%s'
3597            % (ob_class, exporter, session, level,
3598            self.context.__parent__.code, job_id))
3599        self.flash(_('Export started for course tickets with') +
3600                   ' level_session=%s, level=%s' % (
3601                   session, level))
3602        self.redirect(self.url(self.redirect_target))
3603        return
Note: See TracBrowser for help on using the repository browser.