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

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

Create new exporter for lecturers and further restrict the usage of
exporters at course level.

  • Property svn:keywords set to Id
File size: 128.8 KB
Line 
1## $Id: browser.py 13766 2016-03-08 06:22:12Z 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        except RequiredMissing:
2615            self.flash(_('Your data are incomplete'), type="danger")
2616        self.redirect(self.url(self.context))
2617        return
2618
2619class StudyLevelEditFormPage(KofaEditFormPage):
2620    """ Page to edit the student study level data by students
2621    """
2622    grok.context(IStudentStudyLevel)
2623    grok.name('edit')
2624    grok.require('waeup.editStudyLevel')
2625    grok.template('studyleveleditpage')
2626    pnav = 4
2627    placeholder = _('Enter valid course code')
2628
2629    def update(self, ADD=None, course=None):
2630        if not self.context.__parent__.is_current:
2631            emit_lock_message(self)
2632            return
2633        if self.context.student.state != PAID or \
2634            not self.context.is_current_level:
2635            emit_lock_message(self)
2636            return
2637        super(StudyLevelEditFormPage, self).update()
2638        if ADD is not None:
2639            if not course:
2640                self.flash(_('No valid course code entered.'), type="warning")
2641                return
2642            cat = queryUtility(ICatalog, name='courses_catalog')
2643            result = cat.searchResults(code=(course, course))
2644            if len(result) != 1:
2645                self.flash(_('Course not found.'), type="warning")
2646                return
2647            course = list(result)[0]
2648            addCourseTicket(self, course)
2649        return
2650
2651    @property
2652    def label(self):
2653        # Here we know that the cookie has been set
2654        lang = self.request.cookies.get('kofa.language')
2655        level_title = translate(self.context.level_title, 'waeup.kofa',
2656            target_language=lang)
2657        return _('Edit course list of ${a}',
2658            mapping = {'a':level_title})
2659
2660    @property
2661    def translated_values(self):
2662        return translated_values(self)
2663
2664    def _delCourseTicket(self, **data):
2665        form = self.request.form
2666        if 'val_id' in form:
2667            child_id = form['val_id']
2668        else:
2669            self.flash(_('No ticket selected.'), type="warning")
2670            self.redirect(self.url(self.context, '@@edit'))
2671            return
2672        if not isinstance(child_id, list):
2673            child_id = [child_id]
2674        deleted = []
2675        for id in child_id:
2676            # Students are not allowed to remove core tickets
2677            if id in self.context and \
2678                self.context[id].removable_by_student:
2679                del self.context[id]
2680                deleted.append(id)
2681        if len(deleted):
2682            self.flash(_('Successfully removed: ${a}',
2683                mapping = {'a':', '.join(deleted)}))
2684            self.context.writeLogMessage(
2685                self,'removed: %s at %s' %
2686                (', '.join(deleted), self.context.level))
2687        self.redirect(self.url(self.context, u'@@edit'))
2688        return
2689
2690    @jsaction(_('Remove selected tickets'))
2691    def delCourseTicket(self, **data):
2692        self._delCourseTicket(**data)
2693        return
2694
2695    def _registerCourses(self, **data):
2696        if self.context.student.is_postgrad and \
2697            not self.context.student.is_special_postgrad:
2698            self.flash(_(
2699                "You are a postgraduate student, "
2700                "your course list can't bee registered."), type="warning")
2701            self.redirect(self.url(self.context))
2702            return
2703        students_utils = getUtility(IStudentsUtils)
2704        max_credits = students_utils.maxCredits(self.context)
2705        if max_credits and self.context.total_credits > max_credits:
2706            self.flash(_('Maximum credits of ${a} exceeded.',
2707                mapping = {'a':max_credits}), type="warning")
2708            return
2709        if not self.context.course_registration_allowed:
2710            self.flash(_(
2711                "Course registration has ended. "
2712                "Please pay the late registration fee."), type="warning")
2713            #self.redirect(self.url(self.context))
2714            return
2715        IWorkflowInfo(self.context.student).fireTransition(
2716            'register_courses')
2717        self.flash(_('Course list has been registered.'))
2718        self.redirect(self.url(self.context))
2719        return
2720
2721    @action(_('Register course list'), style='primary',
2722        warning=_('You can not edit your course list after registration.'
2723            ' You really want to register?'))
2724    def registerCourses(self, **data):
2725        self._registerCourses(**data)
2726        return
2727
2728class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2729    """Add a course ticket by student.
2730    """
2731    grok.name('ctadd')
2732    grok.require('waeup.handleStudent')
2733    form_fields = grok.AutoFields(ICourseTicketAdd)
2734
2735    def update(self):
2736        if self.context.student.state != PAID or \
2737            not self.context.is_current_level:
2738            emit_lock_message(self)
2739            return
2740        super(CourseTicketAddFormPage2, self).update()
2741        return
2742
2743    @action(_('Add course ticket'))
2744    def addCourseTicket(self, **data):
2745        # Safety belt
2746        if self.context.student.state != PAID:
2747            return
2748        course = data['course']
2749        success = addCourseTicket(self, course)
2750        if success:
2751            self.redirect(self.url(self.context, u'@@edit'))
2752        return
2753
2754class SetPasswordPage(KofaPage):
2755    grok.context(IKofaObject)
2756    grok.name('setpassword')
2757    grok.require('waeup.Anonymous')
2758    grok.template('setpassword')
2759    label = _('Set password for first-time login')
2760    ac_prefix = 'PWD'
2761    pnav = 0
2762    set_button = _('Set')
2763
2764    def update(self, SUBMIT=None):
2765        self.reg_number = self.request.form.get('reg_number', None)
2766        self.ac_series = self.request.form.get('ac_series', None)
2767        self.ac_number = self.request.form.get('ac_number', None)
2768
2769        if SUBMIT is None:
2770            return
2771        hitlist = search(query=self.reg_number,
2772            searchtype='reg_number', view=self)
2773        if not hitlist:
2774            self.flash(_('No student found.'), type="warning")
2775            return
2776        if len(hitlist) != 1:   # Cannot happen but anyway
2777            self.flash(_('More than one student found.'), type="warning")
2778            return
2779        student = hitlist[0].context
2780        self.student_id = student.student_id
2781        student_pw = student.password
2782        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2783        code = get_access_code(pin)
2784        if not code:
2785            self.flash(_('Access code is invalid.'), type="warning")
2786            return
2787        if student_pw and pin == student.adm_code:
2788            self.flash(_(
2789                'Password has already been set. Your Student Id is ${a}',
2790                mapping = {'a':self.student_id}))
2791            return
2792        elif student_pw:
2793            self.flash(
2794                _('Password has already been set. You are using the ' +
2795                'wrong Access Code.'), type="warning")
2796            return
2797        # Mark pin as used (this also fires a pin related transition)
2798        # and set student password
2799        if code.state == USED:
2800            self.flash(_('Access code has already been used.'), type="warning")
2801            return
2802        else:
2803            comment = _(u"invalidated")
2804            # Here we know that the ac is in state initialized so we do not
2805            # expect an exception
2806            invalidate_accesscode(pin,comment)
2807            IUserAccount(student).setPassword(self.ac_number)
2808            student.adm_code = pin
2809        self.flash(_('Password has been set. Your Student Id is ${a}',
2810            mapping = {'a':self.student_id}))
2811        return
2812
2813class StudentRequestPasswordPage(KofaAddFormPage):
2814    """Captcha'd request password page for students.
2815    """
2816    grok.name('requestpw')
2817    grok.require('waeup.Anonymous')
2818    grok.template('requestpw')
2819    form_fields = grok.AutoFields(IStudentRequestPW).select(
2820        'lastname','number','email')
2821    label = _('Request password for first-time login')
2822
2823    def update(self):
2824        blocker = grok.getSite()['configuration'].maintmode_enabled_by
2825        if blocker:
2826            self.flash(_('The portal is in maintenance mode. '
2827                        'Password request forms are temporarily disabled.'),
2828                       type='warning')
2829            self.redirect(self.url(self.context))
2830            return
2831        # Handle captcha
2832        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2833        self.captcha_result = self.captcha.verify(self.request)
2834        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2835        return
2836
2837    def _redirect(self, email, password, student_id):
2838        # Forward only email to landing page in base package.
2839        self.redirect(self.url(self.context, 'requestpw_complete',
2840            data = dict(email=email)))
2841        return
2842
2843    def _pw_used(self):
2844        # XXX: False if password has not been used. We need an extra
2845        #      attribute which remembers if student logged in.
2846        return True
2847
2848    @action(_('Send login credentials to email address'), style='primary')
2849    def get_credentials(self, **data):
2850        if not self.captcha_result.is_valid:
2851            # Captcha will display error messages automatically.
2852            # No need to flash something.
2853            return
2854        number = data.get('number','')
2855        lastname = data.get('lastname','')
2856        cat = getUtility(ICatalog, name='students_catalog')
2857        results = list(
2858            cat.searchResults(reg_number=(number, number)))
2859        if not results:
2860            results = list(
2861                cat.searchResults(matric_number=(number, number)))
2862        if results:
2863            student = results[0]
2864            if getattr(student,'lastname',None) is None:
2865                self.flash(_('An error occurred.'), type="danger")
2866                return
2867            elif student.lastname.lower() != lastname.lower():
2868                # Don't tell the truth here. Anonymous must not
2869                # know that a record was found and only the lastname
2870                # verification failed.
2871                self.flash(_('No student record found.'), type="warning")
2872                return
2873            elif student.password is not None and self._pw_used:
2874                self.flash(_('Your password has already been set and used. '
2875                             'Please proceed to the login page.'),
2876                           type="warning")
2877                return
2878            # Store email address but nothing else.
2879            student.email = data['email']
2880            notify(grok.ObjectModifiedEvent(student))
2881        else:
2882            # No record found, this is the truth.
2883            self.flash(_('No student record found.'), type="warning")
2884            return
2885
2886        kofa_utils = getUtility(IKofaUtils)
2887        password = kofa_utils.genPassword()
2888        mandate = PasswordMandate()
2889        mandate.params['password'] = password
2890        mandate.params['user'] = student
2891        site = grok.getSite()
2892        site['mandates'].addMandate(mandate)
2893        # Send email with credentials
2894        args = {'mandate_id':mandate.mandate_id}
2895        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2896        url_info = u'Confirmation link: %s' % mandate_url
2897        msg = _('You have successfully requested a password for the')
2898        if kofa_utils.sendCredentials(IUserAccount(student),
2899            password, url_info, msg):
2900            email_sent = student.email
2901        else:
2902            email_sent = None
2903        self._redirect(email=email_sent, password=password,
2904            student_id=student.student_id)
2905        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2906        self.context.logger.info(
2907            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2908        return
2909
2910class StudentRequestPasswordEmailSent(KofaPage):
2911    """Landing page after successful password request.
2912
2913    """
2914    grok.name('requestpw_complete')
2915    grok.require('waeup.Public')
2916    grok.template('requestpwmailsent')
2917    label = _('Your password request was successful.')
2918
2919    def update(self, email=None, student_id=None, password=None):
2920        self.email = email
2921        self.password = password
2922        self.student_id = student_id
2923        return
2924
2925class FilterStudentsInDepartmentPage(KofaPage):
2926    """Page that filters and lists students.
2927    """
2928    grok.context(IDepartment)
2929    grok.require('waeup.showStudents')
2930    grok.name('students')
2931    grok.template('filterstudentspage')
2932    pnav = 1
2933    session_label = _('Current Session')
2934    level_label = _('Current Level')
2935
2936    def label(self):
2937        return 'Students in %s' % self.context.longtitle
2938
2939    def _set_session_values(self):
2940        vocab_terms = academic_sessions_vocab.by_value.values()
2941        self.sessions = sorted(
2942            [(x.title, x.token) for x in vocab_terms], reverse=True)
2943        self.sessions += [('All Sessions', 'all')]
2944        return
2945
2946    def _set_level_values(self):
2947        vocab_terms = course_levels.by_value.values()
2948        self.levels = sorted(
2949            [(x.title, x.token) for x in vocab_terms])
2950        self.levels += [('All Levels', 'all')]
2951        return
2952
2953    def _searchCatalog(self, session, level):
2954        if level not in (10, 999, None):
2955            start_level = 100 * (level // 100)
2956            end_level = start_level + 90
2957        else:
2958            start_level = end_level = level
2959        cat = queryUtility(ICatalog, name='students_catalog')
2960        students = cat.searchResults(
2961            current_session=(session, session),
2962            current_level=(start_level, end_level),
2963            depcode=(self.context.code, self.context.code)
2964            )
2965        hitlist = []
2966        for student in students:
2967            hitlist.append(StudentQueryResultItem(student, view=self))
2968        return hitlist
2969
2970    def update(self, SHOW=None, session=None, level=None):
2971        self.parent_url = self.url(self.context.__parent__)
2972        self._set_session_values()
2973        self._set_level_values()
2974        self.hitlist = []
2975        self.session_default = session
2976        self.level_default = level
2977        if SHOW is not None:
2978            if session != 'all':
2979                self.session = int(session)
2980                self.session_string = '%s %s/%s' % (
2981                    self.session_label, self.session, self.session+1)
2982            else:
2983                self.session = None
2984                self.session_string = _('in any session')
2985            if level != 'all':
2986                self.level = int(level)
2987                self.level_string = '%s %s' % (self.level_label, self.level)
2988            else:
2989                self.level = None
2990                self.level_string = _('at any level')
2991            self.hitlist = self._searchCatalog(self.session, self.level)
2992            if not self.hitlist:
2993                self.flash(_('No student found.'), type="warning")
2994        return
2995
2996class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2997    """Page that filters and lists students.
2998    """
2999    grok.context(ICertificate)
3000
3001    def label(self):
3002        return 'Students studying %s' % self.context.longtitle
3003
3004    def _searchCatalog(self, session, level):
3005        if level not in (10, 999, None):
3006            start_level = 100 * (level // 100)
3007            end_level = start_level + 90
3008        else:
3009            start_level = end_level = level
3010        cat = queryUtility(ICatalog, name='students_catalog')
3011        students = cat.searchResults(
3012            current_session=(session, session),
3013            current_level=(start_level, end_level),
3014            certcode=(self.context.code, self.context.code)
3015            )
3016        hitlist = []
3017        for student in students:
3018            hitlist.append(StudentQueryResultItem(student, view=self))
3019        return hitlist
3020
3021class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
3022    """Page that filters and lists students.
3023    """
3024    grok.context(ICourse)
3025    grok.require('waeup.viewStudent')
3026
3027    session_label = _('Session')
3028    level_label = _('Level')
3029
3030    def label(self):
3031        return 'Students registered for %s' % self.context.longtitle
3032
3033    def _searchCatalog(self, session, level):
3034        if level not in (10, 999, None):
3035            start_level = 100 * (level // 100)
3036            end_level = start_level + 90
3037        else:
3038            start_level = end_level = level
3039        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3040        coursetickets = cat.searchResults(
3041            session=(session, session),
3042            level=(start_level, end_level),
3043            code=(self.context.code, self.context.code)
3044            )
3045        hitlist = []
3046        for ticket in coursetickets:
3047            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
3048        return list(set(hitlist))
3049
3050class ClearAllStudentsInDepartmentView(UtilityView, grok.View):
3051    """ Clear all students of a department in state 'clearance requested'.
3052    """
3053    grok.context(IDepartment)
3054    grok.name('clearallstudents')
3055    grok.require('waeup.clearAllStudents')
3056
3057    def update(self):
3058        cat = queryUtility(ICatalog, name='students_catalog')
3059        students = cat.searchResults(
3060            depcode=(self.context.code, self.context.code),
3061            state=(REQUESTED, REQUESTED)
3062            )
3063        num = 0
3064        for student in students:
3065            if getUtility(IStudentsUtils).clearance_disabled_message(student):
3066                continue
3067            IWorkflowInfo(student).fireTransition('clear')
3068            num += 1
3069        self.flash(_('%d students have been cleared.' % num))
3070        self.redirect(self.url(self.context))
3071        return
3072
3073    def render(self):
3074        return
3075
3076
3077class EditScoresPage(KofaPage):
3078    """Page that filters and lists students.
3079    """
3080    grok.context(ICourse)
3081    grok.require('waeup.editScores')
3082    grok.name('edit_scores')
3083    grok.template('editscorespage')
3084    pnav = 1
3085
3086    def label(self):
3087        session = academic_sessions_vocab.getTerm(
3088            self.current_academic_session).title
3089        return '%s tickets in academic session %s' % (
3090            self.context.code, session)
3091
3092    def _searchCatalog(self, session):
3093        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3094        coursetickets = cat.searchResults(
3095            session=(session, session),
3096            code=(self.context.code, self.context.code)
3097            )
3098        return list(coursetickets)
3099
3100    def update(self,  *args, **kw):
3101        form = self.request.form
3102        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3103        self.current_academic_session = grok.getSite()[
3104            'configuration'].current_academic_session
3105        if self.context.__parent__.__parent__.score_editing_disabled:
3106            self.flash(_('Score editing disabled.'), type="warning")
3107            self.redirect(self.url(self.context))
3108            return
3109        if not self.current_academic_session:
3110            self.flash(_('Current academic session not set.'), type="warning")
3111            self.redirect(self.url(self.context))
3112            return
3113        self.tickets = self._searchCatalog(self.current_academic_session)
3114        editable_tickets = [
3115            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3116        if not self.tickets:
3117            self.flash(_('No student found.'), type="warning")
3118            self.redirect(self.url(self.context))
3119            return
3120        if 'UPDATE' in form:
3121            tno = 0
3122            error = ''
3123            if not editable_tickets:
3124                return
3125            scores = form['scores']
3126            if isinstance(scores, basestring):
3127                scores = [scores]
3128            for ticket in editable_tickets:
3129                score = ticket.score
3130                if scores[tno] == '':
3131                    score = None
3132                else:
3133                    try:
3134                        score = int(scores[tno])
3135                    except ValueError:
3136                        error += '%s, ' % ticket.student.display_fullname
3137                if ticket.score != score:
3138                    ticket.score = score
3139                    ticket.student.__parent__.logger.info(
3140                        '%s - %s %s/%s score updated (%s)' %
3141                        (ob_class, ticket.student.student_id,
3142                         ticket.level, ticket.code, score))
3143                    #notify(grok.ObjectModifiedEvent(ticket))
3144                tno += 1
3145            if error:
3146                self.flash(_('Error: Score(s) of %s have not be updated. '
3147                  'Only integers are allowed.' % error.strip(', ')),
3148                  type="danger")
3149        return
3150
3151class ExportJobContainerOverview(KofaPage):
3152    """Page that lists active student data export jobs and provides links
3153    to discard or download CSV files.
3154
3155    """
3156    grok.context(VirtualExportJobContainer)
3157    grok.require('waeup.showStudents')
3158    grok.name('index.html')
3159    grok.template('exportjobsindex')
3160    label = _('Student Data Exports')
3161    pnav = 1
3162    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3163
3164    def update(self, CREATE=None, DISCARD=None, job_id=None):
3165        if CREATE:
3166            self.redirect(self.url('@@exportconfig'))
3167            return
3168        if DISCARD and job_id:
3169            entry = self.context.entry_from_job_id(job_id)
3170            self.context.delete_export_entry(entry)
3171            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3172            self.context.logger.info(
3173                '%s - discarded: job_id=%s' % (ob_class, job_id))
3174            self.flash(_('Discarded export') + ' %s' % job_id)
3175        self.entries = doll_up(self, user=self.request.principal.id)
3176        return
3177
3178class ExportJobContainerJobConfig(KofaPage):
3179    """Page that configures a students export job.
3180
3181    This is a baseclass.
3182    """
3183    grok.baseclass()
3184    grok.name('exportconfig')
3185    grok.require('waeup.showStudents')
3186    grok.template('exportconfig')
3187    label = _('Configure student data export')
3188    pnav = 1
3189    redirect_target = ''
3190    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3191
3192    def _set_session_values(self):
3193        vocab_terms = academic_sessions_vocab.by_value.values()
3194        self.sessions = sorted(
3195            [(x.title, x.token) for x in vocab_terms], reverse=True)
3196        self.sessions += [(_('All Sessions'), 'all')]
3197        return
3198
3199    def _set_level_values(self):
3200        vocab_terms = course_levels.by_value.values()
3201        self.levels = sorted(
3202            [(x.title, x.token) for x in vocab_terms])
3203        self.levels += [(_('All Levels'), 'all')]
3204        return
3205
3206    def _set_mode_values(self):
3207        utils = getUtility(IKofaUtils)
3208        self.modes = sorted([(value, key) for key, value in
3209                      utils.STUDY_MODES_DICT.items()])
3210        self.modes +=[(_('All Modes'), 'all')]
3211        return
3212
3213    def _set_exporter_values(self):
3214        # We provide all student exporters, nothing else, yet.
3215        # Bursary or Department Officers don't have the general exportData
3216        # permission and are only allowed to export bursary or payments
3217        # overview data respectively. This is the only place where
3218        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3219        # are used.
3220        exporters = []
3221        if not checkPermission('waeup.exportData', self.context):
3222            if checkPermission('waeup.exportBursaryData', self.context):
3223                exporters += [('Bursary Data', 'bursary')]
3224            if checkPermission('waeup.exportPaymentsOverview', self.context):
3225                exporters += [('Student Payments Overview', 'paymentsoverview')]
3226            self.exporters = exporters
3227            return
3228        STUDENT_EXPORTER_NAMES = getUtility(
3229            IStudentsUtils).STUDENT_EXPORTER_NAMES
3230        for name in STUDENT_EXPORTER_NAMES:
3231            util = getUtility(ICSVExporter, name=name)
3232            exporters.append((util.title, name),)
3233        self.exporters = exporters
3234        return
3235
3236    @property
3237    def faccode(self):
3238        return None
3239
3240    @property
3241    def depcode(self):
3242        return None
3243
3244    @property
3245    def certcode(self):
3246        return None
3247
3248    def update(self, START=None, session=None, level=None, mode=None,
3249               payments_start=None, payments_end=None,
3250               exporter=None):
3251        self._set_session_values()
3252        self._set_level_values()
3253        self._set_mode_values()
3254        self._set_exporter_values()
3255        if START is None:
3256            return
3257        ena = exports_not_allowed(self)
3258        if ena:
3259            self.flash(ena, type='danger')
3260            return
3261        if payments_start or payments_end:
3262            date_format = '%d/%m/%Y'
3263            try:
3264                dummy = datetime.strptime(payments_start, date_format)
3265                dummy = datetime.strptime(payments_end, date_format)
3266            except ValueError:
3267                self.flash(_('Payment dates do not match format d/m/Y.'),
3268                           type="danger")
3269                return
3270        if session == 'all':
3271            session=None
3272        if level == 'all':
3273            level = None
3274        if mode == 'all':
3275            mode = None
3276        if payments_start == '':
3277            payments_start = None
3278        if payments_end == '':
3279            payments_end = None
3280        if (mode,
3281            level,
3282            session,
3283            self.faccode,
3284            self.depcode,
3285            self.certcode) == (None, None, None, None, None, None):
3286            # Export all students including those without certificate
3287            if payments_start:
3288                job_id = self.context.start_export_job(exporter,
3289                                              self.request.principal.id,
3290                                              payments_start = payments_start,
3291                                              payments_end = payments_end)
3292            else:
3293                job_id = self.context.start_export_job(exporter,
3294                                              self.request.principal.id)
3295        else:
3296            if payments_start:
3297                job_id = self.context.start_export_job(exporter,
3298                                              self.request.principal.id,
3299                                              current_session=session,
3300                                              current_level=level,
3301                                              current_mode=mode,
3302                                              faccode=self.faccode,
3303                                              depcode=self.depcode,
3304                                              certcode=self.certcode,
3305                                              payments_start = payments_start,
3306                                              payments_end = payments_end)
3307            else:
3308                job_id = self.context.start_export_job(exporter,
3309                                              self.request.principal.id,
3310                                              current_session=session,
3311                                              current_level=level,
3312                                              current_mode=mode,
3313                                              faccode=self.faccode,
3314                                              depcode=self.depcode,
3315                                              certcode=self.certcode)
3316        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3317        self.context.logger.info(
3318            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3319            % (ob_class, exporter, session, level, mode, self.faccode,
3320            self.depcode, self.certcode, payments_start, payments_end, job_id))
3321        self.flash(_('Export started for students with') +
3322                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3323                   session, level, mode))
3324        self.redirect(self.url(self.redirect_target))
3325        return
3326
3327class ExportJobContainerDownload(ExportCSVView):
3328    """Page that downloads a students export csv file.
3329
3330    """
3331    grok.context(VirtualExportJobContainer)
3332    grok.require('waeup.showStudents')
3333
3334class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3335    """Page that configures a students export job in datacenter.
3336
3337    """
3338    grok.context(IDataCenter)
3339    redirect_target = '@@export'
3340
3341class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3342    """Page that configures a students export job in datacenter.
3343
3344    """
3345    grok.name('exportselected')
3346    grok.context(IDataCenter)
3347    redirect_target = '@@export'
3348    grok.template('exportselected')
3349    label = _('Configure student data export')
3350
3351    def update(self, START=None, students=None, exporter=None):
3352        self._set_exporter_values()
3353        if START is None:
3354            return
3355        ena = exports_not_allowed(self)
3356        if ena:
3357            self.flash(ena, type='danger')
3358            return
3359        try:
3360            ids = students.replace(',', ' ').split()
3361        except:
3362            self.flash(sys.exc_info()[1])
3363            self.redirect(self.url(self.redirect_target))
3364            return
3365        job_id = self.context.start_export_job(
3366            exporter, self.request.principal.id, selected=ids)
3367        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3368        self.context.logger.info(
3369            '%s - selected students exported: %s, job_id=%s' %
3370            (ob_class, exporter, job_id))
3371        self.flash(_('Export of selected students started.'))
3372        self.redirect(self.url(self.redirect_target))
3373        return
3374
3375class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3376    """Page that configures a students export job in facultiescontainer.
3377
3378    """
3379    grok.context(VirtualFacultiesExportJobContainer)
3380
3381
3382class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3383    """Page that configures a students export job in faculties.
3384
3385    """
3386    grok.context(VirtualFacultyExportJobContainer)
3387
3388    @property
3389    def faccode(self):
3390        return self.context.__parent__.code
3391
3392class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3393    """Page that configures a students export job in departments.
3394
3395    """
3396    grok.context(VirtualDepartmentExportJobContainer)
3397
3398    @property
3399    def depcode(self):
3400        return self.context.__parent__.code
3401
3402class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3403    """Page that configures a students export job for certificates.
3404
3405    """
3406    grok.context(VirtualCertificateExportJobContainer)
3407    grok.template('exportconfig_certificate')
3408
3409    @property
3410    def certcode(self):
3411        return self.context.__parent__.code
3412
3413class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3414    """Page that configures a students export job for courses.
3415
3416    In contrast to department or certificate student data exports the
3417    coursetickets_catalog is searched here. Therefore the update
3418    method from the base class is customized.
3419    """
3420    grok.context(VirtualCourseExportJobContainer)
3421    grok.template('exportconfig_course')
3422
3423    def _set_exporter_values(self):
3424        # We provide only the 'lecturer' exporter but can add more.
3425        exporters = []
3426        for name in ('lecturer',):
3427            util = getUtility(ICSVExporter, name=name)
3428            exporters.append((util.title, name),)
3429        self.exporters = exporters
3430
3431    def _set_session_values(self):
3432        # We allow only current academic session
3433        academic_session = grok.getSite()['configuration'].current_academic_session
3434        if not academic_session:
3435            self.sessions = []
3436            return
3437        x = academic_sessions_vocab.getTerm(academic_session)
3438        self.sessions = [(x.title, x.token)]
3439        return
3440
3441    def update(self, START=None, session=None, level=None, mode=None,
3442               exporter=None):
3443        self._set_session_values()
3444        self._set_level_values()
3445        self._set_mode_values()
3446        self._set_exporter_values()
3447        if not self.sessions:
3448            self.flash(
3449                _('Academic session not set. '
3450                  'Please contact the administrator.'),
3451                type='danger')
3452            self.redirect(self.url(self.context))
3453            return
3454        if START is None:
3455            return
3456        ena = exports_not_allowed(self)
3457        if ena:
3458            self.flash(ena, type='danger')
3459            return
3460        if session == 'all':
3461            session = None
3462        if level == 'all':
3463            level = None
3464        job_id = self.context.start_export_job(exporter,
3465                                      self.request.principal.id,
3466                                      # Use a different catalog and
3467                                      # pass different keywords than
3468                                      # for the (default) students_catalog
3469                                      catalog='coursetickets',
3470                                      session=session,
3471                                      level=level,
3472                                      code=self.context.__parent__.code)
3473        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3474        self.context.logger.info(
3475            '%s - exported: %s (%s, %s, %s), job_id=%s'
3476            % (ob_class, exporter, session, level,
3477            self.context.__parent__.code, job_id))
3478        self.flash(_('Export started for course tickets with') +
3479                   ' level_session=%s, level=%s' % (
3480                   session, level))
3481        self.redirect(self.url(self.redirect_target))
3482        return
Note: See TracBrowser for help on using the repository browser.