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

Last change on this file since 15419 was 15419, checked in by Henrik Bettermann, 5 years ago

Change history message

Remove redundant history message. History message is already added by the event handler.

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