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

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

Add view for displaying cumulative transcript data.

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