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

Last change on this file since 13050 was 13050, checked in by Henrik Bettermann, 10 years ago

Clear already booked bed if student used an ac twice or isn't the owner of the ac. I found this bug when writing the documentation.

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