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

Last change on this file since 13885 was 13874, checked in by Henrik Bettermann, 9 years ago

Bugfix. The order of formscores? depends on Javascript sorting.

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