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

Last change on this file since 11590 was 11580, checked in by Henrik Bettermann, 11 years ago

Show correct flash message color after payment.

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