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

Last change on this file since 17088 was 16939, checked in by Henrik Bettermann, 3 years ago

Not next but last student id.

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