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

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

We need to customize the states in which students can change their passport picture (portrait).

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