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

Last change on this file since 15104 was 15104, checked in by Henrik Bettermann, 6 years ago

Sort on title.

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