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

Last change on this file since 15039 was 14983, checked in by Henrik Bettermann, 7 years ago

Fix typo.

  • Property svn:keywords set to Id
File size: 135.5 KB
Line 
1## $Id: browser.py 14983 2018-04-05 12:22:41Z 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        if self.context.certificate is not None:
935            return self.context.certificate.__parent__.__parent__
936        return
937
938    @property
939    def faculty(self):
940        if self.context.certificate is not None:
941            return self.context.certificate.__parent__.__parent__.__parent__
942        return
943
944    @property
945    def prev_studycourses(self):
946        if self.context.is_current:
947            if self.context.__parent__.get('studycourse_2', None) is not None:
948                return (
949                        {'href':self.url(self.context.student) + '/studycourse_1',
950                        'title':_('First Study Course, ')},
951                        {'href':self.url(self.context.student) + '/studycourse_2',
952                        'title':_('Second Study Course')}
953                        )
954            if self.context.__parent__.get('studycourse_1', None) is not None:
955                return (
956                        {'href':self.url(self.context.student) + '/studycourse_1',
957                        'title':_('First Study Course')},
958                        )
959        return
960
961class StudyCourseManageFormPage(KofaEditFormPage):
962    """ Page to edit the student study course data
963    """
964    grok.context(IStudentStudyCourse)
965    grok.name('manage')
966    grok.require('waeup.manageStudent')
967    grok.template('studycoursemanagepage')
968    label = _('Manage study course')
969    pnav = 4
970    taboneactions = [_('Save'),_('Cancel')]
971    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
972    tabthreeactions = [_('Add study level')]
973
974    @property
975    def form_fields(self):
976        if self.context.is_postgrad:
977            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
978                'previous_verdict')
979        else:
980            form_fields = grok.AutoFields(IStudentStudyCourse)
981        return form_fields
982
983    def update(self):
984        if not self.context.is_current:
985            emit_lock_message(self)
986            return
987        super(StudyCourseManageFormPage, self).update()
988        return
989
990    @action(_('Save'), style='primary')
991    def save(self, **data):
992        try:
993            msave(self, **data)
994        except ConstraintNotSatisfied:
995            # The selected level might not exist in certificate
996            self.flash(_('Current level not available for certificate.'),
997                       type="warning")
998            return
999        notify(grok.ObjectModifiedEvent(self.context.__parent__))
1000        return
1001
1002    @property
1003    def level_dicts(self):
1004        studylevelsource = StudyLevelSource().factory
1005        for code in studylevelsource.getValues(self.context):
1006            title = studylevelsource.getTitle(self.context, code)
1007            yield(dict(code=code, title=title))
1008
1009    @property
1010    def session_dicts(self):
1011        yield(dict(code='', title='--'))
1012        for item in academic_sessions():
1013            code = item[1]
1014            title = item[0]
1015            yield(dict(code=code, title=title))
1016
1017    @action(_('Add study level'), style='primary')
1018    def addStudyLevel(self, **data):
1019        level_code = self.request.form.get('addlevel', None)
1020        level_session = self.request.form.get('level_session', None)
1021        if not level_session:
1022            self.flash(_('You must select a session for the level.'),
1023                       type="warning")
1024            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1025            return
1026        studylevel = createObject(u'waeup.StudentStudyLevel')
1027        studylevel.level = int(level_code)
1028        studylevel.level_session = int(level_session)
1029        try:
1030            self.context.addStudentStudyLevel(
1031                self.context.certificate,studylevel)
1032            self.flash(_('Study level has been added.'))
1033        except KeyError:
1034            self.flash(_('This level exists.'), type="warning")
1035        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1036        return
1037
1038    @jsaction(_('Remove selected levels'))
1039    def delStudyLevels(self, **data):
1040        form = self.request.form
1041        if 'val_id' in form:
1042            child_id = form['val_id']
1043        else:
1044            self.flash(_('No study level selected.'), type="warning")
1045            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1046            return
1047        if not isinstance(child_id, list):
1048            child_id = [child_id]
1049        deleted = []
1050        for id in child_id:
1051            del self.context[id]
1052            deleted.append(id)
1053        if len(deleted):
1054            self.flash(_('Successfully removed: ${a}',
1055                mapping = {'a':', '.join(deleted)}))
1056            self.context.writeLogMessage(
1057                self,'removed: %s' % ', '.join(deleted))
1058        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1059        return
1060
1061class StudentTranscriptRequestPage(KofaPage):
1062    """ Page to request transcript by student
1063    """
1064    grok.context(IStudent)
1065    grok.name('request_transcript')
1066    grok.require('waeup.handleStudent')
1067    grok.template('transcriptrequest')
1068    label = _('Request transcript')
1069    ac_prefix = 'TSC'
1070    notice = ''
1071    pnav = 4
1072    buttonname = _('Submit')
1073    with_ac = True
1074
1075    def update(self, SUBMIT=None):
1076        super(StudentTranscriptRequestPage, self).update()
1077        if not self.context.state == GRADUATED:
1078            self.flash(_("Wrong state"), type="danger")
1079            self.redirect(self.url(self.context))
1080            return
1081        if self.with_ac:
1082            self.ac_series = self.request.form.get('ac_series', None)
1083            self.ac_number = self.request.form.get('ac_number', None)
1084        if self.context.transcript_comment is not None:
1085            self.correspondence = self.context.transcript_comment.replace(
1086                '\n', '<br>')
1087        else:
1088            self.correspondence = ''
1089        if SUBMIT is None:
1090            return
1091        if self.with_ac:
1092            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1093            code = get_access_code(pin)
1094            if not code:
1095                self.flash(_('Activation code is invalid.'), type="warning")
1096                return
1097            if code.state == USED:
1098                self.flash(_('Activation code has already been used.'),
1099                           type="warning")
1100                return
1101            # Mark pin as used (this also fires a pin related transition)
1102            # and fire transition request_transcript
1103            comment = _(u"invalidated")
1104            # Here we know that the ac is in state initialized so we do not
1105            # expect an exception, but the owner might be different
1106            if not invalidate_accesscode(pin, comment, self.context.student_id):
1107                self.flash(_('You are not the owner of this access code.'),
1108                           type="warning")
1109                return
1110            self.context.clr_code = pin
1111        IWorkflowInfo(self.context).fireTransition('request_transcript')
1112        comment = self.request.form.get('comment', '').replace('\r', '')
1113        address = self.request.form.get('address', '').replace('\r', '')
1114        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1115        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1116        old_transcript_comment = self.context.transcript_comment
1117        if old_transcript_comment == None:
1118            old_transcript_comment = ''
1119        self.context.transcript_comment = '''On %s %s wrote:
1120
1121%s
1122
1123Dispatch Address:
1124%s
1125
1126%s''' % (today, self.request.principal.id, comment, address,
1127         old_transcript_comment)
1128        self.context.writeLogMessage(
1129            self, 'comment: %s' % comment.replace('\n', '<br>'))
1130        self.flash(_('Transcript processing has been started.'))
1131        self.redirect(self.url(self.context))
1132        return
1133
1134class StudentTranscriptRequestProcessFormPage(KofaEditFormPage):
1135    """ Page to process transcript requests
1136    """
1137    grok.context(IStudent)
1138    grok.name('process_transcript_request')
1139    grok.require('waeup.viewTranscript')
1140    grok.template('transcriptprocess')
1141    form_fields = grok.AutoFields(IStudentTranscript)
1142    label = _('Process transcript request')
1143    buttonname = _('Save comment and mark as processed')
1144    pnav = 4
1145
1146    def update(self, SUBMIT=None):
1147        super(StudentTranscriptRequestProcessFormPage, self).update()
1148        if self.context.state != TRANSCRIPT:
1149            self.flash(_('Student is in wrong state.'), type="warning")
1150            self.redirect(self.url(self.context))
1151            return
1152        if self.context.transcript_comment is not None:
1153            self.correspondence = self.context.transcript_comment.replace(
1154                '\n', '<br>')
1155        else:
1156            self.correspondence = ''
1157        if SUBMIT is None:
1158            return
1159        IWorkflowInfo(self.context).fireTransition('process_transcript')
1160        self.flash(_('Transcript request processed.'))
1161        comment = self.request.form.get('comment', '').replace('\r', '')
1162        tz = getattr(queryUtility(IKofaUtils), 'tzinfo', pytz.utc)
1163        today = now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
1164        old_transcript_comment = self.context.transcript_comment
1165        if old_transcript_comment == None:
1166            old_transcript_comment = ''
1167        self.context.transcript_comment = '''On %s %s wrote:
1168
1169%s
1170
1171%s''' % (today, self.request.principal.id, comment,
1172         old_transcript_comment)
1173        self.context.writeLogMessage(
1174            self, 'comment: %s' % comment.replace('\n', '<br>'))
1175        subject = _('Transcript processed')
1176        args = {'subject':subject, 'body':comment}
1177        self.redirect(self.url(self.context) +
1178            '/contactstudent?%s' % urlencode(args))
1179        return
1180
1181class StudentTranscriptRequestManageFormPage(KofaEditFormPage):
1182    """ Page to edit personal data by student
1183    """
1184    grok.context(IStudent)
1185    grok.name('manage_transcript_request')
1186    grok.require('waeup.manageStudent')
1187    form_fields = grok.AutoFields(IStudentTranscript)
1188    label = _('Manage transcript request')
1189    pnav = 4
1190
1191    @action(_('Save'), style='primary')
1192    def save(self, **data):
1193        msave(self, **data)
1194        return
1195
1196class StudyCourseTranscriptPage(KofaDisplayFormPage):
1197    """ Page to display the student's transcript.
1198    """
1199    grok.context(IStudentStudyCourseTranscript)
1200    grok.name('transcript')
1201    grok.require('waeup.viewTranscript')
1202    grok.template('transcript')
1203    pnav = 4
1204
1205    def update(self):
1206        if not self.context.student.transcript_enabled:
1207            self.flash(_('You are not allowed to view the transcript.'),
1208                       type="warning")
1209            self.redirect(self.url(self.context))
1210            return
1211        super(StudyCourseTranscriptPage, self).update()
1212        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1213        self.level_dict = level_dict(self.context)
1214        self.session_dict = dict(
1215            [(item[1], item[0]) for item in academic_sessions()])
1216        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1217        return
1218
1219    @property
1220    def label(self):
1221        # Here we know that the cookie has been set
1222        return _('${a}: Transcript Data', mapping = {
1223            'a':self.context.student.display_fullname})
1224
1225class ExportPDFTranscriptSlip(UtilityView, grok.View):
1226    """Deliver a PDF slip of the context.
1227    """
1228    grok.context(IStudentStudyCourse)
1229    grok.name('transcript.pdf')
1230    grok.require('waeup.viewTranscript')
1231    form_fields = grok.AutoFields(IStudentStudyCourseTranscript)
1232    prefix = 'form'
1233    omit_fields = (
1234        'department', 'faculty', 'current_mode', 'entry_session', 'certificate',
1235        'password', 'suspended', 'phone', 'email',
1236        'adm_code', 'suspended_comment', 'current_level', 'flash_notice')
1237
1238    def update(self):
1239        if not self.context.student.transcript_enabled:
1240            self.flash(_('You are not allowed to download the transcript.'),
1241                       type="warning")
1242            self.redirect(self.url(self.context))
1243            return
1244        super(ExportPDFTranscriptSlip, self).update()
1245        self.semester_dict = getUtility(IKofaUtils).SEMESTER_DICT
1246        self.level_dict = level_dict(self.context)
1247        self.session_dict = dict(
1248            [(item[1], item[0]) for item in academic_sessions()])
1249        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1250        return
1251
1252    @property
1253    def label(self):
1254        # Here we know that the cookie has been set
1255        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1256        return translate(_('Academic Transcript'),
1257            'waeup.kofa', target_language=portal_language)
1258
1259    def _sigsInFooter(self):
1260        return (_('CERTIFIED TRUE COPY'),)
1261
1262    def _signatures(self):
1263        return None
1264
1265    def render(self):
1266        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1267        Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
1268        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1269        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1270        Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
1271        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1272        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1273        studentview = StudentBasePDFFormPage(self.context.student,
1274            self.request, self.omit_fields)
1275        students_utils = getUtility(IStudentsUtils)
1276
1277        tableheader = [(Code,'code', 2.5),
1278                         (Title,'title', 7),
1279                         (Term, 'semester', 1.5),
1280                         (Cred, 'credits', 1.5),
1281                         (Score, 'total_score', 1.5),
1282                         (Grade, 'grade', 1.5),
1283                         ]
1284
1285        return students_utils.renderPDFTranscript(
1286            self, 'transcript.pdf',
1287            self.context.student, studentview,
1288            omit_fields=self.omit_fields,
1289            tableheader=tableheader,
1290            signatures=self._signatures(),
1291            sigs_in_footer=self._sigsInFooter(),
1292            )
1293
1294class StudentTransferFormPage(KofaAddFormPage):
1295    """Page to transfer the student.
1296    """
1297    grok.context(IStudent)
1298    grok.name('transfer')
1299    grok.require('waeup.manageStudent')
1300    label = _('Transfer student')
1301    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1302        'entry_mode', 'entry_session')
1303    pnav = 4
1304
1305    @jsaction(_('Transfer'))
1306    def transferStudent(self, **data):
1307        error = self.context.transfer(**data)
1308        if error == -1:
1309            self.flash(_('Current level does not match certificate levels.'),
1310                       type="warning")
1311        elif error == -2:
1312            self.flash(_('Former study course record incomplete.'),
1313                       type="warning")
1314        elif error == -3:
1315            self.flash(_('Maximum number of transfers exceeded.'),
1316                       type="warning")
1317        else:
1318            self.flash(_('Successfully transferred.'))
1319        return
1320
1321class RevertTransferFormPage(KofaEditFormPage):
1322    """View that reverts the previous transfer.
1323    """
1324    grok.context(IStudent)
1325    grok.name('revert_transfer')
1326    grok.require('waeup.manageStudent')
1327    grok.template('reverttransfer')
1328    label = _('Revert previous transfer')
1329
1330    def update(self):
1331        if not self.context.has_key('studycourse_1'):
1332            self.flash(_('No previous transfer.'), type="warning")
1333            self.redirect(self.url(self.context))
1334            return
1335        return
1336
1337    @jsaction(_('Revert now'))
1338    def transferStudent(self, **data):
1339        self.context.revert_transfer()
1340        self.flash(_('Previous transfer reverted.'))
1341        self.redirect(self.url(self.context, 'studycourse'))
1342        return
1343
1344class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1345    """ Page to display student study levels
1346    """
1347    grok.context(IStudentStudyLevel)
1348    grok.name('index')
1349    grok.require('waeup.viewStudent')
1350    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1351    form_fields[
1352        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1353    grok.template('studylevelpage')
1354    pnav = 4
1355
1356    def update(self):
1357        super(StudyLevelDisplayFormPage, self).update()
1358        return
1359
1360    @property
1361    def translated_values(self):
1362        return translated_values(self)
1363
1364    @property
1365    def label(self):
1366        # Here we know that the cookie has been set
1367        lang = self.request.cookies.get('kofa.language')
1368        level_title = translate(self.context.level_title, 'waeup.kofa',
1369            target_language=lang)
1370        return _('${a}: Study Level ${b}', mapping = {
1371            'a':self.context.student.display_fullname,
1372            'b':level_title})
1373
1374class ExportPDFCourseRegistrationSlip(UtilityView, grok.View):
1375    """Deliver a PDF slip of the context.
1376    """
1377    grok.context(IStudentStudyLevel)
1378    grok.name('course_registration_slip.pdf')
1379    grok.require('waeup.viewStudent')
1380    form_fields = grok.AutoFields(IStudentStudyLevel).omit('level')
1381    form_fields[
1382        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1383    prefix = 'form'
1384    omit_fields = (
1385        'password', 'suspended', 'phone', 'date_of_birth',
1386        'adm_code', 'sex', 'suspended_comment', 'current_level',
1387        'flash_notice')
1388
1389    @property
1390    def title(self):
1391        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1392        return translate(_('Level Data'), 'waeup.kofa',
1393            target_language=portal_language)
1394
1395    @property
1396    def label(self):
1397        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1398        lang = self.request.cookies.get('kofa.language', portal_language)
1399        level_title = translate(self.context.level_title, 'waeup.kofa',
1400            target_language=lang)
1401        return translate(_('Course Registration Slip'),
1402            'waeup.kofa', target_language=portal_language) \
1403            + ' %s' % level_title
1404
1405    @property
1406    def tabletitle(self):
1407        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1408        tabletitle = []
1409        tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
1410            target_language=portal_language))
1411        tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
1412            target_language=portal_language))
1413        tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
1414            target_language=portal_language))
1415        return tabletitle
1416
1417    def render(self):
1418        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1419        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1420        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1421        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1422        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1423        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1424        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1425        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1426        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1427        studentview = StudentBasePDFFormPage(self.context.student,
1428            self.request, self.omit_fields)
1429        students_utils = getUtility(IStudentsUtils)
1430
1431        tabledata = []
1432        tableheader = []
1433        for i in range(1,7):
1434            tabledata.append(sorted(
1435                [value for value in self.context.values() if value.semester == i],
1436                key=lambda value: str(value.semester) + value.code))
1437            tableheader.append([(Code,'code', 2.5),
1438                             (Title,'title', 5),
1439                             (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1440                             (Cred, 'credits', 1.5),
1441                             #(Mand, 'mandatory', 1.5),
1442                             (Score, 'score', 1.5),
1443                             (Grade, 'grade', 1.5),
1444                             #('Auto', 'automatic', 1.5)
1445                             ])
1446        return students_utils.renderPDF(
1447            self, 'course_registration_slip.pdf',
1448            self.context.student, studentview,
1449            tableheader=tableheader,
1450            tabledata=tabledata,
1451            omit_fields=self.omit_fields
1452            )
1453
1454class StudyLevelManageFormPage(KofaEditFormPage):
1455    """ Page to edit the student study level data
1456    """
1457    grok.context(IStudentStudyLevel)
1458    grok.name('manage')
1459    grok.require('waeup.manageStudent')
1460    grok.template('studylevelmanagepage')
1461    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1462        'validation_date', 'validated_by', 'total_credits', 'gpa', 'level')
1463    pnav = 4
1464    taboneactions = [_('Save'),_('Cancel')]
1465    tabtwoactions = [_('Add course ticket'),
1466        _('Remove selected tickets'),_('Cancel')]
1467    placeholder = _('Enter valid course code')
1468
1469    def update(self, ADD=None, course=None):
1470        if not self.context.__parent__.is_current:
1471            emit_lock_message(self)
1472            return
1473        super(StudyLevelManageFormPage, self).update()
1474        if ADD is not None:
1475            if not course:
1476                self.flash(_('No valid course code entered.'), type="warning")
1477                self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1478                return
1479            cat = queryUtility(ICatalog, name='courses_catalog')
1480            result = cat.searchResults(code=(course, course))
1481            if len(result) != 1:
1482                self.flash(_('Course not found.'), type="warning")
1483            else:
1484                course = list(result)[0]
1485                addCourseTicket(self, course)
1486            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1487        return
1488
1489    @property
1490    def translated_values(self):
1491        return translated_values(self)
1492
1493    @property
1494    def label(self):
1495        # Here we know that the cookie has been set
1496        lang = self.request.cookies.get('kofa.language')
1497        level_title = translate(self.context.level_title, 'waeup.kofa',
1498            target_language=lang)
1499        return _('Manage study level ${a}',
1500            mapping = {'a':level_title})
1501
1502    @action(_('Save'), style='primary')
1503    def save(self, **data):
1504        msave(self, **data)
1505        return
1506
1507    @jsaction(_('Remove selected tickets'))
1508    def delCourseTicket(self, **data):
1509        form = self.request.form
1510        if 'val_id' in form:
1511            child_id = form['val_id']
1512        else:
1513            self.flash(_('No ticket selected.'), type="warning")
1514            self.redirect(self.url(self.context, '@@manage')+'#tab2')
1515            return
1516        if not isinstance(child_id, list):
1517            child_id = [child_id]
1518        deleted = []
1519        for id in child_id:
1520            del self.context[id]
1521            deleted.append(id)
1522        if len(deleted):
1523            self.flash(_('Successfully removed: ${a}',
1524                mapping = {'a':', '.join(deleted)}))
1525            self.context.writeLogMessage(
1526                self,'removed: %s at %s' %
1527                (', '.join(deleted), self.context.level))
1528        self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1529        return
1530
1531class ValidateCoursesView(UtilityView, grok.View):
1532    """ Validate course list by course adviser
1533    """
1534    grok.context(IStudentStudyLevel)
1535    grok.name('validate_courses')
1536    grok.require('waeup.validateStudent')
1537
1538    def update(self):
1539        if not self.context.__parent__.is_current:
1540            emit_lock_message(self)
1541            return
1542        if str(self.context.__parent__.current_level) != self.context.__name__:
1543            self.flash(_('This is not the student\'s current level.'),
1544                       type="danger")
1545        elif self.context.student.state == REGISTERED:
1546            IWorkflowInfo(self.context.student).fireTransition(
1547                'validate_courses')
1548            self.flash(_('Course list has been validated.'))
1549        else:
1550            self.flash(_('Student is in the wrong state.'), type="warning")
1551        self.redirect(self.url(self.context))
1552        return
1553
1554    def render(self):
1555        return
1556
1557class RejectCoursesView(UtilityView, grok.View):
1558    """ Reject course list by course adviser
1559    """
1560    grok.context(IStudentStudyLevel)
1561    grok.name('reject_courses')
1562    grok.require('waeup.validateStudent')
1563
1564    def update(self):
1565        if not self.context.__parent__.is_current:
1566            emit_lock_message(self)
1567            return
1568        if str(self.context.__parent__.current_level) != self.context.__name__:
1569            self.flash(_('This is not the student\'s current level.'),
1570                       type="danger")
1571            self.redirect(self.url(self.context))
1572            return
1573        elif self.context.student.state == VALIDATED:
1574            IWorkflowInfo(self.context.student).fireTransition('reset8')
1575            message = _('Course list request has been annulled.')
1576            self.flash(message)
1577        elif self.context.student.state == REGISTERED:
1578            IWorkflowInfo(self.context.student).fireTransition('reset7')
1579            message = _('Course list has been unregistered.')
1580            self.flash(message)
1581        else:
1582            self.flash(_('Student is in the wrong state.'), type="warning")
1583            self.redirect(self.url(self.context))
1584            return
1585        args = {'subject':message}
1586        self.redirect(self.url(self.context.student) +
1587            '/contactstudent?%s' % urlencode(args))
1588        return
1589
1590    def render(self):
1591        return
1592
1593class UnregisterCoursesView(UtilityView, grok.View):
1594    """Unregister course list by student
1595    """
1596    grok.context(IStudentStudyLevel)
1597    grok.name('unregister_courses')
1598    grok.require('waeup.handleStudent')
1599
1600    def update(self):
1601        if not self.context.__parent__.is_current:
1602            emit_lock_message(self)
1603            return
1604        try:
1605            deadline = grok.getSite()['configuration'][
1606                str(self.context.level_session)].coursereg_deadline
1607        except (TypeError, KeyError):
1608            deadline = None
1609        if deadline and deadline < datetime.now(pytz.utc):
1610            self.flash(_(
1611                "Course registration has ended. "
1612                "Unregistration is disabled."), type="warning")
1613        elif str(self.context.__parent__.current_level) != self.context.__name__:
1614            self.flash(_('This is not your current level.'), type="danger")
1615        elif self.context.student.state == REGISTERED:
1616            IWorkflowInfo(self.context.student).fireTransition('reset7')
1617            message = _('Course list has been unregistered.')
1618            self.flash(message)
1619        else:
1620            self.flash(_('You are in the wrong state.'), type="warning")
1621        self.redirect(self.url(self.context))
1622        return
1623
1624    def render(self):
1625        return
1626
1627class CourseTicketAddFormPage(KofaAddFormPage):
1628    """Add a course ticket.
1629    """
1630    grok.context(IStudentStudyLevel)
1631    grok.name('add')
1632    grok.require('waeup.manageStudent')
1633    label = _('Add course ticket')
1634    form_fields = grok.AutoFields(ICourseTicketAdd)
1635    pnav = 4
1636
1637    def update(self):
1638        if not self.context.__parent__.is_current:
1639            emit_lock_message(self)
1640            return
1641        super(CourseTicketAddFormPage, self).update()
1642        return
1643
1644    @action(_('Add course ticket'), style='primary')
1645    def addCourseTicket(self, **data):
1646        course = data['course']
1647        success = addCourseTicket(self, course)
1648        if success:
1649            self.redirect(self.url(self.context, u'@@manage')+'#tab2')
1650        return
1651
1652    @action(_('Cancel'), validator=NullValidator)
1653    def cancel(self, **data):
1654        self.redirect(self.url(self.context))
1655
1656class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1657    """ Page to display course tickets
1658    """
1659    grok.context(ICourseTicket)
1660    grok.name('index')
1661    grok.require('waeup.viewStudent')
1662    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1663    grok.template('courseticketpage')
1664    pnav = 4
1665
1666    @property
1667    def label(self):
1668        return _('${a}: Course Ticket ${b}', mapping = {
1669            'a':self.context.student.display_fullname,
1670            'b':self.context.code})
1671
1672class CourseTicketManageFormPage(KofaEditFormPage):
1673    """ Page to manage course tickets
1674    """
1675    grok.context(ICourseTicket)
1676    grok.name('manage')
1677    grok.require('waeup.manageStudent')
1678    form_fields = grok.AutoFields(ICourseTicket).omit('course_category')
1679    form_fields['title'].for_display = True
1680    form_fields['fcode'].for_display = True
1681    form_fields['dcode'].for_display = True
1682    form_fields['semester'].for_display = True
1683    form_fields['passmark'].for_display = True
1684    form_fields['credits'].for_display = True
1685    form_fields['mandatory'].for_display = False
1686    form_fields['automatic'].for_display = True
1687    form_fields['carry_over'].for_display = True
1688    pnav = 4
1689    grok.template('courseticketmanagepage')
1690
1691    @property
1692    def label(self):
1693        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1694
1695    @action('Save', style='primary')
1696    def save(self, **data):
1697        msave(self, **data)
1698        return
1699
1700class PaymentsManageFormPage(KofaEditFormPage):
1701    """ Page to manage the student payments
1702
1703    This manage form page is for both students and students officers.
1704    """
1705    grok.context(IStudentPaymentsContainer)
1706    grok.name('index')
1707    grok.require('waeup.viewStudent')
1708    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1709    grok.template('paymentsmanagepage')
1710    pnav = 4
1711
1712    @property
1713    def manage_payments_allowed(self):
1714        return checkPermission('waeup.payStudent', self.context)
1715
1716    def unremovable(self, ticket):
1717        usertype = getattr(self.request.principal, 'user_type', None)
1718        if not usertype:
1719            return False
1720        if not self.manage_payments_allowed:
1721            return True
1722        return (self.request.principal.user_type == 'student' and ticket.r_code)
1723
1724    @property
1725    def label(self):
1726        return _('${a}: Payments',
1727            mapping = {'a':self.context.__parent__.display_fullname})
1728
1729    @jsaction(_('Remove selected tickets'))
1730    def delPaymentTicket(self, **data):
1731        form = self.request.form
1732        if 'val_id' in form:
1733            child_id = form['val_id']
1734        else:
1735            self.flash(_('No payment selected.'), type="warning")
1736            self.redirect(self.url(self.context))
1737            return
1738        if not isinstance(child_id, list):
1739            child_id = [child_id]
1740        deleted = []
1741        for id in child_id:
1742            # Students are not allowed to remove used payment tickets
1743            ticket = self.context.get(id, None)
1744            if ticket is not None and not self.unremovable(ticket):
1745                del self.context[id]
1746                deleted.append(id)
1747        if len(deleted):
1748            self.flash(_('Successfully removed: ${a}',
1749                mapping = {'a': ', '.join(deleted)}))
1750            self.context.writeLogMessage(
1751                self,'removed: %s' % ', '.join(deleted))
1752        self.redirect(self.url(self.context))
1753        return
1754
1755    #@action(_('Add online payment ticket'))
1756    #def addPaymentTicket(self, **data):
1757    #    self.redirect(self.url(self.context, '@@addop'))
1758
1759class OnlinePaymentAddFormPage(KofaAddFormPage):
1760    """ Page to add an online payment ticket
1761    """
1762    grok.context(IStudentPaymentsContainer)
1763    grok.name('addop')
1764    grok.template('onlinepaymentaddform')
1765    grok.require('waeup.payStudent')
1766    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1767        'p_category')
1768    label = _('Add online payment')
1769    pnav = 4
1770
1771    @property
1772    def selectable_categories(self):
1773        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1774        return sorted(categories.items())
1775
1776    @action(_('Create ticket'), style='primary')
1777    def createTicket(self, **data):
1778        p_category = data['p_category']
1779        previous_session = data.get('p_session', None)
1780        previous_level = data.get('p_level', None)
1781        student = self.context.__parent__
1782        # The hostel_application payment category is temporarily used
1783        # by Uniben.
1784        if p_category in ('bed_allocation', 'hostel_application') and student[
1785            'studycourse'].current_session != grok.getSite()[
1786            'hostels'].accommodation_session:
1787                self.flash(
1788                    _('Your current session does not match ' + \
1789                    'accommodation session.'), type="danger")
1790                return
1791        if 'maintenance' in p_category:
1792            current_session = str(student['studycourse'].current_session)
1793            if not current_session in student['accommodation']:
1794                self.flash(_('You have not yet booked accommodation.'),
1795                           type="warning")
1796                return
1797        students_utils = getUtility(IStudentsUtils)
1798        error, payment = students_utils.setPaymentDetails(
1799            p_category, student, previous_session, previous_level)
1800        if error is not None:
1801            self.flash(error, type="danger")
1802            return
1803        if p_category == 'transfer':
1804            payment.p_item = self.request.form['new_programme']
1805        self.context[payment.p_id] = payment
1806        self.flash(_('Payment ticket created.'))
1807        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1808        self.redirect(self.url(self.context))
1809        return
1810
1811    @action(_('Cancel'), validator=NullValidator)
1812    def cancel(self, **data):
1813        self.redirect(self.url(self.context))
1814
1815class PreviousPaymentAddFormPage(KofaAddFormPage):
1816    """ Page to add an online payment ticket for previous sessions.
1817    """
1818    grok.context(IStudentPaymentsContainer)
1819    grok.name('addpp')
1820    grok.require('waeup.payStudent')
1821    form_fields = grok.AutoFields(IStudentPreviousPayment)
1822    label = _('Add previous session online payment')
1823    pnav = 4
1824
1825    def update(self):
1826        if self.context.student.before_payment:
1827            self.flash(_("No previous payment to be made."), type="warning")
1828            self.redirect(self.url(self.context))
1829        super(PreviousPaymentAddFormPage, self).update()
1830        return
1831
1832    @action(_('Create ticket'), style='primary')
1833    def createTicket(self, **data):
1834        p_category = data['p_category']
1835        previous_session = data.get('p_session', None)
1836        previous_level = data.get('p_level', None)
1837        student = self.context.__parent__
1838        students_utils = getUtility(IStudentsUtils)
1839        error, payment = students_utils.setPaymentDetails(
1840            p_category, student, previous_session, previous_level)
1841        if error is not None:
1842            self.flash(error, type="danger")
1843            return
1844        self.context[payment.p_id] = payment
1845        self.flash(_('Payment ticket created.'))
1846        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1847        self.redirect(self.url(self.context))
1848        return
1849
1850    @action(_('Cancel'), validator=NullValidator)
1851    def cancel(self, **data):
1852        self.redirect(self.url(self.context))
1853
1854class BalancePaymentAddFormPage(KofaAddFormPage):
1855    """ Page to add an online payment which can balance s previous session
1856    payment.
1857    """
1858    grok.context(IStudentPaymentsContainer)
1859    grok.name('addbp')
1860    grok.require('waeup.manageStudent')
1861    form_fields = grok.AutoFields(IStudentBalancePayment)
1862    label = _('Add balance')
1863    pnav = 4
1864
1865    @action(_('Create ticket'), style='primary')
1866    def createTicket(self, **data):
1867        p_category = data['p_category']
1868        balance_session = data.get('balance_session', None)
1869        balance_level = data.get('balance_level', None)
1870        balance_amount = data.get('balance_amount', None)
1871        student = self.context.__parent__
1872        students_utils = getUtility(IStudentsUtils)
1873        error, payment = students_utils.setBalanceDetails(
1874            p_category, student, balance_session,
1875            balance_level, balance_amount)
1876        if error is not None:
1877            self.flash(error, type="danger")
1878            return
1879        self.context[payment.p_id] = payment
1880        self.flash(_('Payment ticket created.'))
1881        self.context.writeLogMessage(self,'added: %s' % payment.p_id)
1882        self.redirect(self.url(self.context))
1883        return
1884
1885    @action(_('Cancel'), validator=NullValidator)
1886    def cancel(self, **data):
1887        self.redirect(self.url(self.context))
1888
1889class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1890    """ Page to view an online payment ticket
1891    """
1892    grok.context(IStudentOnlinePayment)
1893    grok.name('index')
1894    grok.require('waeup.viewStudent')
1895    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1896    form_fields[
1897        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1898    form_fields[
1899        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1900    pnav = 4
1901
1902    @property
1903    def label(self):
1904        return _('${a}: Online Payment Ticket ${b}', mapping = {
1905            'a':self.context.student.display_fullname,
1906            'b':self.context.p_id})
1907
1908class OnlinePaymentApproveView(UtilityView, grok.View):
1909    """ Callback view
1910    """
1911    grok.context(IStudentOnlinePayment)
1912    grok.name('approve')
1913    grok.require('waeup.managePortal')
1914
1915    def update(self):
1916        flashtype, msg, log = self.context.approveStudentPayment()
1917        if log is not None:
1918            # Add log message to students.log
1919            self.context.writeLogMessage(self,log)
1920            # Add log message to payments.log
1921            self.context.logger.info(
1922                '%s,%s,%s,%s,%s,,,,,,' % (
1923                self.context.student.student_id,
1924                self.context.p_id, self.context.p_category,
1925                self.context.amount_auth, self.context.r_code))
1926        self.flash(msg, type=flashtype)
1927        return
1928
1929    def render(self):
1930        self.redirect(self.url(self.context, '@@index'))
1931        return
1932
1933class OnlinePaymentFakeApproveView(OnlinePaymentApproveView):
1934    """ Approval view for students.
1935
1936    This view is used for browser tests only and
1937    must be neutralized in custom pages!
1938    """
1939    grok.name('fake_approve')
1940    grok.require('waeup.payStudent')
1941
1942class ExportPDFPaymentSlip(UtilityView, grok.View):
1943    """Deliver a PDF slip of the context.
1944    """
1945    grok.context(IStudentOnlinePayment)
1946    grok.name('payment_slip.pdf')
1947    grok.require('waeup.viewStudent')
1948    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1949    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1950    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1951    prefix = 'form'
1952    note = None
1953    omit_fields = (
1954        'password', 'suspended', 'phone', 'date_of_birth',
1955        'adm_code', 'sex', 'suspended_comment', 'current_level',
1956        'flash_notice')
1957
1958    @property
1959    def title(self):
1960        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1961        return translate(_('Payment Data'), 'waeup.kofa',
1962            target_language=portal_language)
1963
1964    @property
1965    def label(self):
1966        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1967        return translate(_('Online Payment Slip'),
1968            'waeup.kofa', target_language=portal_language) \
1969            + ' %s' % self.context.p_id
1970
1971    def render(self):
1972        #if self.context.p_state != 'paid':
1973        #    self.flash('Ticket not yet paid.')
1974        #    self.redirect(self.url(self.context))
1975        #    return
1976        studentview = StudentBasePDFFormPage(self.context.student,
1977            self.request, self.omit_fields)
1978        students_utils = getUtility(IStudentsUtils)
1979        return students_utils.renderPDF(self, 'payment_slip.pdf',
1980            self.context.student, studentview, note=self.note,
1981            omit_fields=self.omit_fields)
1982
1983
1984class AccommodationManageFormPage(KofaEditFormPage):
1985    """ Page to manage bed tickets.
1986
1987    This manage form page is for both students and students officers.
1988    """
1989    grok.context(IStudentAccommodation)
1990    grok.name('index')
1991    grok.require('waeup.handleAccommodation')
1992    form_fields = grok.AutoFields(IStudentAccommodation)
1993    grok.template('accommodationmanagepage')
1994    pnav = 4
1995    with_hostel_selection = True
1996
1997    @property
1998    def actionsgroup1(self):
1999        if not self.with_hostel_selection:
2000            return []
2001        students_utils = getUtility(IStudentsUtils)
2002        acc_details  = students_utils.getAccommodationDetails(self.context.student)
2003        error_message = students_utils.checkAccommodationRequirements(
2004            self.context.student, acc_details)
2005        if error_message:
2006            return []
2007        return [_('Save')]
2008
2009    @property
2010    def actionsgroup2(self):
2011        if getattr(self.request.principal, 'user_type', None) == 'student':
2012            return [_('Book accommodation')]
2013        return [_('Book accommodation'), _('Remove selected')]
2014
2015    @property
2016    def label(self):
2017        return _('${a}: Accommodation',
2018            mapping = {'a':self.context.__parent__.display_fullname})
2019
2020    @property
2021    def desired_hostel(self):
2022        if self.context.desired_hostel:
2023            hostel = grok.getSite()['hostels'].get(self.context.desired_hostel)
2024            if hostel is not None:
2025                return hostel.hostel_name
2026        return
2027
2028    def getHostels(self):
2029        """Get a list of all stored hostels.
2030        """
2031        yield(dict(name=None, title='--', selected=''))
2032        for val in grok.getSite()['hostels'].values():
2033            selected = ''
2034            if val.hostel_id == self.context.desired_hostel:
2035                selected = 'selected'
2036            yield(dict(name=val.hostel_id, title=val.hostel_name,
2037                       selected=selected))
2038
2039    @action(_('Save'), style='primary')
2040    def save(self):
2041        hostel = self.request.form.get('hostel', None)
2042        self.context.desired_hostel = hostel
2043        self.flash(_('Your selection has been saved.'))
2044        return
2045
2046    @action(_('Book accommodation'), style='primary')
2047    def bookAccommodation(self, **data):
2048        self.redirect(self.url(self.context, 'add'))
2049        return
2050
2051    @jsaction(_('Remove selected'))
2052    def delBedTickets(self, **data):
2053        if getattr(self.request.principal, 'user_type', None) == 'student':
2054            self.flash(_('You are not allowed to remove bed tickets.'),
2055                       type="warning")
2056            self.redirect(self.url(self.context))
2057            return
2058        form = self.request.form
2059        if 'val_id' in form:
2060            child_id = form['val_id']
2061        else:
2062            self.flash(_('No bed ticket selected.'), type="warning")
2063            self.redirect(self.url(self.context))
2064            return
2065        if not isinstance(child_id, list):
2066            child_id = [child_id]
2067        deleted = []
2068        for id in child_id:
2069            del self.context[id]
2070            deleted.append(id)
2071        if len(deleted):
2072            self.flash(_('Successfully removed: ${a}',
2073                mapping = {'a':', '.join(deleted)}))
2074            self.context.writeLogMessage(
2075                self,'removed: % s' % ', '.join(deleted))
2076        self.redirect(self.url(self.context))
2077        return
2078
2079class BedTicketAddPage(KofaPage):
2080    """ Page to add a bed ticket
2081    """
2082    grok.context(IStudentAccommodation)
2083    grok.name('add')
2084    grok.require('waeup.handleAccommodation')
2085    grok.template('enterpin')
2086    ac_prefix = 'HOS'
2087    label = _('Add bed ticket')
2088    pnav = 4
2089    buttonname = _('Create bed ticket')
2090    notice = ''
2091    with_ac = True
2092
2093    def update(self, SUBMIT=None):
2094        student = self.context.student
2095        students_utils = getUtility(IStudentsUtils)
2096        acc_details  = students_utils.getAccommodationDetails(student)
2097        error_message = students_utils.checkAccommodationRequirements(
2098            student, acc_details)
2099        if error_message:
2100            self.flash(error_message, type="warning")
2101            self.redirect(self.url(self.context))
2102            return
2103        if self.with_ac:
2104            self.ac_series = self.request.form.get('ac_series', None)
2105            self.ac_number = self.request.form.get('ac_number', None)
2106        if SUBMIT is None:
2107            return
2108        if self.with_ac:
2109            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2110            code = get_access_code(pin)
2111            if not code:
2112                self.flash(_('Activation code is invalid.'), type="warning")
2113                return
2114        # Search and book bed
2115        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2116        entries = cat.searchResults(
2117            owner=(student.student_id,student.student_id))
2118        if len(entries):
2119            # If bed space has been manually allocated use this bed
2120            manual = True
2121            bed = [entry for entry in entries][0]
2122            # Safety belt for paranoids: Does this bed really exist on portal?
2123            # XXX: Can be remove if nobody complains.
2124            if bed.__parent__.__parent__ is None:
2125                self.flash(_('System error: Please contact the adminsitrator.'),
2126                           type="danger")
2127                self.context.writeLogMessage(
2128                    self, 'fatal error: %s' % bed.bed_id)
2129                return
2130        else:
2131            # else search for other available beds
2132            manual = False
2133            entries = cat.searchResults(
2134                bed_type=(acc_details['bt'],acc_details['bt']))
2135            available_beds = [
2136                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2137            if available_beds:
2138                students_utils = getUtility(IStudentsUtils)
2139                bed = students_utils.selectBed(
2140                    available_beds, self.context.desired_hostel)
2141                if bed is None:
2142                    self.flash(_(
2143                        'There is no free bed in your desired hostel. '
2144                        'Please try another hostel.'),
2145                        type="warning")
2146                    self.redirect(self.url(self.context))
2147                    return
2148                # Safety belt for paranoids: Does this bed really exist
2149                # in portal?
2150                # XXX: Can be remove if nobody complains.
2151                if bed.__parent__.__parent__ is None:
2152                    self.flash(_(
2153                        'System error: Please contact the administrator.'),
2154                        type="warning")
2155                    self.context.writeLogMessage(
2156                        self, 'fatal error: %s' % bed.bed_id)
2157                    return
2158                bed.bookBed(student.student_id)
2159            else:
2160                self.flash(_('There is no free bed in your category ${a}.',
2161                    mapping = {'a':acc_details['bt']}), type="warning")
2162                self.redirect(self.url(self.context))
2163                return
2164        if self.with_ac:
2165            # Mark pin as used (this also fires a pin related transition)
2166            if code.state == USED:
2167                self.flash(_('Activation code has already been used.'),
2168                           type="warning")
2169                if not manual:
2170                    # Release the previously booked bed
2171                    bed.owner = NOT_OCCUPIED
2172                    # Catalog must be informed
2173                    notify(grok.ObjectModifiedEvent(bed))
2174                return
2175            else:
2176                comment = _(u'invalidated')
2177                # Here we know that the ac is in state initialized so we do not
2178                # expect an exception, but the owner might be different
2179                success = invalidate_accesscode(
2180                    pin, comment, self.context.student.student_id)
2181                if not success:
2182                    self.flash(_('You are not the owner of this access code.'),
2183                               type="warning")
2184                    if not manual:
2185                        # Release the previously booked bed
2186                        bed.owner = NOT_OCCUPIED
2187                        # Catalog must be informed
2188                        notify(grok.ObjectModifiedEvent(bed))
2189                    return
2190        # Create bed ticket
2191        bedticket = createObject(u'waeup.BedTicket')
2192        if self.with_ac:
2193            bedticket.booking_code = pin
2194        bedticket.booking_session = acc_details['booking_session']
2195        bedticket.bed_type = acc_details['bt']
2196        bedticket.bed = bed
2197        hall_title = bed.__parent__.hostel_name
2198        coordinates = bed.coordinates[1:]
2199        block, room_nr, bed_nr = coordinates
2200        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2201            'a':hall_title, 'b':block,
2202            'c':room_nr, 'd':bed_nr,
2203            'e':bed.bed_type})
2204        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2205        bedticket.bed_coordinates = translate(
2206            bc, 'waeup.kofa',target_language=portal_language)
2207        self.context.addBedTicket(bedticket)
2208        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
2209        self.flash(_('Bed ticket created and bed booked: ${a}',
2210            mapping = {'a':bedticket.display_coordinates}))
2211        self.redirect(self.url(self.context))
2212        return
2213
2214class BedTicketDisplayFormPage(KofaDisplayFormPage):
2215    """ Page to display bed tickets
2216    """
2217    grok.context(IBedTicket)
2218    grok.name('index')
2219    grok.require('waeup.handleAccommodation')
2220    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2221    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2222    pnav = 4
2223
2224    @property
2225    def label(self):
2226        return _('Bed Ticket for Session ${a}',
2227            mapping = {'a':self.context.getSessionString()})
2228
2229class ExportPDFBedTicketSlip(UtilityView, grok.View):
2230    """Deliver a PDF slip of the context.
2231    """
2232    grok.context(IBedTicket)
2233    grok.name('bed_allocation_slip.pdf')
2234    grok.require('waeup.handleAccommodation')
2235    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
2236    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
2237    prefix = 'form'
2238    omit_fields = (
2239        'password', 'suspended', 'phone', 'adm_code',
2240        'suspended_comment', 'date_of_birth', 'current_level',
2241        'flash_notice')
2242
2243    @property
2244    def title(self):
2245        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2246        return translate(_('Bed Allocation Data'), 'waeup.kofa',
2247            target_language=portal_language)
2248
2249    @property
2250    def label(self):
2251        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2252        #return translate(_('Bed Allocation: '),
2253        #    'waeup.kofa', target_language=portal_language) \
2254        #    + ' %s' % self.context.bed_coordinates
2255        return translate(_('Bed Allocation Slip'),
2256            'waeup.kofa', target_language=portal_language) \
2257            + ' %s' % self.context.getSessionString()
2258
2259    def render(self):
2260        studentview = StudentBasePDFFormPage(self.context.student,
2261            self.request, self.omit_fields)
2262        students_utils = getUtility(IStudentsUtils)
2263        return students_utils.renderPDF(
2264            self, 'bed_allocation_slip.pdf',
2265            self.context.student, studentview,
2266            omit_fields=self.omit_fields)
2267
2268class BedTicketRelocationView(UtilityView, grok.View):
2269    """ Callback view
2270    """
2271    grok.context(IBedTicket)
2272    grok.name('relocate')
2273    grok.require('waeup.manageHostels')
2274
2275    # Relocate student if student parameters have changed or the bed_type
2276    # of the bed has changed
2277    def update(self):
2278        success, msg = self.context.relocateStudent()
2279        if not success:
2280            self.flash(msg, type="warning")
2281        else:
2282            self.flash(msg)
2283        self.redirect(self.url(self.context))
2284        return
2285
2286    def render(self):
2287        return
2288
2289class StudentHistoryPage(KofaPage):
2290    """ Page to display student history
2291    """
2292    grok.context(IStudent)
2293    grok.name('history')
2294    grok.require('waeup.viewStudent')
2295    grok.template('studenthistory')
2296    pnav = 4
2297
2298    @property
2299    def label(self):
2300        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2301
2302# Pages for students only
2303
2304class StudentBaseEditFormPage(KofaEditFormPage):
2305    """ View to edit student base data
2306    """
2307    grok.context(IStudent)
2308    grok.name('edit_base')
2309    grok.require('waeup.handleStudent')
2310    form_fields = grok.AutoFields(IStudentBase).select(
2311        'email', 'phone')
2312    label = _('Edit base data')
2313    pnav = 4
2314
2315    @action(_('Save'), style='primary')
2316    def save(self, **data):
2317        msave(self, **data)
2318        return
2319
2320class StudentChangePasswordPage(KofaEditFormPage):
2321    """ View to edit student passwords
2322    """
2323    grok.context(IStudent)
2324    grok.name('change_password')
2325    grok.require('waeup.handleStudent')
2326    grok.template('change_password')
2327    label = _('Change password')
2328    pnav = 4
2329
2330    @action(_('Save'), style='primary')
2331    def save(self, **data):
2332        form = self.request.form
2333        password = form.get('change_password', None)
2334        password_ctl = form.get('change_password_repeat', None)
2335        if password:
2336            validator = getUtility(IPasswordValidator)
2337            errors = validator.validate_password(password, password_ctl)
2338            if not errors:
2339                IUserAccount(self.context).setPassword(password)
2340                # Unset temporary password
2341                self.context.temp_password = None
2342                self.context.writeLogMessage(self, 'saved: password')
2343                self.flash(_('Password changed.'))
2344            else:
2345                self.flash( ' '.join(errors), type="warning")
2346        return
2347
2348class StudentFilesUploadPage(KofaPage):
2349    """ View to upload files by student
2350    """
2351    grok.context(IStudent)
2352    grok.name('change_portrait')
2353    grok.require('waeup.uploadStudentFile')
2354    grok.template('filesuploadpage')
2355    label = _('Upload portrait')
2356    pnav = 4
2357
2358    def update(self):
2359        PORTRAIT_CHANGE_STATES = getUtility(IStudentsUtils).PORTRAIT_CHANGE_STATES
2360        if self.context.student.state not in PORTRAIT_CHANGE_STATES:
2361            emit_lock_message(self)
2362            return
2363        super(StudentFilesUploadPage, self).update()
2364        return
2365
2366class StartClearancePage(KofaPage):
2367    grok.context(IStudent)
2368    grok.name('start_clearance')
2369    grok.require('waeup.handleStudent')
2370    grok.template('enterpin')
2371    label = _('Start clearance')
2372    ac_prefix = 'CLR'
2373    notice = ''
2374    pnav = 4
2375    buttonname = _('Start clearance now')
2376    with_ac = True
2377
2378    @property
2379    def all_required_fields_filled(self):
2380        if not self.context.email:
2381            return _("Email address is missing."), 'edit_base'
2382        if not self.context.phone:
2383            return _("Phone number is missing."), 'edit_base'
2384        return
2385
2386    @property
2387    def portrait_uploaded(self):
2388        store = getUtility(IExtFileStore)
2389        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2390            return True
2391        return False
2392
2393    def update(self, SUBMIT=None):
2394        if not self.context.state == ADMITTED:
2395            self.flash(_("Wrong state"), type="warning")
2396            self.redirect(self.url(self.context))
2397            return
2398        if not self.portrait_uploaded:
2399            self.flash(_("No portrait uploaded."), type="warning")
2400            self.redirect(self.url(self.context, 'change_portrait'))
2401            return
2402        if self.all_required_fields_filled:
2403            arf_warning = self.all_required_fields_filled[0]
2404            arf_redirect = self.all_required_fields_filled[1]
2405            self.flash(arf_warning, type="warning")
2406            self.redirect(self.url(self.context, arf_redirect))
2407            return
2408        if self.with_ac:
2409            self.ac_series = self.request.form.get('ac_series', None)
2410            self.ac_number = self.request.form.get('ac_number', None)
2411        if SUBMIT is None:
2412            return
2413        if self.with_ac:
2414            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2415            code = get_access_code(pin)
2416            if not code:
2417                self.flash(_('Activation code is invalid.'), type="warning")
2418                return
2419            if code.state == USED:
2420                self.flash(_('Activation code has already been used.'),
2421                           type="warning")
2422                return
2423            # Mark pin as used (this also fires a pin related transition)
2424            # and fire transition start_clearance
2425            comment = _(u"invalidated")
2426            # Here we know that the ac is in state initialized so we do not
2427            # expect an exception, but the owner might be different
2428            if not invalidate_accesscode(pin, comment, self.context.student_id):
2429                self.flash(_('You are not the owner of this access code.'),
2430                           type="warning")
2431                return
2432            self.context.clr_code = pin
2433        IWorkflowInfo(self.context).fireTransition('start_clearance')
2434        self.flash(_('Clearance process has been started.'))
2435        self.redirect(self.url(self.context,'cedit'))
2436        return
2437
2438class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2439    """ View to edit student clearance data by student
2440    """
2441    grok.context(IStudent)
2442    grok.name('cedit')
2443    grok.require('waeup.handleStudent')
2444    label = _('Edit clearance data')
2445
2446    @property
2447    def form_fields(self):
2448        if self.context.is_postgrad:
2449            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2450                'clr_code', 'officer_comment')
2451        else:
2452            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2453                'clr_code', 'officer_comment')
2454        return form_fields
2455
2456    def update(self):
2457        if self.context.clearance_locked:
2458            emit_lock_message(self)
2459            return
2460        return super(StudentClearanceEditFormPage, self).update()
2461
2462    @action(_('Save'), style='primary')
2463    def save(self, **data):
2464        self.applyData(self.context, **data)
2465        self.flash(_('Clearance form has been saved.'))
2466        return
2467
2468    def dataNotComplete(self):
2469        """To be implemented in the customization package.
2470        """
2471        return False
2472
2473    @action(_('Save and request clearance'), style='primary',
2474            warning=_('You can not edit your data after '
2475            'requesting clearance. You really want to request clearance now?'))
2476    def requestClearance(self, **data):
2477        self.applyData(self.context, **data)
2478        if self.dataNotComplete():
2479            self.flash(self.dataNotComplete(), type="warning")
2480            return
2481        self.flash(_('Clearance form has been saved.'))
2482        if self.context.clr_code:
2483            self.redirect(self.url(self.context, 'request_clearance'))
2484        else:
2485            # We bypass the request_clearance page if student
2486            # has been imported in state 'clearance started' and
2487            # no clr_code was entered before.
2488            state = IWorkflowState(self.context).getState()
2489            if state != CLEARANCE:
2490                # This shouldn't happen, but the application officer
2491                # might have forgotten to lock the form after changing the state
2492                self.flash(_('This form cannot be submitted. Wrong state!'),
2493                           type="danger")
2494                return
2495            IWorkflowInfo(self.context).fireTransition('request_clearance')
2496            self.flash(_('Clearance has been requested.'))
2497            self.redirect(self.url(self.context))
2498        return
2499
2500class RequestClearancePage(KofaPage):
2501    grok.context(IStudent)
2502    grok.name('request_clearance')
2503    grok.require('waeup.handleStudent')
2504    grok.template('enterpin')
2505    label = _('Request clearance')
2506    notice = _('Enter the CLR access code used for starting clearance.')
2507    ac_prefix = 'CLR'
2508    pnav = 4
2509    buttonname = _('Request clearance now')
2510    with_ac = True
2511
2512    def update(self, SUBMIT=None):
2513        if self.with_ac:
2514            self.ac_series = self.request.form.get('ac_series', None)
2515            self.ac_number = self.request.form.get('ac_number', None)
2516        if SUBMIT is None:
2517            return
2518        if self.with_ac:
2519            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2520            if self.context.clr_code and self.context.clr_code != pin:
2521                self.flash(_("This isn't your CLR access code."), type="danger")
2522                return
2523        state = IWorkflowState(self.context).getState()
2524        if state != CLEARANCE:
2525            # This shouldn't happen, but the application officer
2526            # might have forgotten to lock the form after changing the state
2527            self.flash(_('This form cannot be submitted. Wrong state!'),
2528                       type="danger")
2529            return
2530        IWorkflowInfo(self.context).fireTransition('request_clearance')
2531        self.flash(_('Clearance has been requested.'))
2532        self.redirect(self.url(self.context))
2533        return
2534
2535class StartSessionPage(KofaPage):
2536    grok.context(IStudentStudyCourse)
2537    grok.name('start_session')
2538    grok.require('waeup.handleStudent')
2539    grok.template('enterpin')
2540    label = _('Start session')
2541    ac_prefix = 'SFE'
2542    notice = ''
2543    pnav = 4
2544    buttonname = _('Start now')
2545    with_ac = True
2546
2547    def update(self, SUBMIT=None):
2548        if not self.context.is_current:
2549            emit_lock_message(self)
2550            return
2551        super(StartSessionPage, self).update()
2552        if not self.context.next_session_allowed:
2553            self.flash(_("You are not entitled to start session."),
2554                       type="warning")
2555            self.redirect(self.url(self.context))
2556            return
2557        if self.with_ac:
2558            self.ac_series = self.request.form.get('ac_series', None)
2559            self.ac_number = self.request.form.get('ac_number', None)
2560        if SUBMIT is None:
2561            return
2562        if self.with_ac:
2563            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2564            code = get_access_code(pin)
2565            if not code:
2566                self.flash(_('Activation code is invalid.'), type="warning")
2567                return
2568            # Mark pin as used (this also fires a pin related transition)
2569            if code.state == USED:
2570                self.flash(_('Activation code has already been used.'),
2571                           type="warning")
2572                return
2573            else:
2574                comment = _(u"invalidated")
2575                # Here we know that the ac is in state initialized so we do not
2576                # expect an error, but the owner might be different
2577                if not invalidate_accesscode(
2578                    pin,comment,self.context.student.student_id):
2579                    self.flash(_('You are not the owner of this access code.'),
2580                               type="warning")
2581                    return
2582        try:
2583            if self.context.student.state == CLEARED:
2584                IWorkflowInfo(self.context.student).fireTransition(
2585                    'pay_first_school_fee')
2586            elif self.context.student.state == RETURNING:
2587                IWorkflowInfo(self.context.student).fireTransition(
2588                    'pay_school_fee')
2589            elif self.context.student.state == PAID:
2590                IWorkflowInfo(self.context.student).fireTransition(
2591                    'pay_pg_fee')
2592        except ConstraintNotSatisfied:
2593            self.flash(_('An error occurred, please contact the system administrator.'),
2594                       type="danger")
2595            return
2596        self.flash(_('Session started.'))
2597        self.redirect(self.url(self.context))
2598        return
2599
2600class AddStudyLevelFormPage(KofaEditFormPage):
2601    """ Page for students to add current study levels
2602    """
2603    grok.context(IStudentStudyCourse)
2604    grok.name('add')
2605    grok.require('waeup.handleStudent')
2606    grok.template('studyleveladdpage')
2607    form_fields = grok.AutoFields(IStudentStudyCourse)
2608    pnav = 4
2609
2610    @property
2611    def label(self):
2612        studylevelsource = StudyLevelSource().factory
2613        code = self.context.current_level
2614        title = studylevelsource.getTitle(self.context, code)
2615        return _('Add current level ${a}', mapping = {'a':title})
2616
2617    def update(self):
2618        if not self.context.is_current:
2619            emit_lock_message(self)
2620            return
2621        if self.context.student.state != PAID:
2622            emit_lock_message(self)
2623            return
2624        code = self.context.current_level
2625        if code is None:
2626            self.flash(_('Your data are incomplete'), type="danger")
2627            self.redirect(self.url(self.context))
2628            return
2629        super(AddStudyLevelFormPage, self).update()
2630        return
2631
2632    @action(_('Create course list now'), style='primary')
2633    def addStudyLevel(self, **data):
2634        studylevel = createObject(u'waeup.StudentStudyLevel')
2635        studylevel.level = self.context.current_level
2636        studylevel.level_session = self.context.current_session
2637        try:
2638            self.context.addStudentStudyLevel(
2639                self.context.certificate,studylevel)
2640        except KeyError:
2641            self.flash(_('This level exists.'), type="warning")
2642            self.redirect(self.url(self.context))
2643            return
2644        except RequiredMissing:
2645            self.flash(_('Your data are incomplete.'), type="danger")
2646            self.redirect(self.url(self.context))
2647            return
2648        self.flash(_('You successfully created a new course list.'))
2649        self.redirect(self.url(self.context, str(studylevel.level)))
2650        return
2651
2652class StudyLevelEditFormPage(KofaEditFormPage):
2653    """ Page to edit the student study level data by students
2654    """
2655    grok.context(IStudentStudyLevel)
2656    grok.name('edit')
2657    grok.require('waeup.editStudyLevel')
2658    grok.template('studyleveleditpage')
2659    pnav = 4
2660    placeholder = _('Enter valid course code')
2661
2662    def update(self, ADD=None, course=None):
2663        if not self.context.__parent__.is_current:
2664            emit_lock_message(self)
2665            return
2666        if self.context.student.state != PAID or \
2667            not self.context.is_current_level:
2668            emit_lock_message(self)
2669            return
2670        super(StudyLevelEditFormPage, self).update()
2671        if ADD is not None:
2672            if not course:
2673                self.flash(_('No valid course code entered.'), type="warning")
2674                return
2675            cat = queryUtility(ICatalog, name='courses_catalog')
2676            result = cat.searchResults(code=(course, course))
2677            if len(result) != 1:
2678                self.flash(_('Course not found.'), type="warning")
2679                return
2680            course = list(result)[0]
2681            addCourseTicket(self, course)
2682        return
2683
2684    @property
2685    def label(self):
2686        # Here we know that the cookie has been set
2687        lang = self.request.cookies.get('kofa.language')
2688        level_title = translate(self.context.level_title, 'waeup.kofa',
2689            target_language=lang)
2690        return _('Edit course list of ${a}',
2691            mapping = {'a':level_title})
2692
2693    @property
2694    def translated_values(self):
2695        return translated_values(self)
2696
2697    def _delCourseTicket(self, **data):
2698        form = self.request.form
2699        if 'val_id' in form:
2700            child_id = form['val_id']
2701        else:
2702            self.flash(_('No ticket selected.'), type="warning")
2703            self.redirect(self.url(self.context, '@@edit'))
2704            return
2705        if not isinstance(child_id, list):
2706            child_id = [child_id]
2707        deleted = []
2708        for id in child_id:
2709            # Students are not allowed to remove core tickets
2710            if id in self.context and \
2711                self.context[id].removable_by_student:
2712                del self.context[id]
2713                deleted.append(id)
2714        if len(deleted):
2715            self.flash(_('Successfully removed: ${a}',
2716                mapping = {'a':', '.join(deleted)}))
2717            self.context.writeLogMessage(
2718                self,'removed: %s at %s' %
2719                (', '.join(deleted), self.context.level))
2720        self.redirect(self.url(self.context, u'@@edit'))
2721        return
2722
2723    @jsaction(_('Remove selected tickets'))
2724    def delCourseTicket(self, **data):
2725        self._delCourseTicket(**data)
2726        return
2727
2728    def _updateTickets(self, **data):
2729        cat = queryUtility(ICatalog, name='courses_catalog')
2730        invalidated = list()
2731        for value in self.context.values():
2732            result = cat.searchResults(code=(value.code, value.code))
2733            if len(result) != 1:
2734                course = None
2735            else:
2736                course = list(result)[0]
2737            invalid = self.context.updateCourseTicket(value, course)
2738            if invalid:
2739                invalidated.append(invalid)
2740        if invalidated:
2741            invalidated_string = ', '.join(invalidated)
2742            self.context.writeLogMessage(
2743                self, 'course tickets invalidated: %s' % invalidated_string)
2744        self.flash(_('All course tickets updated.'))
2745        return
2746
2747    @action(_('Update all tickets'),
2748        tooltip=_('Update all course parameters including course titles.'))
2749    def updateTickets(self, **data):
2750        self._updateTickets(**data)
2751        return
2752
2753    def _registerCourses(self, **data):
2754        if self.context.student.is_postgrad and \
2755            not self.context.student.is_special_postgrad:
2756            self.flash(_(
2757                "You are a postgraduate student, "
2758                "your course list can't bee registered."), type="warning")
2759            self.redirect(self.url(self.context))
2760            return
2761        students_utils = getUtility(IStudentsUtils)
2762        warning = students_utils.warnCreditsOOR(self.context)
2763        if warning:
2764            self.flash(warning, type="warning")
2765            return
2766        msg = self.context.course_registration_forbidden
2767        if msg:
2768            self.flash(msg, type="warning")
2769            return
2770        IWorkflowInfo(self.context.student).fireTransition(
2771            'register_courses')
2772        self.flash(_('Course list has been registered.'))
2773        self.redirect(self.url(self.context))
2774        return
2775
2776    @action(_('Register course list'), style='primary',
2777        warning=_('You can not edit your course list after registration.'
2778            ' You really want to register?'))
2779    def registerCourses(self, **data):
2780        self._registerCourses(**data)
2781        return
2782
2783class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2784    """Add a course ticket by student.
2785    """
2786    grok.name('ctadd')
2787    grok.require('waeup.handleStudent')
2788    form_fields = grok.AutoFields(ICourseTicketAdd)
2789
2790    def update(self):
2791        if self.context.student.state != PAID or \
2792            not self.context.is_current_level:
2793            emit_lock_message(self)
2794            return
2795        super(CourseTicketAddFormPage2, self).update()
2796        return
2797
2798    @action(_('Add course ticket'))
2799    def addCourseTicket(self, **data):
2800        # Safety belt
2801        if self.context.student.state != PAID:
2802            return
2803        course = data['course']
2804        success = addCourseTicket(self, course)
2805        if success:
2806            self.redirect(self.url(self.context, u'@@edit'))
2807        return
2808
2809class SetPasswordPage(KofaPage):
2810    grok.context(IKofaObject)
2811    grok.name('setpassword')
2812    grok.require('waeup.Anonymous')
2813    grok.template('setpassword')
2814    label = _('Set password for first-time login')
2815    ac_prefix = 'PWD'
2816    pnav = 0
2817    set_button = _('Set')
2818
2819    def update(self, SUBMIT=None):
2820        self.reg_number = self.request.form.get('reg_number', None)
2821        self.ac_series = self.request.form.get('ac_series', None)
2822        self.ac_number = self.request.form.get('ac_number', None)
2823
2824        if SUBMIT is None:
2825            return
2826        hitlist = search(query=self.reg_number,
2827            searchtype='reg_number', view=self)
2828        if not hitlist:
2829            self.flash(_('No student found.'), type="warning")
2830            return
2831        if len(hitlist) != 1:   # Cannot happen but anyway
2832            self.flash(_('More than one student found.'), type="warning")
2833            return
2834        student = hitlist[0].context
2835        self.student_id = student.student_id
2836        student_pw = student.password
2837        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2838        code = get_access_code(pin)
2839        if not code:
2840            self.flash(_('Access code is invalid.'), type="warning")
2841            return
2842        if student_pw and pin == student.adm_code:
2843            self.flash(_(
2844                'Password has already been set. Your Student Id is ${a}',
2845                mapping = {'a':self.student_id}))
2846            return
2847        elif student_pw:
2848            self.flash(
2849                _('Password has already been set. You are using the ' +
2850                'wrong Access Code.'), type="warning")
2851            return
2852        # Mark pin as used (this also fires a pin related transition)
2853        # and set student password
2854        if code.state == USED:
2855            self.flash(_('Access code has already been used.'), type="warning")
2856            return
2857        else:
2858            comment = _(u"invalidated")
2859            # Here we know that the ac is in state initialized so we do not
2860            # expect an exception
2861            invalidate_accesscode(pin,comment)
2862            IUserAccount(student).setPassword(self.ac_number)
2863            student.adm_code = pin
2864        self.flash(_('Password has been set. Your Student Id is ${a}',
2865            mapping = {'a':self.student_id}))
2866        return
2867
2868class StudentRequestPasswordPage(KofaAddFormPage):
2869    """Captcha'd request password page for students.
2870    """
2871    grok.name('requestpw')
2872    grok.require('waeup.Anonymous')
2873    grok.template('requestpw')
2874    form_fields = grok.AutoFields(IStudentRequestPW).select(
2875        'lastname','number','email')
2876    label = _('Request password for first-time login')
2877
2878    def update(self):
2879        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2880        if blocker:
2881            self.flash(_('The portal is in maintenance mode. '
2882                        'Password request forms are temporarily disabled.'),
2883                       type='warning')
2884            self.redirect(self.url(self.context))
2885            return
2886        # Handle captcha
2887        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2888        self.captcha_result = self.captcha.verify(self.request)
2889        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2890        return
2891
2892    def _redirect(self, email, password, student_id):
2893        # Forward only email to landing page in base package.
2894        self.redirect(self.url(self.context, 'requestpw_complete',
2895            data = dict(email=email)))
2896        return
2897
2898    def _redirect_no_student(self):
2899        # No record found, this is the truth. We do not redirect here.
2900        # We are using this method in custom packages
2901        # for redirecting alumni to the application section.
2902        self.flash(_('No student record found.'), type="warning")
2903        return
2904
2905    def _pw_used(self):
2906        # XXX: False if password has not been used. We need an extra
2907        #      attribute which remembers if student logged in.
2908        return True
2909
2910    @action(_('Send login credentials to email address'), style='primary')
2911    def get_credentials(self, **data):
2912        if not self.captcha_result.is_valid:
2913            # Captcha will display error messages automatically.
2914            # No need to flash something.
2915            return
2916        number = data.get('number','')
2917        lastname = data.get('lastname','')
2918        cat = getUtility(ICatalog, name='students_catalog')
2919        results = list(
2920            cat.searchResults(reg_number=(number, number)))
2921        if not results:
2922            results = list(
2923                cat.searchResults(matric_number=(number, number)))
2924        if results:
2925            student = results[0]
2926            if getattr(student,'lastname',None) is None:
2927                self.flash(_('An error occurred.'), type="danger")
2928                return
2929            elif student.lastname.lower() != lastname.lower():
2930                # Don't tell the truth here. Anonymous must not
2931                # know that a record was found and only the lastname
2932                # verification failed.
2933                self.flash(_('No student record found.'), type="warning")
2934                return
2935            elif student.password is not None and self._pw_used:
2936                self.flash(_('Your password has already been set and used. '
2937                             'Please proceed to the login page.'),
2938                           type="warning")
2939                return
2940            # Store email address but nothing else.
2941            student.email = data['email']
2942            notify(grok.ObjectModifiedEvent(student))
2943        else:
2944            self._redirect_no_student()
2945            return
2946
2947        kofa_utils = getUtility(IKofaUtils)
2948        password = kofa_utils.genPassword()
2949        mandate = PasswordMandate()
2950        mandate.params['password'] = password
2951        mandate.params['user'] = student
2952        site = grok.getSite()
2953        site['mandates'].addMandate(mandate)
2954        # Send email with credentials
2955        args = {'mandate_id':mandate.mandate_id}
2956        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2957        url_info = u'Confirmation link: %s' % mandate_url
2958        msg = _('You have successfully requested a password for the')
2959        if kofa_utils.sendCredentials(IUserAccount(student),
2960            password, url_info, msg):
2961            email_sent = student.email
2962        else:
2963            email_sent = None
2964        self._redirect(email=email_sent, password=password,
2965            student_id=student.student_id)
2966        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2967        self.context.logger.info(
2968            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2969        return
2970
2971class StudentRequestPasswordEmailSent(KofaPage):
2972    """Landing page after successful password request.
2973
2974    """
2975    grok.name('requestpw_complete')
2976    grok.require('waeup.Public')
2977    grok.template('requestpwmailsent')
2978    label = _('Your password request was successful.')
2979
2980    def update(self, email=None, student_id=None, password=None):
2981        self.email = email
2982        self.password = password
2983        self.student_id = student_id
2984        return
2985
2986class FilterStudentsInDepartmentPage(KofaPage):
2987    """Page that filters and lists students.
2988    """
2989    grok.context(IDepartment)
2990    grok.require('waeup.showStudents')
2991    grok.name('students')
2992    grok.template('filterstudentspage')
2993    pnav = 1
2994    session_label = _('Current Session')
2995    level_label = _('Current Level')
2996
2997    def label(self):
2998        return 'Students in %s' % self.context.longtitle
2999
3000    def _set_session_values(self):
3001        vocab_terms = academic_sessions_vocab.by_value.values()
3002        self.sessions = sorted(
3003            [(x.title, x.token) for x in vocab_terms], reverse=True)
3004        self.sessions += [('All Sessions', 'all')]
3005        return
3006
3007    def _set_level_values(self):
3008        vocab_terms = course_levels.by_value.values()
3009        self.levels = sorted(
3010            [(x.title, x.token) for x in vocab_terms])
3011        self.levels += [('All Levels', 'all')]
3012        return
3013
3014    def _searchCatalog(self, session, level):
3015        if level not in (10, 999, None):
3016            start_level = 100 * (level // 100)
3017            end_level = start_level + 90
3018        else:
3019            start_level = end_level = level
3020        cat = queryUtility(ICatalog, name='students_catalog')
3021        students = cat.searchResults(
3022            current_session=(session, session),
3023            current_level=(start_level, end_level),
3024            depcode=(self.context.code, self.context.code)
3025            )
3026        hitlist = []
3027        for student in students:
3028            hitlist.append(StudentQueryResultItem(student, view=self))
3029        return hitlist
3030
3031    def update(self, SHOW=None, session=None, level=None):
3032        self.parent_url = self.url(self.context.__parent__)
3033        self._set_session_values()
3034        self._set_level_values()
3035        self.hitlist = []
3036        self.session_default = session
3037        self.level_default = level
3038        if SHOW is not None:
3039            if session != 'all':
3040                self.session = int(session)
3041                self.session_string = '%s %s/%s' % (
3042                    self.session_label, self.session, self.session+1)
3043            else:
3044                self.session = None
3045                self.session_string = _('in any session')
3046            if level != 'all':
3047                self.level = int(level)
3048                self.level_string = '%s %s' % (self.level_label, self.level)
3049            else:
3050                self.level = None
3051                self.level_string = _('at any level')
3052            self.hitlist = self._searchCatalog(self.session, self.level)
3053            if not self.hitlist:
3054                self.flash(_('No student found.'), type="warning")
3055        return
3056
3057class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
3058    """Page that filters and lists students.
3059    """
3060    grok.context(ICertificate)
3061
3062    def label(self):
3063        return 'Students studying %s' % self.context.longtitle
3064
3065    def _searchCatalog(self, session, level):
3066        if level not in (10, 999, None):
3067            start_level = 100 * (level // 100)
3068            end_level = start_level + 90
3069        else:
3070            start_level = end_level = level
3071        cat = queryUtility(ICatalog, name='students_catalog')
3072        students = cat.searchResults(
3073            current_session=(session, session),
3074            current_level=(start_level, end_level),
3075            certcode=(self.context.code, self.context.code)
3076            )
3077        hitlist = []
3078        for student in students:
3079            hitlist.append(StudentQueryResultItem(student, view=self))
3080        return hitlist
3081
3082class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3083    """Page that filters and lists students.
3084    """
3085    grok.context(ICourse)
3086    grok.require('waeup.viewStudent')
3087
3088    session_label = _('Session')
3089    level_label = _('Level')
3090
3091    def label(self):
3092        return 'Students registered for %s' % self.context.longtitle
3093
3094    def _searchCatalog(self, session, level):
3095        if level not in (10, 999, None):
3096            start_level = 100 * (level // 100)
3097            end_level = start_level + 90
3098        else:
3099            start_level = end_level = level
3100        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3101        coursetickets = cat.searchResults(
3102            session=(session, session),
3103            level=(start_level, end_level),
3104            code=(self.context.code, self.context.code)
3105            )
3106        hitlist = []
3107        for ticket in coursetickets:
3108            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3109        return list(set(hitlist))
3110
3111class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3112    """ Clear all students of a department in state 'clearance requested'.
3113    """
3114    grok.context(IDepartment)
3115    grok.name('clearallstudents')
3116    grok.require('waeup.clearAllStudents')
3117
3118    def update(self):
3119        cat = queryUtility(ICatalog, name='students_catalog')
3120        students = cat.searchResults(
3121            depcode=(self.context.code, self.context.code),
3122            state=(REQUESTED, REQUESTED)
3123            )
3124        num = 0
3125        for student in students:
3126            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3127                continue
3128            IWorkflowInfo(student).fireTransition('clear')
3129            num += 1
3130        self.flash(_('%d students have been cleared.' % num))
3131        self.redirect(self.url(self.context))
3132        return
3133
3134    def render(self):
3135        return
3136
3137
3138class EditScoresPage(KofaPage):
3139    """Page that allows to edit batches of scores.
3140    """
3141    grok.context(ICourse)
3142    grok.require('waeup.editScores')
3143    grok.name('edit_scores')
3144    grok.template('editscorespage')
3145    pnav = 1
3146    doclink = DOCLINK + '/students/browser.html#batch-editing-scores-by-lecturers'
3147
3148    def label(self):
3149        return '%s tickets in academic session %s' % (
3150            self.context.code, self.session_title)
3151
3152    def _searchCatalog(self, session):
3153        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3154        coursetickets = cat.searchResults(
3155            session=(session, session),
3156            code=(self.context.code, self.context.code)
3157            )
3158        return list(coursetickets)
3159
3160    def _extract_uploadfile(self, uploadfile):
3161        """Get a mapping of student-ids to scores.
3162
3163        The mapping is constructed by reading contents from `uploadfile`.
3164
3165        We expect uploadfile to be a regular CSV file with columns
3166        ``student_id`` and ``score`` (other cols are ignored).
3167        """
3168        result = dict()
3169        data = StringIO(uploadfile.read())  # ensure we have something seekable
3170        reader = csv.DictReader(data)
3171        for row in reader:
3172            if not 'student_id' in row or not 'score' in row:
3173                continue
3174            result[row['student_id']] = row['score']
3175        return result
3176
3177    def _update_scores(self, form):
3178        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3179        error = ''
3180        if 'UPDATE_FILE' in form:
3181            if form['uploadfile']:
3182                try:
3183                    formvals = self._extract_uploadfile(form['uploadfile'])
3184                except:
3185                    self.flash(
3186                        _('Uploaded file contains illegal data. Ignored'),
3187                        type="danger")
3188                    return False
3189            else:
3190                self.flash(
3191                    _('No file provided.'), type="danger")
3192                return False
3193        else:
3194            formvals = dict(zip(form['sids'], form['scores']))
3195        for ticket in self.editable_tickets:
3196            score = ticket.score
3197            sid = ticket.student.student_id
3198            if sid not in formvals:
3199                continue
3200            if formvals[sid] == '':
3201                score = None
3202            else:
3203                try:
3204                    score = int(formvals[sid])
3205                except ValueError:
3206                    error += '%s, ' % ticket.student.display_fullname
3207            if ticket.score != score:
3208                ticket.score = score
3209                ticket.student.__parent__.logger.info(
3210                    '%s - %s %s/%s score updated (%s)' % (
3211                        ob_class, ticket.student.student_id,
3212                        ticket.level, ticket.code, score)
3213                    )
3214        if error:
3215            self.flash(
3216                _('Error: Score(s) of following students have not been '
3217                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3218                type="danger")
3219        return True
3220
3221    def update(self,  *args, **kw):
3222        form = self.request.form
3223        self.current_academic_session = grok.getSite()[
3224            'configuration'].current_academic_session
3225        if self.context.__parent__.__parent__.score_editing_disabled:
3226            self.flash(_('Score editing disabled.'), type="warning")
3227            self.redirect(self.url(self.context))
3228            return
3229        if not self.current_academic_session:
3230            self.flash(_('Current academic session not set.'), type="warning")
3231            self.redirect(self.url(self.context))
3232            return
3233        self.session_title = academic_sessions_vocab.getTerm(
3234            self.current_academic_session).title
3235        self.tickets = self._searchCatalog(self.current_academic_session)
3236        if not self.tickets:
3237            self.flash(_('No student found.'), type="warning")
3238            self.redirect(self.url(self.context))
3239            return
3240        self.editable_tickets = [
3241            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3242        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3243            return
3244        if not self.editable_tickets:
3245            return
3246        success = self._update_scores(form)
3247        if success:
3248            self.flash(_('You successfully updated course results.'))
3249        return
3250
3251
3252class DownloadScoresView(UtilityView, grok.View):
3253    """View that exports scores.
3254    """
3255    grok.context(ICourse)
3256    grok.require('waeup.editScores')
3257    grok.name('download_scores')
3258
3259    def update(self):
3260        self.current_academic_session = grok.getSite()[
3261            'configuration'].current_academic_session
3262        if self.context.__parent__.__parent__.score_editing_disabled:
3263            self.flash(_('Score editing disabled.'), type="warning")
3264            self.redirect(self.url(self.context))
3265            return
3266        if not self.current_academic_session:
3267            self.flash(_('Current academic session not set.'), type="warning")
3268            self.redirect(self.url(self.context))
3269            return
3270        site = grok.getSite()
3271        exporter = getUtility(ICSVExporter, name='lecturer')
3272        self.csv = exporter.export_filtered(site, filepath=None,
3273                                 catalog='coursetickets',
3274                                 session=self.current_academic_session,
3275                                 level=None,
3276                                 code=self.context.code)
3277        return
3278
3279    def render(self):
3280        filename = 'results_%s_%s.csv' % (
3281            self.context.code, self.current_academic_session)
3282        self.response.setHeader(
3283            'Content-Type', 'text/csv; charset=UTF-8')
3284        self.response.setHeader(
3285            'Content-Disposition:', 'attachment; filename="%s' % filename)
3286        return self.csv
3287
3288class ExportPDFScoresSlip(UtilityView, grok.View,
3289    LocalRoleAssignmentUtilityView):
3290    """Deliver a PDF slip of course tickets for a lecturer.
3291    """
3292    grok.context(ICourse)
3293    grok.name('coursetickets.pdf')
3294    grok.require('waeup.editScores')
3295
3296    def data(self, session):
3297        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3298        coursetickets = cat.searchResults(
3299            session=(session, session),
3300            code=(self.context.code, self.context.code)
3301            )
3302        header = [[_('Matric No.'),
3303                   _('Reg. No.'),
3304                   _('Fullname'),
3305                   _('Status'),
3306                   _('Course of Studies'),
3307                   _('Level'),
3308                   _('Score') ],]
3309        tickets = []
3310        for ticket in list(coursetickets):
3311            row = [ticket.student.matric_number,
3312                  ticket.student.reg_number,
3313                  ticket.student.display_fullname,
3314                  ticket.student.translated_state,
3315                  ticket.student.certcode,
3316                  ticket.level,
3317                  ticket.score]
3318            tickets.append(row)
3319        return header + sorted(tickets, key=lambda value: value[0]), None
3320
3321    def render(self):
3322        session = grok.getSite()['configuration'].current_academic_session
3323        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3324                     if i['local_role'] == 'waeup.local.Lecturer']
3325        lecturers =  ', '.join(lecturers)
3326        students_utils = getUtility(IStudentsUtils)
3327        return students_utils.renderPDFCourseticketsOverview(
3328            self, session, self.data(session), lecturers, 'landscape')
3329
3330class ExportJobContainerOverview(KofaPage):
3331    """Page that lists active student data export jobs and provides links
3332    to discard or download CSV files.
3333
3334    """
3335    grok.context(VirtualExportJobContainer)
3336    grok.require('waeup.showStudents')
3337    grok.name('index.html')
3338    grok.template('exportjobsindex')
3339    label = _('Student Data Exports')
3340    pnav = 1
3341    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3342
3343    def update(self, CREATE=None, DISCARD=None, job_id=None):
3344        if CREATE:
3345            self.redirect(self.url('@@exportconfig'))
3346            return
3347        if DISCARD and job_id:
3348            entry = self.context.entry_from_job_id(job_id)
3349            self.context.delete_export_entry(entry)
3350            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3351            self.context.logger.info(
3352                '%s - discarded: job_id=%s' % (ob_class, job_id))
3353            self.flash(_('Discarded export') + ' %s' % job_id)
3354        self.entries = doll_up(self, user=self.request.principal.id)
3355        return
3356
3357class ExportJobContainerJobConfig(KofaPage):
3358    """Page that configures a students export job.
3359
3360    This is a baseclass.
3361    """
3362    grok.baseclass()
3363    grok.name('exportconfig')
3364    grok.require('waeup.showStudents')
3365    grok.template('exportconfig')
3366    label = _('Configure student data export')
3367    pnav = 1
3368    redirect_target = ''
3369    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3370
3371    def _set_session_values(self):
3372        vocab_terms = academic_sessions_vocab.by_value.values()
3373        self.sessions = sorted(
3374            [(x.title, x.token) for x in vocab_terms], reverse=True)
3375        self.sessions += [(_('All Sessions'), 'all')]
3376        return
3377
3378    def _set_level_values(self):
3379        vocab_terms = course_levels.by_value.values()
3380        self.levels = sorted(
3381            [(x.title, x.token) for x in vocab_terms])
3382        self.levels += [(_('All Levels'), 'all')]
3383        return
3384
3385    def _set_mode_values(self):
3386        utils = getUtility(IKofaUtils)
3387        self.modes = sorted([(value, key) for key, value in
3388                      utils.STUDY_MODES_DICT.items()])
3389        self.modes +=[(_('All Modes'), 'all')]
3390        return
3391
3392    def _set_exporter_values(self):
3393        # We provide all student exporters, nothing else, yet.
3394        # Bursary or Department Officers don't have the general exportData
3395        # permission and are only allowed to export bursary or payments
3396        # overview data respectively. This is the only place where
3397        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3398        # are used.
3399        exporters = []
3400        if not checkPermission('waeup.exportData', self.context):
3401            if checkPermission('waeup.exportBursaryData', self.context):
3402                exporters += [('Bursary Data', 'bursary')]
3403            if checkPermission('waeup.exportPaymentsOverview', self.context):
3404                exporters += [('Student Payments Overview', 'paymentsoverview')]
3405            self.exporters = exporters
3406            return
3407        STUDENT_EXPORTER_NAMES = getUtility(
3408            IStudentsUtils).STUDENT_EXPORTER_NAMES
3409        for name in STUDENT_EXPORTER_NAMES:
3410            util = getUtility(ICSVExporter, name=name)
3411            exporters.append((util.title, name),)
3412        self.exporters = exporters
3413        return
3414
3415    @property
3416    def faccode(self):
3417        return None
3418
3419    @property
3420    def depcode(self):
3421        return None
3422
3423    @property
3424    def certcode(self):
3425        return None
3426
3427    def update(self, START=None, session=None, level=None, mode=None,
3428               payments_start=None, payments_end=None,
3429               exporter=None):
3430        self._set_session_values()
3431        self._set_level_values()
3432        self._set_mode_values()
3433        self._set_exporter_values()
3434        if START is None:
3435            return
3436        ena = exports_not_allowed(self)
3437        if ena:
3438            self.flash(ena, type='danger')
3439            return
3440        if payments_start or payments_end:
3441            date_format = '%d/%m/%Y'
3442            try:
3443                datetime.strptime(payments_start, date_format)
3444                datetime.strptime(payments_end, date_format)
3445            except ValueError:
3446                self.flash(_('Payment dates do not match format d/m/Y.'),
3447                           type="danger")
3448                return
3449        if session == 'all':
3450            session=None
3451        if level == 'all':
3452            level = None
3453        if mode == 'all':
3454            mode = None
3455        if payments_start == '':
3456            payments_start = None
3457        if payments_end == '':
3458            payments_end = None
3459        if (mode,
3460            level,
3461            session,
3462            self.faccode,
3463            self.depcode,
3464            self.certcode) == (None, None, None, None, None, None):
3465            # Export all students including those without certificate
3466            if payments_start:
3467                job_id = self.context.start_export_job(exporter,
3468                                              self.request.principal.id,
3469                                              payments_start = payments_start,
3470                                              payments_end = payments_end)
3471            else:
3472                job_id = self.context.start_export_job(exporter,
3473                                              self.request.principal.id)
3474        else:
3475            if payments_start:
3476                job_id = self.context.start_export_job(exporter,
3477                                              self.request.principal.id,
3478                                              current_session=session,
3479                                              current_level=level,
3480                                              current_mode=mode,
3481                                              faccode=self.faccode,
3482                                              depcode=self.depcode,
3483                                              certcode=self.certcode,
3484                                              payments_start = payments_start,
3485                                              payments_end = payments_end)
3486            else:
3487                job_id = self.context.start_export_job(exporter,
3488                                              self.request.principal.id,
3489                                              current_session=session,
3490                                              current_level=level,
3491                                              current_mode=mode,
3492                                              faccode=self.faccode,
3493                                              depcode=self.depcode,
3494                                              certcode=self.certcode)
3495        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3496        self.context.logger.info(
3497            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3498            % (ob_class, exporter, session, level, mode, self.faccode,
3499            self.depcode, self.certcode, payments_start, payments_end, job_id))
3500        self.flash(_('Export started for students with') +
3501                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3502                   session, level, mode))
3503        self.redirect(self.url(self.redirect_target))
3504        return
3505
3506class ExportJobContainerDownload(ExportCSVView):
3507    """Page that downloads a students export csv file.
3508
3509    """
3510    grok.context(VirtualExportJobContainer)
3511    grok.require('waeup.showStudents')
3512
3513class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3514    """Page that configures a students export job in datacenter.
3515
3516    """
3517    grok.context(IDataCenter)
3518    redirect_target = '@@export'
3519
3520class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3521    """Page that configures a students export job in datacenter.
3522
3523    """
3524    grok.name('exportselected')
3525    grok.context(IDataCenter)
3526    redirect_target = '@@export'
3527    grok.template('exportselected')
3528    label = _('Configure student data export')
3529
3530    def update(self, START=None, students=None, exporter=None):
3531        self._set_exporter_values()
3532        if START is None:
3533            return
3534        ena = exports_not_allowed(self)
3535        if ena:
3536            self.flash(ena, type='danger')
3537            return
3538        try:
3539            ids = students.replace(',', ' ').split()
3540        except:
3541            self.flash(sys.exc_info()[1])
3542            self.redirect(self.url(self.redirect_target))
3543            return
3544        job_id = self.context.start_export_job(
3545            exporter, self.request.principal.id, selected=ids)
3546        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3547        self.context.logger.info(
3548            '%s - selected students exported: %s, job_id=%s' %
3549            (ob_class, exporter, job_id))
3550        self.flash(_('Export of selected students started.'))
3551        self.redirect(self.url(self.redirect_target))
3552        return
3553
3554class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3555    """Page that configures a students export job in facultiescontainer.
3556
3557    """
3558    grok.context(VirtualFacultiesExportJobContainer)
3559
3560
3561class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3562    """Page that configures a students export job in faculties.
3563
3564    """
3565    grok.context(VirtualFacultyExportJobContainer)
3566
3567    @property
3568    def faccode(self):
3569        return self.context.__parent__.code
3570
3571class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3572    """Page that configures a students export job in departments.
3573
3574    """
3575    grok.context(VirtualDepartmentExportJobContainer)
3576
3577    @property
3578    def depcode(self):
3579        return self.context.__parent__.code
3580
3581class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3582    """Page that configures a students export job for certificates.
3583
3584    """
3585    grok.context(VirtualCertificateExportJobContainer)
3586    grok.template('exportconfig_certificate')
3587
3588    @property
3589    def certcode(self):
3590        return self.context.__parent__.code
3591
3592class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3593    """Page that configures a students export job for courses.
3594
3595    In contrast to department or certificate student data exports the
3596    coursetickets_catalog is searched here. Therefore the update
3597    method from the base class is customized.
3598    """
3599    grok.context(VirtualCourseExportJobContainer)
3600    grok.template('exportconfig_course')
3601
3602    def _set_exporter_values(self):
3603        # We provide only the 'coursetickets' and 'lecturer' exporter
3604        # but can add more.
3605        exporters = []
3606        for name in ('coursetickets', 'lecturer'):
3607            util = getUtility(ICSVExporter, name=name)
3608            exporters.append((util.title, name),)
3609        self.exporters = exporters
3610
3611    def _set_session_values(self):
3612        # We allow only current academic session
3613        academic_session = grok.getSite()['configuration'].current_academic_session
3614        if not academic_session:
3615            self.sessions = []
3616            return
3617        x = academic_sessions_vocab.getTerm(academic_session)
3618        self.sessions = [(x.title, x.token)]
3619        return
3620
3621    def update(self, START=None, session=None, level=None, mode=None,
3622               exporter=None):
3623        self._set_session_values()
3624        self._set_level_values()
3625        self._set_mode_values()
3626        self._set_exporter_values()
3627        if not self.sessions:
3628            self.flash(
3629                _('Academic session not set. '
3630                  'Please contact the administrator.'),
3631                type='danger')
3632            self.redirect(self.url(self.context))
3633            return
3634        if START is None:
3635            return
3636        ena = exports_not_allowed(self)
3637        if ena:
3638            self.flash(ena, type='danger')
3639            return
3640        if session == 'all':
3641            session = None
3642        if level == 'all':
3643            level = None
3644        job_id = self.context.start_export_job(exporter,
3645                                      self.request.principal.id,
3646                                      # Use a different catalog and
3647                                      # pass different keywords than
3648                                      # for the (default) students_catalog
3649                                      catalog='coursetickets',
3650                                      session=session,
3651                                      level=level,
3652                                      code=self.context.__parent__.code)
3653        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3654        self.context.logger.info(
3655            '%s - exported: %s (%s, %s, %s), job_id=%s'
3656            % (ob_class, exporter, session, level,
3657            self.context.__parent__.code, job_id))
3658        self.flash(_('Export started for course tickets with') +
3659                   ' level_session=%s, level=%s' % (
3660                   session, level))
3661        self.redirect(self.url(self.redirect_target))
3662        return
Note: See TracBrowser for help on using the repository browser.