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

Last change on this file since 9804 was 9804, checked in by uli, 12 years ago

Provide all student exporters in department-local exports.

  • Property svn:keywords set to Id
File size: 94.5 KB
Line 
1## $Id: browser.py 9804 2012-12-16 15:37:10Z uli $
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
22from urllib import urlencode
23from datetime import datetime
24from copy import deepcopy
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 hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
32from waeup.kofa.accesscodes import (
33    invalidate_accesscode, get_access_code)
34from waeup.kofa.accesscodes.workflow import USED
35from waeup.kofa.browser.layout import (
36    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
37    KofaForm, NullValidator)
38from waeup.kofa.browser.breadcrumbs import Breadcrumb
39from waeup.kofa.browser.pages import ContactAdminForm
40from waeup.kofa.browser.resources import (
41    datepicker, datatable, tabs, warning, toggleall)
42from waeup.kofa.browser.layout import jsaction, action, UtilityView
43from waeup.kofa.browser.interfaces import ICaptchaManager
44from waeup.kofa.hostels.hostel import NOT_OCCUPIED
45from waeup.kofa.interfaces import (
46    IKofaObject, IUserAccount, IExtFileStore, IPasswordValidator, IContactForm,
47    IKofaUtils, IUniversity, IObjectHistory, academic_sessions, ICSVExporter,
48    academic_sessions_vocab, IJobManager)
49from waeup.kofa.interfaces import MessageFactory as _
50from waeup.kofa.widgets.datewidget import (
51    FriendlyDateWidget, FriendlyDateDisplayWidget,
52    FriendlyDatetimeDisplayWidget)
53from waeup.kofa.mandates.mandate import PasswordMandate
54from waeup.kofa.university.department import (
55    VirtualDepartmentExportJobContainer,)
56from waeup.kofa.university.vocabularies import course_levels
57from waeup.kofa.utils.helpers import get_current_principal, to_timezone
58from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
59from waeup.kofa.students.interfaces import (
60    IStudentsContainer, IStudent,
61    IUGStudentClearance,IPGStudentClearance,
62    IStudentPersonal, IStudentPersonalEdit, IStudentBase, IStudentStudyCourse,
63    IStudentStudyCourseTransfer,
64    IStudentAccommodation, IStudentStudyLevel,
65    ICourseTicket, ICourseTicketAdd, IStudentPaymentsContainer,
66    IStudentOnlinePayment, IStudentPreviousPayment,
67    IBedTicket, IStudentsUtils, IStudentRequestPW
68    )
69from waeup.kofa.students.catalog import search, StudentsQuery
70from waeup.kofa.students.export import EXPORTER_NAMES
71from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
72from waeup.kofa.students.vocabularies import StudyLevelSource
73from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
74    CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
75    FORBIDDEN_POSTGRAD_TRANS)
76
77
78grok.context(IKofaObject) # Make IKofaObject the default context
79
80# Save function used for save methods in pages
81def msave(view, **data):
82    changed_fields = view.applyData(view.context, **data)
83    # Turn list of lists into single list
84    if changed_fields:
85        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
86    # Inform catalog if certificate has changed
87    # (applyData does this only for the context)
88    if 'certificate' in changed_fields:
89        notify(grok.ObjectModifiedEvent(view.context.student))
90    fields_string = ' + '.join(changed_fields)
91    view.flash(_('Form has been saved.'))
92    if fields_string:
93        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
94    return
95
96def emit_lock_message(view):
97    """Flash a lock message.
98    """
99    view.flash(_('The requested form is locked (read-only).'))
100    view.redirect(view.url(view.context))
101    return
102
103def translated_values(view):
104    """Translate course ticket attribute values to be displayed on
105    studylevel pages.
106    """
107    lang = view.request.cookies.get('kofa.language')
108    for value in view.context.values():
109        # We have to unghostify (according to Tres Seaver) the __dict__
110        # by activating the object, otherwise value_dict will be empty
111        # when calling the first time.
112        value._p_activate()
113        value_dict = dict([i for i in value.__dict__.items()])
114        value_dict['removable_by_student'] = value.removable_by_student
115        value_dict['mandatory'] = translate(str(value.mandatory), 'zope',
116            target_language=lang)
117        value_dict['carry_over'] = translate(str(value.carry_over), 'zope',
118            target_language=lang)
119        value_dict['automatic'] = translate(str(value.automatic), 'zope',
120            target_language=lang)
121        value_dict['grade'] = value.grade
122        value_dict['weight'] = value.weight
123        yield value_dict
124
125class StudentsBreadcrumb(Breadcrumb):
126    """A breadcrumb for the students container.
127    """
128    grok.context(IStudentsContainer)
129    title = _('Students')
130
131    @property
132    def target(self):
133        user = get_current_principal()
134        if getattr(user, 'user_type', None) == 'student':
135            return None
136        return self.viewname
137
138class StudentBreadcrumb(Breadcrumb):
139    """A breadcrumb for the student container.
140    """
141    grok.context(IStudent)
142
143    def title(self):
144        return self.context.display_fullname
145
146class SudyCourseBreadcrumb(Breadcrumb):
147    """A breadcrumb for the student study course.
148    """
149    grok.context(IStudentStudyCourse)
150
151    def title(self):
152        if self.context.is_current:
153            return _('Study Course')
154        else:
155            return _('Previous Study Course')
156
157class PaymentsBreadcrumb(Breadcrumb):
158    """A breadcrumb for the student payments folder.
159    """
160    grok.context(IStudentPaymentsContainer)
161    title = _('Payments')
162
163class OnlinePaymentBreadcrumb(Breadcrumb):
164    """A breadcrumb for payments.
165    """
166    grok.context(IStudentOnlinePayment)
167
168    @property
169    def title(self):
170        return self.context.p_id
171
172class AccommodationBreadcrumb(Breadcrumb):
173    """A breadcrumb for the student accommodation folder.
174    """
175    grok.context(IStudentAccommodation)
176    title = _('Accommodation')
177
178class BedTicketBreadcrumb(Breadcrumb):
179    """A breadcrumb for bed tickets.
180    """
181    grok.context(IBedTicket)
182
183    @property
184    def title(self):
185        return _('Bed Ticket ${a}',
186            mapping = {'a':self.context.getSessionString()})
187
188class StudyLevelBreadcrumb(Breadcrumb):
189    """A breadcrumb for course lists.
190    """
191    grok.context(IStudentStudyLevel)
192
193    @property
194    def title(self):
195        return self.context.level_title
196
197class StudentsContainerPage(KofaPage):
198    """The standard view for student containers.
199    """
200    grok.context(IStudentsContainer)
201    grok.name('index')
202    grok.require('waeup.viewStudentsContainer')
203    grok.template('containerpage')
204    label = _('Student Section')
205    search_button = _('Search')
206    pnav = 4
207
208    def update(self, *args, **kw):
209        datatable.need()
210        form = self.request.form
211        self.hitlist = []
212        if form.get('searchtype', None) == 'suspended':
213            self.searchtype = form['searchtype']
214            self.searchterm = None
215        elif 'searchterm' in form and form['searchterm']:
216            self.searchterm = form['searchterm']
217            self.searchtype = form['searchtype']
218        elif 'old_searchterm' in form:
219            self.searchterm = form['old_searchterm']
220            self.searchtype = form['old_searchtype']
221        else:
222            if 'search' in form:
223                self.flash(_('Empty search string'))
224            return
225        if self.searchtype == 'current_session':
226            try:
227                self.searchterm = int(self.searchterm)
228            except ValueError:
229                self.flash(_('Only year dates allowed (e.g. 2011).'))
230                return
231        self.hitlist = search(query=self.searchterm,
232            searchtype=self.searchtype, view=self)
233        if not self.hitlist:
234            self.flash(_('No student found.'))
235        return
236
237class StudentsContainerManagePage(KofaPage):
238    """The manage page for student containers.
239    """
240    grok.context(IStudentsContainer)
241    grok.name('manage')
242    grok.require('waeup.manageStudent')
243    grok.template('containermanagepage')
244    pnav = 4
245    label = _('Manage student section')
246    search_button = _('Search')
247    remove_button = _('Remove selected')
248
249    def update(self, *args, **kw):
250        datatable.need()
251        toggleall.need()
252        warning.need()
253        form = self.request.form
254        self.hitlist = []
255        if form.get('searchtype', None) == 'suspended':
256            self.searchtype = form['searchtype']
257            self.searchterm = None
258        elif 'searchterm' in form and form['searchterm']:
259            self.searchterm = form['searchterm']
260            self.searchtype = form['searchtype']
261        elif 'old_searchterm' in form:
262            self.searchterm = form['old_searchterm']
263            self.searchtype = form['old_searchtype']
264        else:
265            if 'search' in form:
266                self.flash(_('Empty search string'))
267            return
268        if self.searchtype == 'current_session':
269            try:
270                self.searchterm = int(self.searchterm)
271            except ValueError:
272                self.flash('Only year dates allowed (e.g. 2011).')
273                return
274        if not 'entries' in form:
275            self.hitlist = search(query=self.searchterm,
276                searchtype=self.searchtype, view=self)
277            if not self.hitlist:
278                self.flash(_('No student found.'))
279            if 'remove' in form:
280                self.flash(_('No item selected.'))
281            return
282        entries = form['entries']
283        if isinstance(entries, basestring):
284            entries = [entries]
285        deleted = []
286        for entry in entries:
287            if 'remove' in form:
288                del self.context[entry]
289                deleted.append(entry)
290        self.hitlist = search(query=self.searchterm,
291            searchtype=self.searchtype, view=self)
292        if len(deleted):
293            self.flash(_('Successfully removed: ${a}',
294                mapping = {'a':', '.join(deleted)}))
295        return
296
297class StudentAddFormPage(KofaAddFormPage):
298    """Add-form to add a student.
299    """
300    grok.context(IStudentsContainer)
301    grok.require('waeup.manageStudent')
302    grok.name('addstudent')
303    form_fields = grok.AutoFields(IStudent).select(
304        'firstname', 'middlename', 'lastname', 'reg_number')
305    label = _('Add student')
306    pnav = 4
307
308    @action(_('Create student record'), style='primary')
309    def addStudent(self, **data):
310        student = createObject(u'waeup.Student')
311        self.applyData(student, **data)
312        self.context.addStudent(student)
313        self.flash(_('Student record created.'))
314        self.redirect(self.url(self.context[student.student_id], 'index'))
315        return
316
317class LoginAsStudentStep1(KofaEditFormPage):
318    """ View to temporarily set a student password.
319    """
320    grok.context(IStudent)
321    grok.name('loginasstep1')
322    grok.require('waeup.loginAsStudent')
323    grok.template('loginasstep1')
324    pnav = 4
325
326    def label(self):
327        return _(u'Set temporary password for ${a}',
328            mapping = {'a':self.context.display_fullname})
329
330    @action('Set password now', style='primary')
331    def setPassword(self, *args, **data):
332        kofa_utils = getUtility(IKofaUtils)
333        password = kofa_utils.genPassword()
334        self.context.setTempPassword(self.request.principal.id, password)
335        self.context.writeLogMessage(
336            self, 'temp_password generated: %s' % password)
337        args = {'password':password}
338        self.redirect(self.url(self.context) +
339            '/loginasstep2?%s' % urlencode(args))
340        return
341
342class LoginAsStudentStep2(KofaPage):
343    """ View to temporarily login as student with a temporary password.
344    """
345    grok.context(IStudent)
346    grok.name('loginasstep2')
347    grok.require('waeup.Public')
348    grok.template('loginasstep2')
349    login_button = _('Login now')
350    pnav = 4
351
352    def label(self):
353        return _(u'Login as ${a}',
354            mapping = {'a':self.context.student_id})
355
356    def update(self, SUBMIT=None, password=None):
357        self.password = password
358        if SUBMIT is not None:
359            self.flash(_('You successfully logged in as student.'))
360            self.redirect(self.url(self.context))
361        return
362
363class StudentBaseDisplayFormPage(KofaDisplayFormPage):
364    """ Page to display student base data
365    """
366    grok.context(IStudent)
367    grok.name('index')
368    grok.require('waeup.viewStudent')
369    grok.template('basepage')
370    form_fields = grok.AutoFields(IStudentBase).omit(
371        'password', 'suspended', 'suspended_comment')
372    pnav = 4
373
374    @property
375    def label(self):
376        if self.context.suspended:
377            return _('${a}: Base Data (account deactivated)',
378                mapping = {'a':self.context.display_fullname})
379        return  _('${a}: Base Data',
380            mapping = {'a':self.context.display_fullname})
381
382    @property
383    def hasPassword(self):
384        if self.context.password:
385            return _('set')
386        return _('unset')
387
388class StudentBasePDFFormPage(KofaDisplayFormPage):
389    """ Page to display student base data in pdf files.
390    """
391
392    def __init__(self, context, request, omit_fields):
393        self.omit_fields = omit_fields
394        super(StudentBasePDFFormPage, self).__init__(context, request)
395
396    @property
397    def form_fields(self):
398        form_fields = grok.AutoFields(IStudentBase)
399        for field in self.omit_fields:
400            form_fields = form_fields.omit(field)
401        return form_fields
402
403class ContactStudentForm(ContactAdminForm):
404    grok.context(IStudent)
405    grok.name('contactstudent')
406    grok.require('waeup.viewStudent')
407    pnav = 4
408    form_fields = grok.AutoFields(IContactForm).select('subject', 'body')
409
410    def update(self, subject=u'', body=u''):
411        self.form_fields.get('subject').field.default = subject
412        self.form_fields.get('body').field.default = body
413        return super(ContactStudentForm, self).update()
414
415    def label(self):
416        return _(u'Send message to ${a}',
417            mapping = {'a':self.context.display_fullname})
418
419    @action('Send message now', style='primary')
420    def send(self, *args, **data):
421        try:
422            email = self.request.principal.email
423        except AttributeError:
424            email = self.config.email_admin
425        usertype = getattr(self.request.principal,
426                           'user_type', 'system').title()
427        kofa_utils = getUtility(IKofaUtils)
428        success = kofa_utils.sendContactForm(
429                self.request.principal.title,email,
430                self.context.display_fullname,self.context.email,
431                self.request.principal.id,usertype,
432                self.config.name,
433                data['body'],data['subject'])
434        if success:
435            self.flash(_('Your message has been sent.'))
436        else:
437            self.flash(_('An smtp server error occurred.'))
438        return
439
440class ExportPDFAdmissionSlipPage(UtilityView, grok.View):
441    """Deliver a PDF Admission slip.
442    """
443    grok.context(IStudent)
444    grok.name('admission_slip.pdf')
445    grok.require('waeup.viewStudent')
446    prefix = 'form'
447
448    form_fields = grok.AutoFields(IStudentBase).select('student_id', 'reg_number')
449
450    @property
451    def label(self):
452        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
453        return translate(_('Admission Letter of'),
454            'waeup.kofa', target_language=portal_language) \
455            + ' %s' % self.context.display_fullname
456
457    def render(self):
458        students_utils = getUtility(IStudentsUtils)
459        return students_utils.renderPDFAdmissionLetter(self,
460            self.context.student)
461
462class StudentBaseManageFormPage(KofaEditFormPage):
463    """ View to manage student base data
464    """
465    grok.context(IStudent)
466    grok.name('manage_base')
467    grok.require('waeup.manageStudent')
468    form_fields = grok.AutoFields(IStudentBase).omit(
469        'student_id', 'adm_code', 'suspended')
470    grok.template('basemanagepage')
471    label = _('Manage base data')
472    pnav = 4
473
474    def update(self):
475        datepicker.need() # Enable jQuery datepicker in date fields.
476        tabs.need()
477        self.tab1 = self.tab2 = ''
478        qs = self.request.get('QUERY_STRING', '')
479        if not qs:
480            qs = 'tab1'
481        setattr(self, qs, 'active')
482        super(StudentBaseManageFormPage, self).update()
483        self.wf_info = IWorkflowInfo(self.context)
484        return
485
486    @action(_('Save'), style='primary')
487    def save(self, **data):
488        form = self.request.form
489        password = form.get('password', None)
490        password_ctl = form.get('control_password', None)
491        if password:
492            validator = getUtility(IPasswordValidator)
493            errors = validator.validate_password(password, password_ctl)
494            if errors:
495                self.flash( ' '.join(errors))
496                return
497        changed_fields = self.applyData(self.context, **data)
498        # Turn list of lists into single list
499        if changed_fields:
500            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
501        else:
502            changed_fields = []
503        if password:
504            # Now we know that the form has no errors and can set password
505            IUserAccount(self.context).setPassword(password)
506            changed_fields.append('password')
507        fields_string = ' + '.join(changed_fields)
508        self.flash(_('Form has been saved.'))
509        if fields_string:
510            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
511        return
512
513class StudentTriggerTransitionFormPage(KofaEditFormPage):
514    """ View to manage student base data
515    """
516    grok.context(IStudent)
517    grok.name('trigtrans')
518    grok.require('waeup.triggerTransition')
519    grok.template('trigtrans')
520    label = _('Trigger registration transition')
521    pnav = 4
522
523    def getTransitions(self):
524        """Return a list of dicts of allowed transition ids and titles.
525
526        Each list entry provides keys ``name`` and ``title`` for
527        internal name and (human readable) title of a single
528        transition.
529        """
530        wf_info = IWorkflowInfo(self.context)
531        allowed_transitions = [t for t in wf_info.getManualTransitions()
532            if not t[0].startswith('pay')]
533        if self.context.is_postgrad:
534            allowed_transitions = [t for t in allowed_transitions
535                if not t[0] in FORBIDDEN_POSTGRAD_TRANS]
536        return [dict(name='', title=_('No transition'))] +[
537            dict(name=x, title=y) for x, y in allowed_transitions]
538
539    @action(_('Save'), style='primary')
540    def save(self, **data):
541        form = self.request.form
542        if 'transition' in form and form['transition']:
543            transition_id = form['transition']
544            wf_info = IWorkflowInfo(self.context)
545            wf_info.fireTransition(transition_id)
546        return
547
548class StudentActivatePage(UtilityView, grok.View):
549    """ Activate student account
550    """
551    grok.context(IStudent)
552    grok.name('activate')
553    grok.require('waeup.manageStudent')
554
555    def update(self):
556        self.context.suspended = False
557        self.context.writeLogMessage(self, 'account activated')
558        history = IObjectHistory(self.context)
559        history.addMessage('Student account activated')
560        self.flash(_('Student account has been activated.'))
561        self.redirect(self.url(self.context))
562        return
563
564    def render(self):
565        return
566
567class StudentDeactivatePage(UtilityView, grok.View):
568    """ Deactivate student account
569    """
570    grok.context(IStudent)
571    grok.name('deactivate')
572    grok.require('waeup.manageStudent')
573
574    def update(self):
575        self.context.suspended = True
576        self.context.writeLogMessage(self, 'account deactivated')
577        history = IObjectHistory(self.context)
578        history.addMessage('Student account deactivated')
579        self.flash(_('Student account has been deactivated.'))
580        self.redirect(self.url(self.context))
581        return
582
583    def render(self):
584        return
585
586class StudentClearanceDisplayFormPage(KofaDisplayFormPage):
587    """ Page to display student clearance data
588    """
589    grok.context(IStudent)
590    grok.name('view_clearance')
591    grok.require('waeup.viewStudent')
592    pnav = 4
593
594    @property
595    def separators(self):
596        return getUtility(IStudentsUtils).SEPARATORS_DICT
597
598    @property
599    def form_fields(self):
600        if self.context.is_postgrad:
601            form_fields = grok.AutoFields(
602                IPGStudentClearance).omit('clearance_locked')
603        else:
604            form_fields = grok.AutoFields(
605                IUGStudentClearance).omit('clearance_locked')
606        if not getattr(self.context, 'officer_comment'):
607            form_fields = form_fields.omit('officer_comment')
608        else:
609            form_fields['officer_comment'].custom_widget = BytesDisplayWidget
610        return form_fields
611
612    @property
613    def label(self):
614        return _('${a}: Clearance Data',
615            mapping = {'a':self.context.display_fullname})
616
617class ExportPDFClearanceSlipPage(grok.View):
618    """Deliver a PDF slip of the context.
619    """
620    grok.context(IStudent)
621    grok.name('clearance_slip.pdf')
622    grok.require('waeup.viewStudent')
623    prefix = 'form'
624    omit_fields = (
625        'password', 'suspended', 'phone',
626        'adm_code', 'suspended_comment')
627
628    @property
629    def form_fields(self):
630        if self.context.is_postgrad:
631            form_fields = grok.AutoFields(
632                IPGStudentClearance).omit('clearance_locked')
633        else:
634            form_fields = grok.AutoFields(
635                IUGStudentClearance).omit('clearance_locked')
636        if not getattr(self.context, 'officer_comment'):
637            form_fields = form_fields.omit('officer_comment')
638        return form_fields
639
640    @property
641    def title(self):
642        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
643        return translate(_('Clearance Data'), 'waeup.kofa',
644            target_language=portal_language)
645
646    @property
647    def label(self):
648        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
649        return translate(_('Clearance Slip of'),
650            'waeup.kofa', target_language=portal_language) \
651            + ' %s' % self.context.display_fullname
652
653    def _signatures(self):
654        isStudent = getattr(
655            self.request.principal, 'user_type', None) == 'student'
656        if not isStudent and self.context.state in (CLEARED, ):
657            return (_('Student Signature'), _('Clearance Officer Signature'))
658        return
659
660    def _sigsInFooter(self):
661        isStudent = getattr(
662            self.request.principal, 'user_type', None) == 'student'
663        if not isStudent and self.context.state in (CLEARED, ):
664            return (_('Date, Student Signature'),
665                    _('Date, Clearance Officer Signature'),
666                    )
667        return ()
668
669    def render(self):
670        studentview = StudentBasePDFFormPage(self.context.student,
671            self.request, self.omit_fields)
672        students_utils = getUtility(IStudentsUtils)
673        return students_utils.renderPDF(
674            self, 'clearance_slip.pdf',
675            self.context.student, studentview, signatures=self._signatures(),
676            sigs_in_footer=self._sigsInFooter())
677
678class StudentClearanceManageFormPage(KofaEditFormPage):
679    """ Page to manage student clearance data
680    """
681    grok.context(IStudent)
682    grok.name('manage_clearance')
683    grok.require('waeup.manageStudent')
684    grok.template('clearanceeditpage')
685    label = _('Manage clearance data')
686    pnav = 4
687
688    @property
689    def separators(self):
690        return getUtility(IStudentsUtils).SEPARATORS_DICT
691
692    @property
693    def form_fields(self):
694        if self.context.is_postgrad:
695            form_fields = grok.AutoFields(IPGStudentClearance).omit('clr_code')
696        else:
697            form_fields = grok.AutoFields(IUGStudentClearance).omit('clr_code')
698        return form_fields
699
700    def update(self):
701        datepicker.need() # Enable jQuery datepicker in date fields.
702        tabs.need()
703        self.tab1 = self.tab2 = ''
704        qs = self.request.get('QUERY_STRING', '')
705        if not qs:
706            qs = 'tab1'
707        setattr(self, qs, 'active')
708        return super(StudentClearanceManageFormPage, self).update()
709
710    @action(_('Save'), style='primary')
711    def save(self, **data):
712        msave(self, **data)
713        return
714
715class StudentClearPage(UtilityView, grok.View):
716    """ Clear student by clearance officer
717    """
718    grok.context(IStudent)
719    grok.name('clear')
720    grok.require('waeup.clearStudent')
721
722    def update(self):
723        if self.context.state == REQUESTED:
724            IWorkflowInfo(self.context).fireTransition('clear')
725            self.flash(_('Student has been cleared.'))
726        else:
727            self.flash(_('Student is in wrong state.'))
728        self.redirect(self.url(self.context,'view_clearance'))
729        return
730
731    def render(self):
732        return
733
734class StudentRejectClearancePage(KofaEditFormPage):
735    """ Reject clearance by clearance officers
736    """
737    grok.context(IStudent)
738    grok.name('reject_clearance')
739    label = _('Reject clearance')
740    grok.require('waeup.clearStudent')
741    form_fields = grok.AutoFields(
742        IUGStudentClearance).select('officer_comment')
743
744    @action(_('Save comment and reject clearance now'), style='primary')
745    def reject(self, **data):
746        if self.context.state == CLEARED:
747            IWorkflowInfo(self.context).fireTransition('reset4')
748            message = _('Clearance has been annulled.')
749            self.flash(message)
750        elif self.context.state == REQUESTED:
751            IWorkflowInfo(self.context).fireTransition('reset3')
752            message = _('Clearance request has been rejected.')
753            self.flash(message)
754        else:
755            self.flash(_('Student is in wrong state.'))
756            self.redirect(self.url(self.context,'view_clearance'))
757            return
758        self.applyData(self.context, **data)
759        comment = data['officer_comment']
760        if comment:
761            self.context.writeLogMessage(
762                self, 'comment: %s' % comment.replace('\n', '<br>'))
763            args = {'subject':message, 'body':comment}
764        else:
765            args = {'subject':message,}
766        self.redirect(self.url(self.context) +
767            '/contactstudent?%s' % urlencode(args))
768        return
769
770    #def render(self):
771    #    return
772
773class StudentPersonalDisplayFormPage(KofaDisplayFormPage):
774    """ Page to display student personal data
775    """
776    grok.context(IStudent)
777    grok.name('view_personal')
778    grok.require('waeup.viewStudent')
779    form_fields = grok.AutoFields(IStudentPersonal)
780    form_fields['perm_address'].custom_widget = BytesDisplayWidget
781    form_fields[
782        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
783    pnav = 4
784
785    @property
786    def label(self):
787        return _('${a}: Personal Data',
788            mapping = {'a':self.context.display_fullname})
789
790class StudentPersonalManageFormPage(KofaEditFormPage):
791    """ Page to manage personal data
792    """
793    grok.context(IStudent)
794    grok.name('manage_personal')
795    grok.require('waeup.manageStudent')
796    form_fields = grok.AutoFields(IStudentPersonal)
797    form_fields['personal_updated'].for_display = True
798    form_fields[
799        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
800    label = _('Manage personal data')
801    pnav = 4
802
803    @action(_('Save'), style='primary')
804    def save(self, **data):
805        msave(self, **data)
806        return
807
808class StudentPersonalEditFormPage(KofaEditFormPage):
809    """ Page to edit personal data
810    """
811    grok.context(IStudent)
812    grok.name('edit_personal')
813    grok.require('waeup.handleStudent')
814    form_fields = grok.AutoFields(IStudentPersonalEdit).omit('personal_updated')
815    label = _('Edit personal data')
816    pnav = 4
817
818    @action(_('Save/Confirm'), style='primary')
819    def save(self, **data):
820        msave(self, **data)
821        self.context.personal_updated = datetime.utcnow()
822        return
823
824class StudyCourseDisplayFormPage(KofaDisplayFormPage):
825    """ Page to display the student study course data
826    """
827    grok.context(IStudentStudyCourse)
828    grok.name('index')
829    grok.require('waeup.viewStudent')
830    grok.template('studycoursepage')
831    pnav = 4
832
833    @property
834    def form_fields(self):
835        if self.context.is_postgrad:
836            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
837                'previous_verdict')
838        else:
839            form_fields = grok.AutoFields(IStudentStudyCourse)
840        return form_fields
841
842    @property
843    def label(self):
844        if self.context.is_current:
845            return _('${a}: Study Course',
846                mapping = {'a':self.context.__parent__.display_fullname})
847        else:
848            return _('${a}: Previous Study Course',
849                mapping = {'a':self.context.__parent__.display_fullname})
850
851    @property
852    def current_mode(self):
853        if self.context.certificate is not None:
854            studymodes_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
855            return studymodes_dict[self.context.certificate.study_mode]
856        return
857
858    @property
859    def department(self):
860        if self.context.certificate is not None:
861            return self.context.certificate.__parent__.__parent__
862        return
863
864    @property
865    def faculty(self):
866        if self.context.certificate is not None:
867            return self.context.certificate.__parent__.__parent__.__parent__
868        return
869
870    @property
871    def prev_studycourses(self):
872        if self.context.is_current:
873            if self.context.__parent__.get('studycourse_2', None) is not None:
874                return (
875                        {'href':self.url(self.context.student) + '/studycourse_1',
876                        'title':_('First Study Course, ')},
877                        {'href':self.url(self.context.student) + '/studycourse_2',
878                        'title':_('Second Study Course')}
879                        )
880            if self.context.__parent__.get('studycourse_1', None) is not None:
881                return (
882                        {'href':self.url(self.context.student) + '/studycourse_1',
883                        'title':_('First Study Course')},
884                        )
885        return
886
887class StudyCourseManageFormPage(KofaEditFormPage):
888    """ Page to edit the student study course data
889    """
890    grok.context(IStudentStudyCourse)
891    grok.name('manage')
892    grok.require('waeup.manageStudent')
893    grok.template('studycoursemanagepage')
894    label = _('Manage study course')
895    pnav = 4
896    taboneactions = [_('Save'),_('Cancel')]
897    tabtwoactions = [_('Remove selected levels'),_('Cancel')]
898    tabthreeactions = [_('Add study level')]
899
900    @property
901    def form_fields(self):
902        if self.context.is_postgrad:
903            form_fields = grok.AutoFields(IStudentStudyCourse).omit(
904                'previous_verdict')
905        else:
906            form_fields = grok.AutoFields(IStudentStudyCourse)
907        return form_fields
908
909    def update(self):
910        if not self.context.is_current:
911            emit_lock_message(self)
912            return
913        super(StudyCourseManageFormPage, self).update()
914        tabs.need()
915        self.tab1 = self.tab2 = ''
916        qs = self.request.get('QUERY_STRING', '')
917        if not qs:
918            qs = 'tab1'
919        setattr(self, qs, 'active')
920        warning.need()
921        datatable.need()
922        return
923
924    @action(_('Save'), style='primary')
925    def save(self, **data):
926        try:
927            msave(self, **data)
928        except ConstraintNotSatisfied:
929            # The selected level might not exist in certificate
930            self.flash(_('Current level not available for certificate.'))
931            return
932        notify(grok.ObjectModifiedEvent(self.context.__parent__))
933        return
934
935    @property
936    def level_dict(self):
937        studylevelsource = StudyLevelSource().factory
938        for code in studylevelsource.getValues(self.context):
939            title = studylevelsource.getTitle(self.context, code)
940            yield(dict(code=code, title=title))
941
942    @property
943    def session_dict(self):
944        yield(dict(code='', title='--'))
945        for item in academic_sessions():
946            code = item[1]
947            title = item[0]
948            yield(dict(code=code, title=title))
949
950    @action(_('Add study level'))
951    def addStudyLevel(self, **data):
952        level_code = self.request.form.get('addlevel', None)
953        level_session = self.request.form.get('level_session', None)
954        if not level_session:
955            self.flash(_('You must select a session for the level.'))
956            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
957            return
958        studylevel = createObject(u'waeup.StudentStudyLevel')
959        studylevel.level = int(level_code)
960        studylevel.level_session = int(level_session)
961        try:
962            self.context.addStudentStudyLevel(
963                self.context.certificate,studylevel)
964            self.flash(_('Study level has been added.'))
965        except KeyError:
966            self.flash(_('This level exists.'))
967        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
968        return
969
970    @jsaction(_('Remove selected levels'))
971    def delStudyLevels(self, **data):
972        form = self.request.form
973        if 'val_id' in form:
974            child_id = form['val_id']
975        else:
976            self.flash(_('No study level selected.'))
977            self.redirect(self.url(self.context, '@@manage')+'?tab2')
978            return
979        if not isinstance(child_id, list):
980            child_id = [child_id]
981        deleted = []
982        for id in child_id:
983            del self.context[id]
984            deleted.append(id)
985        if len(deleted):
986            self.flash(_('Successfully removed: ${a}',
987                mapping = {'a':', '.join(deleted)}))
988            self.context.writeLogMessage(
989                self,'removed: %s' % ', '.join(deleted))
990        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
991        return
992
993class StudentTransferFormPage(KofaAddFormPage):
994    """Page to transfer the student.
995    """
996    grok.context(IStudent)
997    grok.name('transfer')
998    grok.require('waeup.manageStudent')
999    label = _('Transfer student')
1000    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1001        'entry_mode', 'entry_session')
1002    pnav = 4
1003
1004    def update(self):
1005        super(StudentTransferFormPage, self).update()
1006        warning.need()
1007        return
1008
1009    @jsaction(_('Transfer'))
1010    def transferStudent(self, **data):
1011        error = self.context.transfer(**data)
1012        if error == -1:
1013            self.flash(_('Current level does not match certificate levels.'))
1014        elif error == -2:
1015            self.flash(_('Former study course record incomplete.'))
1016        elif error == -3:
1017            self.flash(_('Maximum number of transfers exceeded.'))
1018        else:
1019            self.flash(_('Successfully transferred.'))
1020        return
1021
1022class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1023    """ Page to display student study levels
1024    """
1025    grok.context(IStudentStudyLevel)
1026    grok.name('index')
1027    grok.require('waeup.viewStudent')
1028    form_fields = grok.AutoFields(IStudentStudyLevel)
1029    form_fields[
1030        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1031    grok.template('studylevelpage')
1032    pnav = 4
1033
1034    def update(self):
1035        super(StudyLevelDisplayFormPage, self).update()
1036        datatable.need()
1037        return
1038
1039    @property
1040    def translated_values(self):
1041        return translated_values(self)
1042
1043    @property
1044    def label(self):
1045        # Here we know that the cookie has been set
1046        lang = self.request.cookies.get('kofa.language')
1047        level_title = translate(self.context.level_title, 'waeup.kofa',
1048            target_language=lang)
1049        return _('${a}: Study Level ${b}', mapping = {
1050            'a':self.context.student.display_fullname,
1051            'b':level_title})
1052
1053class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1054    """Deliver a PDF slip of the context.
1055    """
1056    grok.context(IStudentStudyLevel)
1057    grok.name('course_registration_slip.pdf')
1058    grok.require('waeup.viewStudent')
1059    form_fields = grok.AutoFields(IStudentStudyLevel)
1060    form_fields[
1061        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1062    prefix = 'form'
1063    omit_fields = (
1064        'password', 'suspended', 'phone',
1065        'adm_code', 'sex', 'suspended_comment')
1066
1067    @property
1068    def title(self):
1069        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1070        return translate(_('Level Data'), 'waeup.kofa',
1071            target_language=portal_language)
1072
1073    @property
1074    def content_title(self):
1075        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1076        return translate(_('Course List'), 'waeup.kofa',
1077            target_language=portal_language)
1078
1079    @property
1080    def label(self):
1081        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1082        lang = self.request.cookies.get('kofa.language', portal_language)
1083        level_title = translate(self.context.level_title, 'waeup.kofa',
1084            target_language=lang)
1085        return translate(_('Course Registration Slip'),
1086            'waeup.kofa', target_language=portal_language) \
1087            + ' %s' % level_title
1088
1089    def render(self):
1090        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1091        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1092        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1093        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1094        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1095        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1096        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1097        Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1098        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1099        studentview = StudentBasePDFFormPage(self.context.student,
1100            self.request, self.omit_fields)
1101        students_utils = getUtility(IStudentsUtils)
1102        tabledata = sorted(self.context.values(),
1103            key=lambda value: str(value.semester) + value.code)
1104        return students_utils.renderPDF(
1105            self, 'course_registration_slip.pdf',
1106            self.context.student, studentview,
1107            tableheader=[(Sem,'semester', 1.5),(Code,'code', 2.5),
1108                         (Title,'title', 5),
1109                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1110                         (Cred, 'credits', 1.5),
1111                         (Mand, 'mandatory', 1.5),
1112                         (Score, 'score', 1.5),
1113                         #('Auto', 'automatic', 1.5)
1114                         ],
1115            tabledata=tabledata)
1116
1117class StudyLevelManageFormPage(KofaEditFormPage):
1118    """ Page to edit the student study level data
1119    """
1120    grok.context(IStudentStudyLevel)
1121    grok.name('manage')
1122    grok.require('waeup.manageStudent')
1123    grok.template('studylevelmanagepage')
1124    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1125        'validation_date', 'validated_by', 'total_credits', 'gpa')
1126    pnav = 4
1127    taboneactions = [_('Save'),_('Cancel')]
1128    tabtwoactions = [_('Add course ticket'),
1129        _('Remove selected tickets'),_('Cancel')]
1130
1131    def update(self):
1132        if not self.context.__parent__.is_current:
1133            emit_lock_message(self)
1134            return
1135        super(StudyLevelManageFormPage, self).update()
1136        tabs.need()
1137        self.tab1 = self.tab2 = ''
1138        qs = self.request.get('QUERY_STRING', '')
1139        if not qs:
1140            qs = 'tab1'
1141        setattr(self, qs, 'active')
1142        warning.need()
1143        datatable.need()
1144        return
1145
1146    @property
1147    def translated_values(self):
1148        return translated_values(self)
1149
1150    @property
1151    def label(self):
1152        # Here we know that the cookie has been set
1153        lang = self.request.cookies.get('kofa.language')
1154        level_title = translate(self.context.level_title, 'waeup.kofa',
1155            target_language=lang)
1156        return _('Manage study level ${a}',
1157            mapping = {'a':level_title})
1158
1159    @action(_('Save'), style='primary')
1160    def save(self, **data):
1161        msave(self, **data)
1162        return
1163
1164    @action(_('Add course ticket'))
1165    def addCourseTicket(self, **data):
1166        self.redirect(self.url(self.context, '@@add'))
1167
1168    @jsaction(_('Remove selected tickets'))
1169    def delCourseTicket(self, **data):
1170        form = self.request.form
1171        if 'val_id' in form:
1172            child_id = form['val_id']
1173        else:
1174            self.flash(_('No ticket selected.'))
1175            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1176            return
1177        if not isinstance(child_id, list):
1178            child_id = [child_id]
1179        deleted = []
1180        for id in child_id:
1181            del self.context[id]
1182            deleted.append(id)
1183        if len(deleted):
1184            self.flash(_('Successfully removed: ${a}',
1185                mapping = {'a':', '.join(deleted)}))
1186            self.context.writeLogMessage(
1187                self,'removed: %s' % ', '.join(deleted))
1188        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1189        return
1190
1191class ValidateCoursesPage(UtilityView, grok.View):
1192    """ Validate course list by course adviser
1193    """
1194    grok.context(IStudentStudyLevel)
1195    grok.name('validate_courses')
1196    grok.require('waeup.validateStudent')
1197
1198    def update(self):
1199        if not self.context.__parent__.is_current:
1200            emit_lock_message(self)
1201            return
1202        if str(self.context.__parent__.current_level) != self.context.__name__:
1203            self.flash(_('This level does not correspond current level.'))
1204        elif self.context.student.state == REGISTERED:
1205            IWorkflowInfo(self.context.student).fireTransition(
1206                'validate_courses')
1207            self.flash(_('Course list has been validated.'))
1208        else:
1209            self.flash(_('Student is in the wrong state.'))
1210        self.redirect(self.url(self.context))
1211        return
1212
1213    def render(self):
1214        return
1215
1216class RejectCoursesPage(UtilityView, grok.View):
1217    """ Reject course list by course adviser
1218    """
1219    grok.context(IStudentStudyLevel)
1220    grok.name('reject_courses')
1221    grok.require('waeup.validateStudent')
1222
1223    def update(self):
1224        if not self.context.__parent__.is_current:
1225            emit_lock_message(self)
1226            return
1227        if str(self.context.__parent__.current_level) != self.context.__name__:
1228            self.flash(_('This level does not correspond current level.'))
1229            self.redirect(self.url(self.context))
1230            return
1231        elif self.context.student.state == VALIDATED:
1232            IWorkflowInfo(self.context.student).fireTransition('reset8')
1233            message = _('Course list request has been annulled.')
1234            self.flash(message)
1235        elif self.context.student.state == REGISTERED:
1236            IWorkflowInfo(self.context.student).fireTransition('reset7')
1237            message = _('Course list request has been rejected:')
1238            self.flash(message)
1239        else:
1240            self.flash(_('Student is in the wrong state.'))
1241            self.redirect(self.url(self.context))
1242            return
1243        args = {'subject':message}
1244        self.redirect(self.url(self.context.student) +
1245            '/contactstudent?%s' % urlencode(args))
1246        return
1247
1248    def render(self):
1249        return
1250
1251class CourseTicketAddFormPage(KofaAddFormPage):
1252    """Add a course ticket.
1253    """
1254    grok.context(IStudentStudyLevel)
1255    grok.name('add')
1256    grok.require('waeup.manageStudent')
1257    label = _('Add course ticket')
1258    form_fields = grok.AutoFields(ICourseTicketAdd)
1259    pnav = 4
1260
1261    def update(self):
1262        if not self.context.__parent__.is_current:
1263            emit_lock_message(self)
1264            return
1265        super(CourseTicketAddFormPage, self).update()
1266        return
1267
1268    @action(_('Add course ticket'))
1269    def addCourseTicket(self, **data):
1270        students_utils = getUtility(IStudentsUtils)
1271        ticket = createObject(u'waeup.CourseTicket')
1272        course = data['course']
1273        ticket.automatic = False
1274        ticket.carry_over = False
1275        max_credits = students_utils.maxCreditsExceeded(self.context, course)
1276        if max_credits:
1277            self.flash(_(
1278                'Total credits exceed ${a}.',
1279                mapping = {'a': max_credits}))
1280            return
1281        try:
1282            self.context.addCourseTicket(ticket, course)
1283        except KeyError:
1284            self.flash(_('The ticket exists.'))
1285            return
1286        self.flash(_('Successfully added ${a}.',
1287            mapping = {'a':ticket.code}))
1288        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1289        return
1290
1291    @action(_('Cancel'), validator=NullValidator)
1292    def cancel(self, **data):
1293        self.redirect(self.url(self.context))
1294
1295class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1296    """ Page to display course tickets
1297    """
1298    grok.context(ICourseTicket)
1299    grok.name('index')
1300    grok.require('waeup.viewStudent')
1301    form_fields = grok.AutoFields(ICourseTicket)
1302    grok.template('courseticketpage')
1303    pnav = 4
1304
1305    @property
1306    def label(self):
1307        return _('${a}: Course Ticket ${b}', mapping = {
1308            'a':self.context.student.display_fullname,
1309            'b':self.context.code})
1310
1311class CourseTicketManageFormPage(KofaEditFormPage):
1312    """ Page to manage course tickets
1313    """
1314    grok.context(ICourseTicket)
1315    grok.name('manage')
1316    grok.require('waeup.manageStudent')
1317    form_fields = grok.AutoFields(ICourseTicket)
1318    form_fields['title'].for_display = True
1319    form_fields['fcode'].for_display = True
1320    form_fields['dcode'].for_display = True
1321    form_fields['semester'].for_display = True
1322    form_fields['passmark'].for_display = True
1323    form_fields['credits'].for_display = True
1324    form_fields['mandatory'].for_display = False
1325    form_fields['automatic'].for_display = True
1326    form_fields['carry_over'].for_display = True
1327    pnav = 4
1328    grok.template('courseticketmanagepage')
1329
1330    @property
1331    def label(self):
1332        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1333
1334    @action('Save', style='primary')
1335    def save(self, **data):
1336        msave(self, **data)
1337        return
1338
1339class PaymentsManageFormPage(KofaEditFormPage):
1340    """ Page to manage the student payments
1341
1342    This manage form page is for both students and students officers.
1343    """
1344    grok.context(IStudentPaymentsContainer)
1345    grok.name('index')
1346    grok.require('waeup.payStudent')
1347    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1348    grok.template('paymentsmanagepage')
1349    pnav = 4
1350
1351    def unremovable(self, ticket):
1352        usertype = getattr(self.request.principal, 'user_type', None)
1353        if not usertype:
1354            return False
1355        return (self.request.principal.user_type == 'student' and ticket.r_code)
1356
1357    @property
1358    def label(self):
1359        return _('${a}: Payments',
1360            mapping = {'a':self.context.__parent__.display_fullname})
1361
1362    def update(self):
1363        super(PaymentsManageFormPage, self).update()
1364        datatable.need()
1365        warning.need()
1366        return
1367
1368    @jsaction(_('Remove selected tickets'))
1369    def delPaymentTicket(self, **data):
1370        form = self.request.form
1371        if 'val_id' in form:
1372            child_id = form['val_id']
1373        else:
1374            self.flash(_('No payment selected.'))
1375            self.redirect(self.url(self.context))
1376            return
1377        if not isinstance(child_id, list):
1378            child_id = [child_id]
1379        deleted = []
1380        for id in child_id:
1381            # Students are not allowed to remove used payment tickets
1382            if not self.unremovable(self.context[id]):
1383                del self.context[id]
1384                deleted.append(id)
1385        if len(deleted):
1386            self.flash(_('Successfully removed: ${a}',
1387                mapping = {'a': ', '.join(deleted)}))
1388            self.context.writeLogMessage(
1389                self,'removed: %s' % ', '.join(deleted))
1390        self.redirect(self.url(self.context))
1391        return
1392
1393    #@action(_('Add online payment ticket'))
1394    #def addPaymentTicket(self, **data):
1395    #    self.redirect(self.url(self.context, '@@addop'))
1396
1397class OnlinePaymentAddFormPage(KofaAddFormPage):
1398    """ Page to add an online payment ticket
1399    """
1400    grok.context(IStudentPaymentsContainer)
1401    grok.name('addop')
1402    grok.template('onlinepaymentaddform')
1403    grok.require('waeup.payStudent')
1404    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1405        'p_category')
1406    label = _('Add online payment')
1407    pnav = 4
1408
1409    @property
1410    def selectable_categories(self):
1411        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1412        return sorted(categories.items())
1413
1414    @action(_('Create ticket'), style='primary')
1415    def createTicket(self, **data):
1416        p_category = data['p_category']
1417        previous_session = data.get('p_session', None)
1418        previous_level = data.get('p_level', None)
1419        student = self.context.__parent__
1420        if p_category == 'bed_allocation' and student[
1421            'studycourse'].current_session != grok.getSite()[
1422            'hostels'].accommodation_session:
1423                self.flash(
1424                    _('Your current session does not match ' + \
1425                    'accommodation session.'))
1426                return
1427        if 'maintenance' in p_category:
1428            current_session = str(student['studycourse'].current_session)
1429            if not current_session in student['accommodation']:
1430                self.flash(_('You have not yet booked accommodation.'))
1431                return
1432        students_utils = getUtility(IStudentsUtils)
1433        error, payment = students_utils.setPaymentDetails(
1434            p_category, student, previous_session, previous_level)
1435        if error is not None:
1436            self.flash(error)
1437            return
1438        self.context[payment.p_id] = payment
1439        self.flash(_('Payment ticket created.'))
1440        self.redirect(self.url(self.context))
1441        return
1442
1443    @action(_('Cancel'), validator=NullValidator)
1444    def cancel(self, **data):
1445        self.redirect(self.url(self.context))
1446
1447class PreviousPaymentAddFormPage(OnlinePaymentAddFormPage):
1448    """ Page to add an online payment ticket for previous sessions
1449    """
1450    grok.context(IStudentPaymentsContainer)
1451    grok.name('addpp')
1452    grok.template('previouspaymentaddform')
1453    grok.require('waeup.payStudent')
1454    form_fields = grok.AutoFields(IStudentPreviousPayment).select(
1455        'p_category', 'p_session', 'p_level')
1456    label = _('Add previous session online payment')
1457    pnav = 4
1458
1459    def update(self):
1460        if self.context.student.before_payment:
1461            self.flash(_("No previous payment to be made."))
1462            self.redirect(self.url(self.context))
1463        super(PreviousPaymentAddFormPage, self).update()
1464        return
1465
1466class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1467    """ Page to view an online payment ticket
1468    """
1469    grok.context(IStudentOnlinePayment)
1470    grok.name('index')
1471    grok.require('waeup.viewStudent')
1472    form_fields = grok.AutoFields(IStudentOnlinePayment)
1473    form_fields[
1474        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1475    form_fields[
1476        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1477    pnav = 4
1478
1479    @property
1480    def label(self):
1481        return _('${a}: Online Payment Ticket ${b}', mapping = {
1482            'a':self.context.student.display_fullname,
1483            'b':self.context.p_id})
1484
1485class OnlinePaymentApprovePage(UtilityView, grok.View):
1486    """ Callback view
1487    """
1488    grok.context(IStudentOnlinePayment)
1489    grok.name('approve')
1490    grok.require('waeup.managePortal')
1491
1492    def update(self):
1493        success, msg, log = self.context.approveStudentPayment()
1494        if log is not None:
1495            # Add log message to students.log
1496            self.context.writeLogMessage(self,log)
1497            # Add log message to payments.log
1498            self.context.logger.info(
1499                '%s,%s,%s,%s,%s,,,,,,' % (
1500                self.context.student.student_id,
1501                self.context.p_id, self.context.p_category,
1502                self.context.amount_auth, self.context.r_code))
1503        self.flash(msg)
1504        return
1505
1506    def render(self):
1507        self.redirect(self.url(self.context, '@@index'))
1508        return
1509
1510class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1511    """ Approval view for students.
1512
1513    This view is used for browser tests only and
1514    must be neutralized in custom pages!
1515    """
1516
1517    grok.name('fake_approve')
1518    grok.require('waeup.payStudent')
1519
1520class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1521    """Deliver a PDF slip of the context.
1522    """
1523    grok.context(IStudentOnlinePayment)
1524    grok.name('payment_slip.pdf')
1525    grok.require('waeup.viewStudent')
1526    form_fields = grok.AutoFields(IStudentOnlinePayment)
1527    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1528    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1529    prefix = 'form'
1530    note = None
1531    omit_fields = (
1532        'password', 'suspended', 'phone',
1533        'adm_code', 'sex', 'suspended_comment')
1534
1535    @property
1536    def title(self):
1537        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1538        return translate(_('Payment Data'), 'waeup.kofa',
1539            target_language=portal_language)
1540
1541    @property
1542    def label(self):
1543        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1544        return translate(_('Online Payment Slip'),
1545            'waeup.kofa', target_language=portal_language) \
1546            + ' %s' % self.context.p_id
1547
1548    def render(self):
1549        #if self.context.p_state != 'paid':
1550        #    self.flash('Ticket not yet paid.')
1551        #    self.redirect(self.url(self.context))
1552        #    return
1553        studentview = StudentBasePDFFormPage(self.context.student,
1554            self.request, self.omit_fields)
1555        students_utils = getUtility(IStudentsUtils)
1556        return students_utils.renderPDF(self, 'payment_slip.pdf',
1557            self.context.student, studentview, note=self.note)
1558
1559
1560class AccommodationManageFormPage(KofaEditFormPage):
1561    """ Page to manage bed tickets.
1562
1563    This manage form page is for both students and students officers.
1564    """
1565    grok.context(IStudentAccommodation)
1566    grok.name('index')
1567    grok.require('waeup.handleAccommodation')
1568    form_fields = grok.AutoFields(IStudentAccommodation)
1569    grok.template('accommodationmanagepage')
1570    pnav = 4
1571    officers_only_actions = [_('Remove selected')]
1572
1573    @property
1574    def label(self):
1575        return _('${a}: Accommodation',
1576            mapping = {'a':self.context.__parent__.display_fullname})
1577
1578    def update(self):
1579        super(AccommodationManageFormPage, self).update()
1580        datatable.need()
1581        warning.need()
1582        return
1583
1584    @jsaction(_('Remove selected'))
1585    def delBedTickets(self, **data):
1586        if getattr(self.request.principal, 'user_type', None) == 'student':
1587            self.flash(_('You are not allowed to remove bed tickets.'))
1588            self.redirect(self.url(self.context))
1589            return
1590        form = self.request.form
1591        if 'val_id' in form:
1592            child_id = form['val_id']
1593        else:
1594            self.flash(_('No bed ticket selected.'))
1595            self.redirect(self.url(self.context))
1596            return
1597        if not isinstance(child_id, list):
1598            child_id = [child_id]
1599        deleted = []
1600        for id in child_id:
1601            del self.context[id]
1602            deleted.append(id)
1603        if len(deleted):
1604            self.flash(_('Successfully removed: ${a}',
1605                mapping = {'a':', '.join(deleted)}))
1606            self.context.writeLogMessage(
1607                self,'removed: % s' % ', '.join(deleted))
1608        self.redirect(self.url(self.context))
1609        return
1610
1611    @property
1612    def selected_actions(self):
1613        if getattr(self.request.principal, 'user_type', None) == 'student':
1614            return [action for action in self.actions
1615                    if not action.label in self.officers_only_actions]
1616        return self.actions
1617
1618class BedTicketAddPage(KofaPage):
1619    """ Page to add an online payment ticket
1620    """
1621    grok.context(IStudentAccommodation)
1622    grok.name('add')
1623    grok.require('waeup.handleAccommodation')
1624    grok.template('enterpin')
1625    ac_prefix = 'HOS'
1626    label = _('Add bed ticket')
1627    pnav = 4
1628    buttonname = _('Create bed ticket')
1629    notice = ''
1630    with_ac = True
1631
1632    def update(self, SUBMIT=None):
1633        student = self.context.student
1634        students_utils = getUtility(IStudentsUtils)
1635        acc_details  = students_utils.getAccommodationDetails(student)
1636        if acc_details.get('expired', False):
1637            startdate = acc_details.get('startdate')
1638            enddate = acc_details.get('enddate')
1639            if startdate and enddate:
1640                tz = getUtility(IKofaUtils).tzinfo
1641                startdate = to_timezone(
1642                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1643                enddate = to_timezone(
1644                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1645                self.flash(_("Outside booking period: ${a} - ${b}",
1646                    mapping = {'a': startdate, 'b': enddate}))
1647            else:
1648                self.flash(_("Outside booking period."))
1649            self.redirect(self.url(self.context))
1650            return
1651        if not acc_details:
1652            self.flash(_("Your data are incomplete."))
1653            self.redirect(self.url(self.context))
1654            return
1655        if not student.state in acc_details['allowed_states']:
1656            self.flash(_("You are in the wrong registration state."))
1657            self.redirect(self.url(self.context))
1658            return
1659        if student['studycourse'].current_session != acc_details[
1660            'booking_session']:
1661            self.flash(
1662                _('Your current session does not match accommodation session.'))
1663            self.redirect(self.url(self.context))
1664            return
1665        if str(acc_details['booking_session']) in self.context.keys():
1666            self.flash(
1667                _('You already booked a bed space in current ' \
1668                    + 'accommodation session.'))
1669            self.redirect(self.url(self.context))
1670            return
1671        if self.with_ac:
1672            self.ac_series = self.request.form.get('ac_series', None)
1673            self.ac_number = self.request.form.get('ac_number', None)
1674        if SUBMIT is None:
1675            return
1676        if self.with_ac:
1677            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1678            code = get_access_code(pin)
1679            if not code:
1680                self.flash(_('Activation code is invalid.'))
1681                return
1682        # Search and book bed
1683        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1684        entries = cat.searchResults(
1685            owner=(student.student_id,student.student_id))
1686        if len(entries):
1687            # If bed space has been manually allocated use this bed
1688            bed = [entry for entry in entries][0]
1689            # Safety belt for paranoids: Does this bed really exist on portal?
1690            # XXX: Can be remove if nobody complains.
1691            if bed.__parent__.__parent__ is None:
1692                self.flash(_('System error: Please contact the adminsitrator.'))
1693                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1694                return
1695        else:
1696            # else search for other available beds
1697            entries = cat.searchResults(
1698                bed_type=(acc_details['bt'],acc_details['bt']))
1699            available_beds = [
1700                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1701            if available_beds:
1702                students_utils = getUtility(IStudentsUtils)
1703                bed = students_utils.selectBed(available_beds)
1704                # Safety belt for paranoids: Does this bed really exist in portal?
1705                # XXX: Can be remove if nobody complains.
1706                if bed.__parent__.__parent__ is None:
1707                    self.flash(_('System error: Please contact the adminsitrator.'))
1708                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1709                    return
1710                bed.bookBed(student.student_id)
1711            else:
1712                self.flash(_('There is no free bed in your category ${a}.',
1713                    mapping = {'a':acc_details['bt']}))
1714                return
1715        if self.with_ac:
1716            # Mark pin as used (this also fires a pin related transition)
1717            if code.state == USED:
1718                self.flash(_('Activation code has already been used.'))
1719                return
1720            else:
1721                comment = _(u'invalidated')
1722                # Here we know that the ac is in state initialized so we do not
1723                # expect an exception, but the owner might be different
1724                if not invalidate_accesscode(
1725                    pin,comment,self.context.student.student_id):
1726                    self.flash(_('You are not the owner of this access code.'))
1727                    return
1728        # Create bed ticket
1729        bedticket = createObject(u'waeup.BedTicket')
1730        if self.with_ac:
1731            bedticket.booking_code = pin
1732        bedticket.booking_session = acc_details['booking_session']
1733        bedticket.bed_type = acc_details['bt']
1734        bedticket.bed = bed
1735        hall_title = bed.__parent__.hostel_name
1736        coordinates = bed.coordinates[1:]
1737        block, room_nr, bed_nr = coordinates
1738        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1739            'a':hall_title, 'b':block,
1740            'c':room_nr, 'd':bed_nr,
1741            'e':bed.bed_type})
1742        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1743        bedticket.bed_coordinates = translate(
1744            bc, 'waeup.kofa',target_language=portal_language)
1745        self.context.addBedTicket(bedticket)
1746        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1747        self.flash(_('Bed ticket created and bed booked: ${a}',
1748            mapping = {'a':bedticket.bed_coordinates}))
1749        self.redirect(self.url(self.context))
1750        return
1751
1752class BedTicketDisplayFormPage(KofaDisplayFormPage):
1753    """ Page to display bed tickets
1754    """
1755    grok.context(IBedTicket)
1756    grok.name('index')
1757    grok.require('waeup.handleAccommodation')
1758    form_fields = grok.AutoFields(IBedTicket)
1759    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1760    pnav = 4
1761
1762    @property
1763    def label(self):
1764        return _('Bed Ticket for Session ${a}',
1765            mapping = {'a':self.context.getSessionString()})
1766
1767class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1768    """Deliver a PDF slip of the context.
1769    """
1770    grok.context(IBedTicket)
1771    grok.name('bed_allocation_slip.pdf')
1772    grok.require('waeup.handleAccommodation')
1773    form_fields = grok.AutoFields(IBedTicket)
1774    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1775    prefix = 'form'
1776    omit_fields = (
1777        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
1778
1779    @property
1780    def title(self):
1781        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1782        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1783            target_language=portal_language)
1784
1785    @property
1786    def label(self):
1787        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1788        #return translate(_('Bed Allocation: '),
1789        #    'waeup.kofa', target_language=portal_language) \
1790        #    + ' %s' % self.context.bed_coordinates
1791        return translate(_('Bed Allocation Slip'),
1792            'waeup.kofa', target_language=portal_language) \
1793            + ' %s' % self.context.getSessionString()
1794
1795    def render(self):
1796        studentview = StudentBasePDFFormPage(self.context.student,
1797            self.request, self.omit_fields)
1798        students_utils = getUtility(IStudentsUtils)
1799        return students_utils.renderPDF(
1800            self, 'bed_allocation_slip.pdf',
1801            self.context.student, studentview)
1802
1803class BedTicketRelocationPage(UtilityView, grok.View):
1804    """ Callback view
1805    """
1806    grok.context(IBedTicket)
1807    grok.name('relocate')
1808    grok.require('waeup.manageHostels')
1809
1810    # Relocate student if student parameters have changed or the bed_type
1811    # of the bed has changed
1812    def update(self):
1813        student = self.context.student
1814        students_utils = getUtility(IStudentsUtils)
1815        acc_details  = students_utils.getAccommodationDetails(student)
1816        if self.context.bed != None and \
1817              'reserved' in self.context.bed.bed_type:
1818            self.flash(_("Students in reserved beds can't be relocated."))
1819            self.redirect(self.url(self.context))
1820            return
1821        if acc_details['bt'] == self.context.bed_type and \
1822                self.context.bed != None and \
1823                self.context.bed.bed_type == self.context.bed_type:
1824            self.flash(_("Student can't be relocated."))
1825            self.redirect(self.url(self.context))
1826            return
1827        # Search a bed
1828        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1829        entries = cat.searchResults(
1830            owner=(student.student_id,student.student_id))
1831        if len(entries) and self.context.bed == None:
1832            # If booking has been cancelled but other bed space has been
1833            # manually allocated after cancellation use this bed
1834            new_bed = [entry for entry in entries][0]
1835        else:
1836            # Search for other available beds
1837            entries = cat.searchResults(
1838                bed_type=(acc_details['bt'],acc_details['bt']))
1839            available_beds = [
1840                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1841            if available_beds:
1842                students_utils = getUtility(IStudentsUtils)
1843                new_bed = students_utils.selectBed(available_beds)
1844                new_bed.bookBed(student.student_id)
1845            else:
1846                self.flash(_('There is no free bed in your category ${a}.',
1847                    mapping = {'a':acc_details['bt']}))
1848                self.redirect(self.url(self.context))
1849                return
1850        # Release old bed if exists
1851        if self.context.bed != None:
1852            self.context.bed.owner = NOT_OCCUPIED
1853            notify(grok.ObjectModifiedEvent(self.context.bed))
1854        # Alocate new bed
1855        self.context.bed_type = acc_details['bt']
1856        self.context.bed = new_bed
1857        hall_title = new_bed.__parent__.hostel_name
1858        coordinates = new_bed.coordinates[1:]
1859        block, room_nr, bed_nr = coordinates
1860        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1861            'a':hall_title, 'b':block,
1862            'c':room_nr, 'd':bed_nr,
1863            'e':new_bed.bed_type})
1864        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1865        self.context.bed_coordinates = translate(
1866            bc, 'waeup.kofa',target_language=portal_language)
1867        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
1868        self.flash(_('Student relocated: ${a}',
1869            mapping = {'a':self.context.bed_coordinates}))
1870        self.redirect(self.url(self.context))
1871        return
1872
1873    def render(self):
1874        return
1875
1876class StudentHistoryPage(KofaPage):
1877    """ Page to display student clearance data
1878    """
1879    grok.context(IStudent)
1880    grok.name('history')
1881    grok.require('waeup.viewStudent')
1882    grok.template('studenthistory')
1883    pnav = 4
1884
1885    @property
1886    def label(self):
1887        return _('${a}: History', mapping = {'a':self.context.display_fullname})
1888
1889# Pages for students only
1890
1891class StudentBaseEditFormPage(KofaEditFormPage):
1892    """ View to edit student base data
1893    """
1894    grok.context(IStudent)
1895    grok.name('edit_base')
1896    grok.require('waeup.handleStudent')
1897    form_fields = grok.AutoFields(IStudentBase).select(
1898        'email', 'phone')
1899    label = _('Edit base data')
1900    pnav = 4
1901
1902    @action(_('Save'), style='primary')
1903    def save(self, **data):
1904        msave(self, **data)
1905        return
1906
1907class StudentChangePasswordPage(KofaEditFormPage):
1908    """ View to manage student base data
1909    """
1910    grok.context(IStudent)
1911    grok.name('change_password')
1912    grok.require('waeup.handleStudent')
1913    grok.template('change_password')
1914    label = _('Change password')
1915    pnav = 4
1916
1917    @action(_('Save'), style='primary')
1918    def save(self, **data):
1919        form = self.request.form
1920        password = form.get('change_password', None)
1921        password_ctl = form.get('change_password_repeat', None)
1922        if password:
1923            validator = getUtility(IPasswordValidator)
1924            errors = validator.validate_password(password, password_ctl)
1925            if not errors:
1926                IUserAccount(self.context).setPassword(password)
1927                self.context.writeLogMessage(self, 'saved: password')
1928                self.flash(_('Password changed.'))
1929            else:
1930                self.flash( ' '.join(errors))
1931        return
1932
1933class StudentFilesUploadPage(KofaPage):
1934    """ View to upload files by student
1935    """
1936    grok.context(IStudent)
1937    grok.name('change_portrait')
1938    grok.require('waeup.uploadStudentFile')
1939    grok.template('filesuploadpage')
1940    label = _('Upload portrait')
1941    pnav = 4
1942
1943    def update(self):
1944        if self.context.student.state != ADMITTED:
1945            emit_lock_message(self)
1946            return
1947        super(StudentFilesUploadPage, self).update()
1948        return
1949
1950class StartClearancePage(KofaPage):
1951    grok.context(IStudent)
1952    grok.name('start_clearance')
1953    grok.require('waeup.handleStudent')
1954    grok.template('enterpin')
1955    label = _('Start clearance')
1956    ac_prefix = 'CLR'
1957    notice = ''
1958    pnav = 4
1959    buttonname = _('Start clearance now')
1960
1961    @property
1962    def all_required_fields_filled(self):
1963        if self.context.email and self.context.phone:
1964            return True
1965        return False
1966
1967    @property
1968    def portrait_uploaded(self):
1969        store = getUtility(IExtFileStore)
1970        if store.getFileByContext(self.context, attr=u'passport.jpg'):
1971            return True
1972        return False
1973
1974    def update(self, SUBMIT=None):
1975        if not self.context.state == ADMITTED:
1976            self.flash(_("Wrong state"))
1977            self.redirect(self.url(self.context))
1978            return
1979        if not self.portrait_uploaded:
1980            self.flash(_("No portrait uploaded."))
1981            self.redirect(self.url(self.context, 'change_portrait'))
1982            return
1983        if not self.all_required_fields_filled:
1984            self.flash(_("Not all required fields filled."))
1985            self.redirect(self.url(self.context, 'edit_base'))
1986            return
1987        self.ac_series = self.request.form.get('ac_series', None)
1988        self.ac_number = self.request.form.get('ac_number', None)
1989
1990        if SUBMIT is None:
1991            return
1992        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1993        code = get_access_code(pin)
1994        if not code:
1995            self.flash(_('Activation code is invalid.'))
1996            return
1997        if code.state == USED:
1998            self.flash(_('Activation code has already been used.'))
1999            return
2000        # Mark pin as used (this also fires a pin related transition)
2001        # and fire transition start_clearance
2002        comment = _(u"invalidated")
2003        # Here we know that the ac is in state initialized so we do not
2004        # expect an exception, but the owner might be different
2005        if not invalidate_accesscode(pin, comment, self.context.student_id):
2006            self.flash(_('You are not the owner of this access code.'))
2007            return
2008        self.context.clr_code = pin
2009        IWorkflowInfo(self.context).fireTransition('start_clearance')
2010        self.flash(_('Clearance process has been started.'))
2011        self.redirect(self.url(self.context,'cedit'))
2012        return
2013
2014class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2015    """ View to edit student clearance data by student
2016    """
2017    grok.context(IStudent)
2018    grok.name('cedit')
2019    grok.require('waeup.handleStudent')
2020    label = _('Edit clearance data')
2021
2022    @property
2023    def form_fields(self):
2024        if self.context.is_postgrad:
2025            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2026                'clearance_locked', 'clr_code', 'officer_comment')
2027        else:
2028            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2029                'clearance_locked', 'clr_code', 'officer_comment')
2030        return form_fields
2031
2032    def update(self):
2033        if self.context.clearance_locked:
2034            emit_lock_message(self)
2035            return
2036        return super(StudentClearanceEditFormPage, self).update()
2037
2038    @action(_('Save'), style='primary')
2039    def save(self, **data):
2040        self.applyData(self.context, **data)
2041        self.flash(_('Clearance form has been saved.'))
2042        return
2043
2044    def dataNotComplete(self):
2045        """To be implemented in the customization package.
2046        """
2047        return False
2048
2049    @action(_('Save and request clearance'), style='primary')
2050    def requestClearance(self, **data):
2051        self.applyData(self.context, **data)
2052        if self.dataNotComplete():
2053            self.flash(self.dataNotComplete())
2054            return
2055        self.flash(_('Clearance form has been saved.'))
2056        if self.context.clr_code:
2057            self.redirect(self.url(self.context, 'request_clearance'))
2058        else:
2059            # We bypass the request_clearance page if student
2060            # has been imported in state 'clearance started' and
2061            # no clr_code was entered before.
2062            state = IWorkflowState(self.context).getState()
2063            if state != CLEARANCE:
2064                # This shouldn't happen, but the application officer
2065                # might have forgotten to lock the form after changing the state
2066                self.flash(_('This form cannot be submitted. Wrong state!'))
2067                return
2068            IWorkflowInfo(self.context).fireTransition('request_clearance')
2069            self.flash(_('Clearance has been requested.'))
2070            self.redirect(self.url(self.context))
2071        return
2072
2073class RequestClearancePage(KofaPage):
2074    grok.context(IStudent)
2075    grok.name('request_clearance')
2076    grok.require('waeup.handleStudent')
2077    grok.template('enterpin')
2078    label = _('Request clearance')
2079    notice = _('Enter the CLR access code used for starting clearance.')
2080    ac_prefix = 'CLR'
2081    pnav = 4
2082    buttonname = _('Request clearance now')
2083
2084    def update(self, SUBMIT=None):
2085        self.ac_series = self.request.form.get('ac_series', None)
2086        self.ac_number = self.request.form.get('ac_number', None)
2087        if SUBMIT is None:
2088            return
2089        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2090        if self.context.clr_code and self.context.clr_code != pin:
2091            self.flash(_("This isn't your CLR access code."))
2092            return
2093        state = IWorkflowState(self.context).getState()
2094        if state != CLEARANCE:
2095            # This shouldn't happen, but the application officer
2096            # might have forgotten to lock the form after changing the state
2097            self.flash(_('This form cannot be submitted. Wrong state!'))
2098            return
2099        IWorkflowInfo(self.context).fireTransition('request_clearance')
2100        self.flash(_('Clearance has been requested.'))
2101        self.redirect(self.url(self.context))
2102        return
2103
2104class StartSessionPage(KofaPage):
2105    grok.context(IStudentStudyCourse)
2106    grok.name('start_session')
2107    grok.require('waeup.handleStudent')
2108    grok.template('enterpin')
2109    label = _('Start session')
2110    ac_prefix = 'SFE'
2111    notice = ''
2112    pnav = 4
2113    buttonname = _('Start now')
2114
2115    def update(self, SUBMIT=None):
2116        if not self.context.is_current:
2117            emit_lock_message(self)
2118            return
2119        super(StartSessionPage, self).update()
2120        if not self.context.next_session_allowed:
2121            self.flash(_("You are not entitled to start session."))
2122            self.redirect(self.url(self.context))
2123            return
2124        self.ac_series = self.request.form.get('ac_series', None)
2125        self.ac_number = self.request.form.get('ac_number', None)
2126
2127        if SUBMIT is None:
2128            return
2129        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2130        code = get_access_code(pin)
2131        if not code:
2132            self.flash(_('Activation code is invalid.'))
2133            return
2134        # Mark pin as used (this also fires a pin related transition)
2135        if code.state == USED:
2136            self.flash(_('Activation code has already been used.'))
2137            return
2138        else:
2139            comment = _(u"invalidated")
2140            # Here we know that the ac is in state initialized so we do not
2141            # expect an error, but the owner might be different
2142            if not invalidate_accesscode(
2143                pin,comment,self.context.student.student_id):
2144                self.flash(_('You are not the owner of this access code.'))
2145                return
2146        try:
2147            if self.context.student.state == CLEARED:
2148                IWorkflowInfo(self.context.student).fireTransition(
2149                    'pay_first_school_fee')
2150            elif self.context.student.state == RETURNING:
2151                IWorkflowInfo(self.context.student).fireTransition(
2152                    'pay_school_fee')
2153            elif self.context.student.state == PAID:
2154                IWorkflowInfo(self.context.student).fireTransition(
2155                    'pay_pg_fee')
2156        except ConstraintNotSatisfied:
2157            self.flash(_('An error occurred, please contact the system administrator.'))
2158            return
2159        self.flash(_('Session started.'))
2160        self.redirect(self.url(self.context))
2161        return
2162
2163class AddStudyLevelFormPage(KofaEditFormPage):
2164    """ Page for students to add current study levels
2165    """
2166    grok.context(IStudentStudyCourse)
2167    grok.name('add')
2168    grok.require('waeup.handleStudent')
2169    grok.template('studyleveladdpage')
2170    form_fields = grok.AutoFields(IStudentStudyCourse)
2171    pnav = 4
2172
2173    @property
2174    def label(self):
2175        studylevelsource = StudyLevelSource().factory
2176        code = self.context.current_level
2177        title = studylevelsource.getTitle(self.context, code)
2178        return _('Add current level ${a}', mapping = {'a':title})
2179
2180    def update(self):
2181        if not self.context.is_current:
2182            emit_lock_message(self)
2183            return
2184        if self.context.student.state != PAID:
2185            emit_lock_message(self)
2186            return
2187        super(AddStudyLevelFormPage, self).update()
2188        return
2189
2190    @action(_('Create course list now'), style='primary')
2191    def addStudyLevel(self, **data):
2192        studylevel = createObject(u'waeup.StudentStudyLevel')
2193        studylevel.level = self.context.current_level
2194        studylevel.level_session = self.context.current_session
2195        try:
2196            self.context.addStudentStudyLevel(
2197                self.context.certificate,studylevel)
2198        except KeyError:
2199            self.flash(_('This level exists.'))
2200        except RequiredMissing:
2201            self.flash(_('Your data are incomplete'))
2202        self.redirect(self.url(self.context))
2203        return
2204
2205class StudyLevelEditFormPage(KofaEditFormPage):
2206    """ Page to edit the student study level data by students
2207    """
2208    grok.context(IStudentStudyLevel)
2209    grok.name('edit')
2210    grok.require('waeup.handleStudent')
2211    grok.template('studyleveleditpage')
2212    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2213        'level_session', 'level_verdict')
2214    pnav = 4
2215    max_credits = 50
2216
2217    def update(self):
2218        if not self.context.__parent__.is_current:
2219            emit_lock_message(self)
2220            return
2221        if self.context.student.state != PAID or \
2222            not self.context.is_current_level:
2223            emit_lock_message(self)
2224            return
2225        super(StudyLevelEditFormPage, self).update()
2226        datatable.need()
2227        warning.need()
2228        return
2229
2230    @property
2231    def label(self):
2232        # Here we know that the cookie has been set
2233        lang = self.request.cookies.get('kofa.language')
2234        level_title = translate(self.context.level_title, 'waeup.kofa',
2235            target_language=lang)
2236        return _('Edit course list of ${a}',
2237            mapping = {'a':level_title})
2238
2239    @property
2240    def translated_values(self):
2241        return translated_values(self)
2242
2243    @action(_('Add course ticket'))
2244    def addCourseTicket(self, **data):
2245        self.redirect(self.url(self.context, 'ctadd'))
2246
2247    def _delCourseTicket(self, **data):
2248        form = self.request.form
2249        if 'val_id' in form:
2250            child_id = form['val_id']
2251        else:
2252            self.flash(_('No ticket selected.'))
2253            self.redirect(self.url(self.context, '@@edit'))
2254            return
2255        if not isinstance(child_id, list):
2256            child_id = [child_id]
2257        deleted = []
2258        for id in child_id:
2259            # Students are not allowed to remove core tickets
2260            if id in self.context and \
2261                self.context[id].removable_by_student:
2262                del self.context[id]
2263                deleted.append(id)
2264        if len(deleted):
2265            self.flash(_('Successfully removed: ${a}',
2266                mapping = {'a':', '.join(deleted)}))
2267            self.context.writeLogMessage(
2268                self,'removed: %s' % ', '.join(deleted))
2269        self.redirect(self.url(self.context, u'@@edit'))
2270        return
2271
2272    @jsaction(_('Remove selected tickets'))
2273    def delCourseTicket(self, **data):
2274        self._delCourseTicket(**data)
2275        return
2276
2277    def _registerCourses(self, **data):
2278        if self.context.student.is_postgrad:
2279            self.flash(_(
2280                "You are a postgraduate student, "
2281                "your course list can't bee registered."))
2282            self.redirect(self.url(self.context))
2283            return
2284        if self.context.total_credits > self.max_credits:
2285            self.flash(_('Maximum credits of ${a} exceeded.',
2286                mapping = {'a':self.max_credits}))
2287            return
2288        IWorkflowInfo(self.context.student).fireTransition(
2289            'register_courses')
2290        self.flash(_('Course list has been registered.'))
2291        self.redirect(self.url(self.context))
2292        return
2293
2294    @action(_('Register course list'), style='primary')
2295    def registerCourses(self, **data):
2296        self._registerCourses(**data)
2297        return
2298
2299
2300class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2301    """Add a course ticket by student.
2302    """
2303    grok.name('ctadd')
2304    grok.require('waeup.handleStudent')
2305    form_fields = grok.AutoFields(ICourseTicketAdd)
2306
2307    def update(self):
2308        if self.context.student.state != PAID or \
2309            not self.context.is_current_level:
2310            emit_lock_message(self)
2311            return
2312        super(CourseTicketAddFormPage2, self).update()
2313        return
2314
2315    @action(_('Add course ticket'))
2316    def addCourseTicket(self, **data):
2317        students_utils = getUtility(IStudentsUtils)
2318        # Safety belt
2319        if self.context.student.state != PAID:
2320            return
2321        ticket = createObject(u'waeup.CourseTicket')
2322        course = data['course']
2323        ticket.automatic = False
2324        ticket.carry_over = False
2325        max_credits = students_utils.maxCreditsExceeded(self.context, course)
2326        if max_credits:
2327            self.flash(_(
2328                'Your total credits exceed ${a}.',
2329                mapping = {'a': max_credits}))
2330            return
2331        try:
2332            self.context.addCourseTicket(ticket, course)
2333        except KeyError:
2334            self.flash(_('The ticket exists.'))
2335            return
2336        self.flash(_('Successfully added ${a}.',
2337            mapping = {'a':ticket.code}))
2338        self.redirect(self.url(self.context, u'@@edit'))
2339        return
2340
2341
2342class SetPasswordPage(KofaPage):
2343    grok.context(IKofaObject)
2344    grok.name('setpassword')
2345    grok.require('waeup.Anonymous')
2346    grok.template('setpassword')
2347    label = _('Set password for first-time login')
2348    ac_prefix = 'PWD'
2349    pnav = 0
2350    set_button = _('Set')
2351
2352    def update(self, SUBMIT=None):
2353        self.reg_number = self.request.form.get('reg_number', None)
2354        self.ac_series = self.request.form.get('ac_series', None)
2355        self.ac_number = self.request.form.get('ac_number', None)
2356
2357        if SUBMIT is None:
2358            return
2359        hitlist = search(query=self.reg_number,
2360            searchtype='reg_number', view=self)
2361        if not hitlist:
2362            self.flash(_('No student found.'))
2363            return
2364        if len(hitlist) != 1:   # Cannot happen but anyway
2365            self.flash(_('More than one student found.'))
2366            return
2367        student = hitlist[0].context
2368        self.student_id = student.student_id
2369        student_pw = student.password
2370        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2371        code = get_access_code(pin)
2372        if not code:
2373            self.flash(_('Access code is invalid.'))
2374            return
2375        if student_pw and pin == student.adm_code:
2376            self.flash(_(
2377                'Password has already been set. Your Student Id is ${a}',
2378                mapping = {'a':self.student_id}))
2379            return
2380        elif student_pw:
2381            self.flash(
2382                _('Password has already been set. You are using the ' +
2383                'wrong Access Code.'))
2384            return
2385        # Mark pin as used (this also fires a pin related transition)
2386        # and set student password
2387        if code.state == USED:
2388            self.flash(_('Access code has already been used.'))
2389            return
2390        else:
2391            comment = _(u"invalidated")
2392            # Here we know that the ac is in state initialized so we do not
2393            # expect an exception
2394            invalidate_accesscode(pin,comment)
2395            IUserAccount(student).setPassword(self.ac_number)
2396            student.adm_code = pin
2397        self.flash(_('Password has been set. Your Student Id is ${a}',
2398            mapping = {'a':self.student_id}))
2399        return
2400
2401class StudentRequestPasswordPage(KofaAddFormPage):
2402    """Captcha'd registration page for applicants.
2403    """
2404    grok.name('requestpw')
2405    grok.require('waeup.Anonymous')
2406    grok.template('requestpw')
2407    form_fields = grok.AutoFields(IStudentRequestPW).select(
2408        'firstname','number','email')
2409    label = _('Request password for first-time login')
2410
2411    def update(self):
2412        # Handle captcha
2413        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2414        self.captcha_result = self.captcha.verify(self.request)
2415        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2416        return
2417
2418    def _redirect(self, email, password, student_id):
2419        # Forward only email to landing page in base package.
2420        self.redirect(self.url(self.context, 'requestpw_complete',
2421            data = dict(email=email)))
2422        return
2423
2424    def _pw_used(self):
2425        # XXX: False if password has not been used. We need an extra
2426        #      attribute which remembers if student logged in.
2427        return True
2428
2429    @action(_('Send login credentials to email address'), style='primary')
2430    def get_credentials(self, **data):
2431        if not self.captcha_result.is_valid:
2432            # Captcha will display error messages automatically.
2433            # No need to flash something.
2434            return
2435        number = data.get('number','')
2436        firstname = data.get('firstname','')
2437        cat = getUtility(ICatalog, name='students_catalog')
2438        results = list(
2439            cat.searchResults(reg_number=(number, number)))
2440        if not results:
2441            results = list(
2442                cat.searchResults(matric_number=(number, number)))
2443        if results:
2444            student = results[0]
2445            if getattr(student,'firstname',None) is None:
2446                self.flash(_('An error occurred.'))
2447                return
2448            elif student.firstname.lower() != firstname.lower():
2449                # Don't tell the truth here. Anonymous must not
2450                # know that a record was found and only the firstname
2451                # verification failed.
2452                self.flash(_('No student record found.'))
2453                return
2454            elif student.password is not None and self._pw_used:
2455                self.flash(_('Your password has already been set and used. '
2456                             'Please proceed to the login page.'))
2457                return
2458            # Store email address but nothing else.
2459            student.email = data['email']
2460            notify(grok.ObjectModifiedEvent(student))
2461        else:
2462            # No record found, this is the truth.
2463            self.flash(_('No student record found.'))
2464            return
2465
2466        kofa_utils = getUtility(IKofaUtils)
2467        password = kofa_utils.genPassword()
2468        mandate = PasswordMandate()
2469        mandate.params['password'] = password
2470        mandate.params['user'] = student
2471        site = grok.getSite()
2472        site['mandates'].addMandate(mandate)
2473        # Send email with credentials
2474        args = {'mandate_id':mandate.mandate_id}
2475        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2476        url_info = u'Confirmation link: %s' % mandate_url
2477        msg = _('You have successfully requested a password for the')
2478        if kofa_utils.sendCredentials(IUserAccount(student),
2479            password, url_info, msg):
2480            email_sent = student.email
2481        else:
2482            email_sent = None
2483        self._redirect(email=email_sent, password=password,
2484            student_id=student.student_id)
2485        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2486        self.context.logger.info(
2487            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2488        return
2489
2490class StudentRequestPasswordEmailSent(KofaPage):
2491    """Landing page after successful password request.
2492
2493    """
2494    grok.name('requestpw_complete')
2495    grok.require('waeup.Public')
2496    grok.template('requestpwmailsent')
2497    label = _('Your password request was successful.')
2498
2499    def update(self, email=None, student_id=None, password=None):
2500        self.email = email
2501        self.password = password
2502        self.student_id = student_id
2503        return
2504
2505class DepartmentExportJobContainerOverview(KofaPage):
2506    """Page that filters and lists students.
2507    """
2508    grok.context(VirtualDepartmentExportJobContainer)
2509    grok.require('waeup.showStudents')
2510    grok.name('index.html')
2511    grok.template('exportjobsindex')
2512    label = _('Department Exports')
2513    pnav = 1
2514
2515    def doll_up(self):
2516        job_entries = self.context.get_running_export_jobs(
2517            self.request.principal.id)
2518        job_manager = getUtility(IJobManager)
2519        entries = []
2520        for job_id, exporter_name, user_id in job_entries:
2521            job = job_manager.get(job_id)
2522            descr = 'Export (%r, %r)' % (job.args[1:], job.kwargs)
2523            status = job.status
2524            start_time = '%s' % job.begin_after
2525            download_url = self.url(self.context, 'download',
2526                                    data=dict(job_id=job_id))
2527            new_entry = dict(
2528                job=job_id, descr=descr, status=status, start_time=start_time,
2529                download_url=download_url)
2530            entries.append(new_entry)
2531        self.entries = entries
2532        pass
2533
2534    def update(self, CREATE=None, DISCARD=None, job_id=None):
2535        if CREATE:
2536            self.redirect(self.url('jobconfig'))
2537            return
2538        if DISCARD and job_id:
2539            entry = self.context.entry_from_job_id(job_id)
2540            self.context.delete_export_entry(entry)
2541            self.flash('Discarded export %s' % job_id)
2542        self.doll_up()
2543        return
2544
2545class DepartmentExportJobContainerJobConfig(KofaPage):
2546    """Page that configures a students export job.
2547    """
2548    grok.context(VirtualDepartmentExportJobContainer)
2549    grok.require('waeup.showStudents')
2550    grok.name('jobconfig')
2551    grok.template('exportjobsjobconfig')
2552    label = _('Create New Export Job')
2553    pnav = 1
2554
2555    def _set_session_values(self):
2556        vocab_terms = academic_sessions_vocab.by_value.values()
2557        self.sessions = sorted(
2558            [(x.title, x.token) for x in vocab_terms], reverse=True)
2559        self.sessions += [('All Sessions', 'all')]
2560        return
2561
2562    def _set_level_values(self):
2563        vocab_terms = course_levels.by_value.values()
2564        self.levels = sorted(
2565            [(x.title, x.token) for x in vocab_terms])
2566        self.levels += [('All Levels', 'all')]
2567        return
2568
2569    def _set_mode_values(self):
2570        utils = getUtility(IKofaUtils)
2571        self.modes = [(value, key) for key, value in
2572                      utils.STUDY_MODES_DICT.items()]
2573        self.modes +=[('All Modes', 'all')]
2574        return
2575
2576    def _set_exporter_values(self):
2577        # We provide all student exporters, nothing else, yet.
2578        exporters = []
2579        for name in EXPORTER_NAMES:
2580            util = getUtility(ICSVExporter, name=name)
2581            exporters.append((util.title, name),)
2582        self.exporters = exporters
2583
2584    def update(self, START=None, session=None, level=None, mode=None,
2585               exporter=None):
2586        self._set_session_values()
2587        self._set_level_values()
2588        self._set_mode_values()
2589        self._set_exporter_values()
2590        if START is None:
2591            return
2592        if session == 'all':
2593            session=None
2594        if level == 'all':
2595            level = None
2596        if mode == 'all':
2597            mode = None
2598        code = self.context.__parent__.code
2599        self.context.start_export_job(exporter, self.request.principal.id,
2600                                      current_session=session,
2601                                      current_level=level,
2602                                      current_mode=mode,
2603                                      depcode=code)
2604        self.flash('Export started for students from %s '
2605                   '(session=%s, level=%s)' % (
2606                       code, session, level))
2607        self.redirect(self.url(self.context))
2608        return
2609
2610class DepartmentExportJobContainerDownload(grok.View):
2611    """Page that configures a students export job.
2612    """
2613    grok.context(VirtualDepartmentExportJobContainer)
2614    grok.require('waeup.showStudents')
2615    grok.name('download')
2616
2617    def update(self, job_id=None):
2618        self.job_id=job_id
2619        return
2620
2621    def render(self):
2622        job = getUtility(IJobManager).get(self.job_id)
2623        self.response.setHeader(
2624            'Content-Type', 'text/csv; charset=UTF-8')
2625        self.response.setHeader(
2626            'Content-Disposition:', 'attachment; filename="%s' % (
2627                'students.csv',))
2628        return open(job.result, 'rb')
2629        return
Note: See TracBrowser for help on using the repository browser.