source: main/waeup.kofa/branches/uli-diazo-themed/src/waeup/kofa/students/browser.py @ 11107

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

Remove Bootstrap 1 trash and adjust pagetemplates further.

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