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

Last change on this file since 10267 was 10266, checked in by Henrik Bettermann, 12 years ago

Define correct dictionaries. level_dict must include probation levels.

Add buttons for navigating to transcripts.

Add property attribute 'transcript_enabled' which eases defining conditions in custom packages.

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