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

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

Reinvent temporary/unrectified/momentary/sessional gpa as a property attribute of study levels. The sessional gpa shown on transcript slips is now called 'rectified sessional gpa'.

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