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

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

Let transcript officers find their students via the students container page.

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