source: main/waeup.kofa/branches/henrik-transcript-workflow/src/waeup/kofa/students/browser.py @ 15158

Last change on this file since 15158 was 15158, checked in by Henrik Bettermann, 6 years ago

Allow signees to sign only once.
Show signatures on transcript page.

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