source: main/waeup.kofa/branches/uli-diazo-themed/src/waeup/kofa/students/browser.py @ 11016

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

Remove resources and theming completely.

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