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

Last change on this file since 10627 was 10627, checked in by Henrik Bettermann, 11 years ago

Implement view for batch-editing of scores for lecturers. This is work in progress!

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