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

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

Backup work in progress.

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