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

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

Add permission and role for transcript officers.

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