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

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

Add level column.

  • Property svn:keywords set to Id
File size: 109.0 KB
Line 
1## $Id: browser.py 10179 2013-05-15 07:34:06Z 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.course_levels = course_levels
1061        self.studymode_dict = getUtility(IKofaUtils).STUDY_MODES_DICT
1062        return
1063
1064    @property
1065    def label(self):
1066        # Here we know that the cookie has been set
1067        lang = self.request.cookies.get('kofa.language')
1068        return _('${a}: Transcript Data', mapping = {
1069            'a':self.context.student.display_fullname})
1070
1071class StudentTransferFormPage(KofaAddFormPage):
1072    """Page to transfer the student.
1073    """
1074    grok.context(IStudent)
1075    grok.name('transfer')
1076    grok.require('waeup.manageStudent')
1077    label = _('Transfer student')
1078    form_fields = grok.AutoFields(IStudentStudyCourseTransfer).omit(
1079        'entry_mode', 'entry_session')
1080    pnav = 4
1081
1082    def update(self):
1083        super(StudentTransferFormPage, self).update()
1084        warning.need()
1085        return
1086
1087    @jsaction(_('Transfer'))
1088    def transferStudent(self, **data):
1089        error = self.context.transfer(**data)
1090        if error == -1:
1091            self.flash(_('Current level does not match certificate levels.'))
1092        elif error == -2:
1093            self.flash(_('Former study course record incomplete.'))
1094        elif error == -3:
1095            self.flash(_('Maximum number of transfers exceeded.'))
1096        else:
1097            self.flash(_('Successfully transferred.'))
1098        return
1099
1100class RevertTransferFormPage(KofaEditFormPage):
1101    """View that reverts the previous transfer.
1102    """
1103    grok.context(IStudent)
1104    grok.name('revert_transfer')
1105    grok.require('waeup.manageStudent')
1106    grok.template('reverttransfer')
1107    label = _('Revert previous transfer')
1108
1109    def update(self):
1110        warning.need()
1111        if not self.context.has_key('studycourse_1'):
1112            self.flash(_('No previous transfer.'))
1113            self.redirect(self.url(self.context))
1114            return
1115        return
1116
1117    @jsaction(_('Revert now'))
1118    def transferStudent(self, **data):
1119        self.context.revert_transfer()
1120        self.flash(_('Previous transfer reverted.'))
1121        self.redirect(self.url(self.context, 'studycourse'))
1122        return
1123
1124class StudyLevelDisplayFormPage(KofaDisplayFormPage):
1125    """ Page to display student study levels
1126    """
1127    grok.context(IStudentStudyLevel)
1128    grok.name('index')
1129    grok.require('waeup.viewStudent')
1130    form_fields = grok.AutoFields(IStudentStudyLevel)
1131    form_fields[
1132        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1133    grok.template('studylevelpage')
1134    pnav = 4
1135
1136    def update(self):
1137        super(StudyLevelDisplayFormPage, self).update()
1138        datatable.need()
1139        return
1140
1141    @property
1142    def translated_values(self):
1143        return translated_values(self)
1144
1145    @property
1146    def label(self):
1147        # Here we know that the cookie has been set
1148        lang = self.request.cookies.get('kofa.language')
1149        level_title = translate(self.context.level_title, 'waeup.kofa',
1150            target_language=lang)
1151        return _('${a}: Study Level ${b}', mapping = {
1152            'a':self.context.student.display_fullname,
1153            'b':level_title})
1154
1155class ExportPDFCourseRegistrationSlipPage(UtilityView, grok.View):
1156    """Deliver a PDF slip of the context.
1157    """
1158    grok.context(IStudentStudyLevel)
1159    grok.name('course_registration_slip.pdf')
1160    grok.require('waeup.viewStudent')
1161    form_fields = grok.AutoFields(IStudentStudyLevel)
1162    form_fields[
1163        'validation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1164    prefix = 'form'
1165    omit_fields = (
1166        'password', 'suspended', 'phone',
1167        'adm_code', 'sex', 'suspended_comment')
1168
1169    @property
1170    def title(self):
1171        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1172        return translate(_('Level Data'), 'waeup.kofa',
1173            target_language=portal_language)
1174
1175    @property
1176    def content_title_1(self):
1177        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1178        return translate(_('1st Semester Courses'), 'waeup.kofa',
1179            target_language=portal_language)
1180
1181    @property
1182    def content_title_2(self):
1183        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1184        return translate(_('2nd Semester Courses'), 'waeup.kofa',
1185            target_language=portal_language)
1186
1187    @property
1188    def content_title_3(self):
1189        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1190        return translate(_('Level Courses'), 'waeup.kofa',
1191            target_language=portal_language)
1192
1193    @property
1194    def label(self):
1195        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1196        lang = self.request.cookies.get('kofa.language', portal_language)
1197        level_title = translate(self.context.level_title, 'waeup.kofa',
1198            target_language=lang)
1199        return translate(_('Course Registration Slip'),
1200            'waeup.kofa', target_language=portal_language) \
1201            + ' %s' % level_title
1202
1203    def render(self):
1204        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1205        Sem = translate(_('Sem.'), 'waeup.kofa', target_language=portal_language)
1206        Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
1207        Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
1208        Dept = translate(_('Dept.'), 'waeup.kofa', target_language=portal_language)
1209        Faculty = translate(_('Faculty'), 'waeup.kofa', target_language=portal_language)
1210        Cred = translate(_('Cred.'), 'waeup.kofa', target_language=portal_language)
1211        #Mand = translate(_('Requ.'), 'waeup.kofa', target_language=portal_language)
1212        Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
1213        Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
1214        studentview = StudentBasePDFFormPage(self.context.student,
1215            self.request, self.omit_fields)
1216        students_utils = getUtility(IStudentsUtils)
1217        tabledata_1 = sorted(
1218            [value for value in self.context.values() if value.semester == 1],
1219            key=lambda value: str(value.semester) + value.code)
1220        tabledata_2 = sorted(
1221            [value for value in self.context.values() if value.semester == 2],
1222            key=lambda value: str(value.semester) + value.code)
1223        tabledata_3 = sorted(
1224            [value for value in self.context.values() if value.semester == 3],
1225            key=lambda value: str(value.semester) + value.code)
1226        tableheader = [(Code,'code', 2.5),
1227                         (Title,'title', 5),
1228                         (Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
1229                         (Cred, 'credits', 1.5),
1230                         #(Mand, 'mandatory', 1.5),
1231                         (Score, 'score', 1.5),
1232                         (Grade, 'grade', 1.5),
1233                         #('Auto', 'automatic', 1.5)
1234                         ]
1235        return students_utils.renderPDF(
1236            self, 'course_registration_slip.pdf',
1237            self.context.student, studentview,
1238            tableheader_1=tableheader,
1239            tabledata_1=tabledata_1,
1240            tableheader_2=tableheader,
1241            tabledata_2=tabledata_2,
1242            tableheader_3=tableheader,
1243            tabledata_3=tabledata_3
1244            )
1245
1246class StudyLevelManageFormPage(KofaEditFormPage):
1247    """ Page to edit the student study level data
1248    """
1249    grok.context(IStudentStudyLevel)
1250    grok.name('manage')
1251    grok.require('waeup.manageStudent')
1252    grok.template('studylevelmanagepage')
1253    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
1254        'validation_date', 'validated_by', 'total_credits', 'gpa')
1255    pnav = 4
1256    taboneactions = [_('Save'),_('Cancel')]
1257    tabtwoactions = [_('Add course ticket'),
1258        _('Remove selected tickets'),_('Cancel')]
1259
1260    def update(self, ADD=None, course=None):
1261        if not self.context.__parent__.is_current:
1262            emit_lock_message(self)
1263            return
1264        super(StudyLevelManageFormPage, self).update()
1265        tabs.need()
1266        self.tab1 = self.tab2 = ''
1267        qs = self.request.get('QUERY_STRING', '')
1268        if not qs:
1269            qs = 'tab1'
1270        setattr(self, qs, 'active')
1271        warning.need()
1272        datatable.need()
1273        if ADD is not None:
1274            if not course:
1275                self.flash(_('No valid course code entered.'))
1276                self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1277                return
1278            cat = queryUtility(ICatalog, name='courses_catalog')
1279            result = cat.searchResults(code=(course, course))
1280            if len(result) != 1:
1281                self.flash(_('Course not found.'))
1282            else:
1283                course = list(result)[0]
1284                addCourseTicket(self, course)
1285            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1286        return
1287
1288    @property
1289    def translated_values(self):
1290        return translated_values(self)
1291
1292    @property
1293    def label(self):
1294        # Here we know that the cookie has been set
1295        lang = self.request.cookies.get('kofa.language')
1296        level_title = translate(self.context.level_title, 'waeup.kofa',
1297            target_language=lang)
1298        return _('Manage study level ${a}',
1299            mapping = {'a':level_title})
1300
1301    @action(_('Save'), style='primary')
1302    def save(self, **data):
1303        msave(self, **data)
1304        return
1305
1306    @jsaction(_('Remove selected tickets'))
1307    def delCourseTicket(self, **data):
1308        form = self.request.form
1309        if 'val_id' in form:
1310            child_id = form['val_id']
1311        else:
1312            self.flash(_('No ticket selected.'))
1313            self.redirect(self.url(self.context, '@@manage')+'?tab2')
1314            return
1315        if not isinstance(child_id, list):
1316            child_id = [child_id]
1317        deleted = []
1318        for id in child_id:
1319            del self.context[id]
1320            deleted.append(id)
1321        if len(deleted):
1322            self.flash(_('Successfully removed: ${a}',
1323                mapping = {'a':', '.join(deleted)}))
1324            self.context.writeLogMessage(
1325                self,'removed: %s at %s' %
1326                (', '.join(deleted), self.context.level))
1327        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1328        return
1329
1330class ValidateCoursesPage(UtilityView, grok.View):
1331    """ Validate course list by course adviser
1332    """
1333    grok.context(IStudentStudyLevel)
1334    grok.name('validate_courses')
1335    grok.require('waeup.validateStudent')
1336
1337    def update(self):
1338        if not self.context.__parent__.is_current:
1339            emit_lock_message(self)
1340            return
1341        if str(self.context.__parent__.current_level) != self.context.__name__:
1342            self.flash(_('This level does not correspond current level.'))
1343        elif self.context.student.state == REGISTERED:
1344            IWorkflowInfo(self.context.student).fireTransition(
1345                'validate_courses')
1346            self.flash(_('Course list has been validated.'))
1347        else:
1348            self.flash(_('Student is in the wrong state.'))
1349        self.redirect(self.url(self.context))
1350        return
1351
1352    def render(self):
1353        return
1354
1355class RejectCoursesPage(UtilityView, grok.View):
1356    """ Reject course list by course adviser
1357    """
1358    grok.context(IStudentStudyLevel)
1359    grok.name('reject_courses')
1360    grok.require('waeup.validateStudent')
1361
1362    def update(self):
1363        if not self.context.__parent__.is_current:
1364            emit_lock_message(self)
1365            return
1366        if str(self.context.__parent__.current_level) != self.context.__name__:
1367            self.flash(_('This level does not correspond current level.'))
1368            self.redirect(self.url(self.context))
1369            return
1370        elif self.context.student.state == VALIDATED:
1371            IWorkflowInfo(self.context.student).fireTransition('reset8')
1372            message = _('Course list request has been annulled.')
1373            self.flash(message)
1374        elif self.context.student.state == REGISTERED:
1375            IWorkflowInfo(self.context.student).fireTransition('reset7')
1376            message = _('Course list request has been rejected:')
1377            self.flash(message)
1378        else:
1379            self.flash(_('Student is in the wrong state.'))
1380            self.redirect(self.url(self.context))
1381            return
1382        args = {'subject':message}
1383        self.redirect(self.url(self.context.student) +
1384            '/contactstudent?%s' % urlencode(args))
1385        return
1386
1387    def render(self):
1388        return
1389
1390class CourseTicketAddFormPage(KofaAddFormPage):
1391    """Add a course ticket.
1392    """
1393    grok.context(IStudentStudyLevel)
1394    grok.name('add')
1395    grok.require('waeup.manageStudent')
1396    label = _('Add course ticket')
1397    form_fields = grok.AutoFields(ICourseTicketAdd)
1398    pnav = 4
1399
1400    def update(self):
1401        if not self.context.__parent__.is_current:
1402            emit_lock_message(self)
1403            return
1404        super(CourseTicketAddFormPage, self).update()
1405        return
1406
1407    @action(_('Add course ticket'))
1408    def addCourseTicket(self, **data):
1409        course = data['course']
1410        success = addCourseTicket(self, course)
1411        if success:
1412            self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1413        return
1414
1415    @action(_('Cancel'), validator=NullValidator)
1416    def cancel(self, **data):
1417        self.redirect(self.url(self.context))
1418
1419class CourseTicketDisplayFormPage(KofaDisplayFormPage):
1420    """ Page to display course tickets
1421    """
1422    grok.context(ICourseTicket)
1423    grok.name('index')
1424    grok.require('waeup.viewStudent')
1425    form_fields = grok.AutoFields(ICourseTicket)
1426    grok.template('courseticketpage')
1427    pnav = 4
1428
1429    @property
1430    def label(self):
1431        return _('${a}: Course Ticket ${b}', mapping = {
1432            'a':self.context.student.display_fullname,
1433            'b':self.context.code})
1434
1435class CourseTicketManageFormPage(KofaEditFormPage):
1436    """ Page to manage course tickets
1437    """
1438    grok.context(ICourseTicket)
1439    grok.name('manage')
1440    grok.require('waeup.manageStudent')
1441    form_fields = grok.AutoFields(ICourseTicket)
1442    form_fields['title'].for_display = True
1443    form_fields['fcode'].for_display = True
1444    form_fields['dcode'].for_display = True
1445    form_fields['semester'].for_display = True
1446    form_fields['passmark'].for_display = True
1447    form_fields['credits'].for_display = True
1448    form_fields['mandatory'].for_display = False
1449    form_fields['automatic'].for_display = True
1450    form_fields['carry_over'].for_display = True
1451    pnav = 4
1452    grok.template('courseticketmanagepage')
1453
1454    @property
1455    def label(self):
1456        return _('Manage course ticket ${a}', mapping = {'a':self.context.code})
1457
1458    @action('Save', style='primary')
1459    def save(self, **data):
1460        msave(self, **data)
1461        return
1462
1463class PaymentsManageFormPage(KofaEditFormPage):
1464    """ Page to manage the student payments
1465
1466    This manage form page is for both students and students officers.
1467    """
1468    grok.context(IStudentPaymentsContainer)
1469    grok.name('index')
1470    grok.require('waeup.viewStudent')
1471    form_fields = grok.AutoFields(IStudentPaymentsContainer)
1472    grok.template('paymentsmanagepage')
1473    pnav = 4
1474
1475    @property
1476    def manage_payments_allowed(self):
1477        return checkPermission('waeup.payStudent', self.context)
1478
1479    def unremovable(self, ticket):
1480        usertype = getattr(self.request.principal, 'user_type', None)
1481        if not usertype:
1482            return False
1483        if not self.manage_payments_allowed:
1484            return True
1485        return (self.request.principal.user_type == 'student' and ticket.r_code)
1486
1487    @property
1488    def label(self):
1489        return _('${a}: Payments',
1490            mapping = {'a':self.context.__parent__.display_fullname})
1491
1492    def update(self):
1493        super(PaymentsManageFormPage, self).update()
1494        datatable.need()
1495        warning.need()
1496        return
1497
1498    @jsaction(_('Remove selected tickets'))
1499    def delPaymentTicket(self, **data):
1500        form = self.request.form
1501        if 'val_id' in form:
1502            child_id = form['val_id']
1503        else:
1504            self.flash(_('No payment selected.'))
1505            self.redirect(self.url(self.context))
1506            return
1507        if not isinstance(child_id, list):
1508            child_id = [child_id]
1509        deleted = []
1510        for id in child_id:
1511            # Students are not allowed to remove used payment tickets
1512            ticket = self.context.get(id, None)
1513            if ticket is not None and not self.unremovable(ticket):
1514                del self.context[id]
1515                deleted.append(id)
1516        if len(deleted):
1517            self.flash(_('Successfully removed: ${a}',
1518                mapping = {'a': ', '.join(deleted)}))
1519            self.context.writeLogMessage(
1520                self,'removed: %s' % ', '.join(deleted))
1521        self.redirect(self.url(self.context))
1522        return
1523
1524    #@action(_('Add online payment ticket'))
1525    #def addPaymentTicket(self, **data):
1526    #    self.redirect(self.url(self.context, '@@addop'))
1527
1528class OnlinePaymentAddFormPage(KofaAddFormPage):
1529    """ Page to add an online payment ticket
1530    """
1531    grok.context(IStudentPaymentsContainer)
1532    grok.name('addop')
1533    grok.template('onlinepaymentaddform')
1534    grok.require('waeup.payStudent')
1535    form_fields = grok.AutoFields(IStudentOnlinePayment).select(
1536        'p_category')
1537    label = _('Add online payment')
1538    pnav = 4
1539
1540    @property
1541    def selectable_categories(self):
1542        categories = getUtility(IKofaUtils).SELECTABLE_PAYMENT_CATEGORIES
1543        return sorted(categories.items())
1544
1545    @action(_('Create ticket'), style='primary')
1546    def createTicket(self, **data):
1547        p_category = data['p_category']
1548        previous_session = data.get('p_session', None)
1549        previous_level = data.get('p_level', None)
1550        student = self.context.__parent__
1551        if p_category == 'bed_allocation' and student[
1552            'studycourse'].current_session != grok.getSite()[
1553            'hostels'].accommodation_session:
1554                self.flash(
1555                    _('Your current session does not match ' + \
1556                    'accommodation session.'))
1557                return
1558        if 'maintenance' in p_category:
1559            current_session = str(student['studycourse'].current_session)
1560            if not current_session in student['accommodation']:
1561                self.flash(_('You have not yet booked accommodation.'))
1562                return
1563        students_utils = getUtility(IStudentsUtils)
1564        error, payment = students_utils.setPaymentDetails(
1565            p_category, student, previous_session, previous_level)
1566        if error is not None:
1567            self.flash(error)
1568            return
1569        self.context[payment.p_id] = payment
1570        self.flash(_('Payment ticket created.'))
1571        self.redirect(self.url(self.context))
1572        return
1573
1574    @action(_('Cancel'), validator=NullValidator)
1575    def cancel(self, **data):
1576        self.redirect(self.url(self.context))
1577
1578class PreviousPaymentAddFormPage(KofaAddFormPage):
1579    """ Page to add an online payment ticket for previous sessions
1580    """
1581    grok.context(IStudentPaymentsContainer)
1582    grok.name('addpp')
1583    grok.require('waeup.payStudent')
1584    form_fields = grok.AutoFields(IStudentPreviousPayment)
1585    label = _('Add previous session online payment')
1586    pnav = 4
1587
1588    def update(self):
1589        if self.context.student.before_payment:
1590            self.flash(_("No previous payment to be made."))
1591            self.redirect(self.url(self.context))
1592        super(PreviousPaymentAddFormPage, self).update()
1593        return
1594
1595    @action(_('Create ticket'), style='primary')
1596    def createTicket(self, **data):
1597        p_category = data['p_category']
1598        previous_session = data.get('p_session', None)
1599        previous_level = data.get('p_level', None)
1600        student = self.context.__parent__
1601        students_utils = getUtility(IStudentsUtils)
1602        error, payment = students_utils.setPaymentDetails(
1603            p_category, student, previous_session, previous_level)
1604        if error is not None:
1605            self.flash(error)
1606            return
1607        self.context[payment.p_id] = payment
1608        self.flash(_('Payment ticket created.'))
1609        self.redirect(self.url(self.context))
1610        return
1611
1612    @action(_('Cancel'), validator=NullValidator)
1613    def cancel(self, **data):
1614        self.redirect(self.url(self.context))
1615
1616class BalancePaymentAddFormPage(KofaAddFormPage):
1617    """ Page to add an online payment ticket for balance sessions
1618    """
1619    grok.context(IStudentPaymentsContainer)
1620    grok.name('addbp')
1621    grok.require('waeup.manageStudent')
1622    form_fields = grok.AutoFields(IStudentBalancePayment)
1623    label = _('Add balance')
1624    pnav = 4
1625
1626    @action(_('Create ticket'), style='primary')
1627    def createTicket(self, **data):
1628        p_category = data['p_category']
1629        balance_session = data.get('balance_session', None)
1630        balance_level = data.get('balance_level', None)
1631        balance_amount = data.get('balance_amount', None)
1632        student = self.context.__parent__
1633        students_utils = getUtility(IStudentsUtils)
1634        error, payment = students_utils.setBalanceDetails(
1635            p_category, student, balance_session,
1636            balance_level, balance_amount)
1637        if error is not None:
1638            self.flash(error)
1639            return
1640        self.context[payment.p_id] = payment
1641        self.flash(_('Payment ticket created.'))
1642        self.redirect(self.url(self.context))
1643        return
1644
1645    @action(_('Cancel'), validator=NullValidator)
1646    def cancel(self, **data):
1647        self.redirect(self.url(self.context))
1648
1649class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
1650    """ Page to view an online payment ticket
1651    """
1652    grok.context(IStudentOnlinePayment)
1653    grok.name('index')
1654    grok.require('waeup.viewStudent')
1655    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1656    form_fields[
1657        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1658    form_fields[
1659        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1660    pnav = 4
1661
1662    @property
1663    def label(self):
1664        return _('${a}: Online Payment Ticket ${b}', mapping = {
1665            'a':self.context.student.display_fullname,
1666            'b':self.context.p_id})
1667
1668class OnlinePaymentApprovePage(UtilityView, grok.View):
1669    """ Callback view
1670    """
1671    grok.context(IStudentOnlinePayment)
1672    grok.name('approve')
1673    grok.require('waeup.managePortal')
1674
1675    def update(self):
1676        success, msg, log = self.context.approveStudentPayment()
1677        if log is not None:
1678            # Add log message to students.log
1679            self.context.writeLogMessage(self,log)
1680            # Add log message to payments.log
1681            self.context.logger.info(
1682                '%s,%s,%s,%s,%s,,,,,,' % (
1683                self.context.student.student_id,
1684                self.context.p_id, self.context.p_category,
1685                self.context.amount_auth, self.context.r_code))
1686        self.flash(msg)
1687        return
1688
1689    def render(self):
1690        self.redirect(self.url(self.context, '@@index'))
1691        return
1692
1693class OnlinePaymentFakeApprovePage(OnlinePaymentApprovePage):
1694    """ Approval view for students.
1695
1696    This view is used for browser tests only and
1697    must be neutralized in custom pages!
1698    """
1699
1700    grok.name('fake_approve')
1701    grok.require('waeup.payStudent')
1702
1703class ExportPDFPaymentSlipPage(UtilityView, grok.View):
1704    """Deliver a PDF slip of the context.
1705    """
1706    grok.context(IStudentOnlinePayment)
1707    grok.name('payment_slip.pdf')
1708    grok.require('waeup.viewStudent')
1709    form_fields = grok.AutoFields(IStudentOnlinePayment).omit('p_item')
1710    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1711    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1712    prefix = 'form'
1713    note = None
1714    omit_fields = (
1715        'password', 'suspended', 'phone',
1716        'adm_code', 'sex', 'suspended_comment')
1717
1718    @property
1719    def title(self):
1720        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1721        return translate(_('Payment Data'), 'waeup.kofa',
1722            target_language=portal_language)
1723
1724    @property
1725    def label(self):
1726        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1727        return translate(_('Online Payment Slip'),
1728            'waeup.kofa', target_language=portal_language) \
1729            + ' %s' % self.context.p_id
1730
1731    def render(self):
1732        #if self.context.p_state != 'paid':
1733        #    self.flash('Ticket not yet paid.')
1734        #    self.redirect(self.url(self.context))
1735        #    return
1736        studentview = StudentBasePDFFormPage(self.context.student,
1737            self.request, self.omit_fields)
1738        students_utils = getUtility(IStudentsUtils)
1739        return students_utils.renderPDF(self, 'payment_slip.pdf',
1740            self.context.student, studentview, note=self.note)
1741
1742
1743class AccommodationManageFormPage(KofaEditFormPage):
1744    """ Page to manage bed tickets.
1745
1746    This manage form page is for both students and students officers.
1747    """
1748    grok.context(IStudentAccommodation)
1749    grok.name('index')
1750    grok.require('waeup.handleAccommodation')
1751    form_fields = grok.AutoFields(IStudentAccommodation)
1752    grok.template('accommodationmanagepage')
1753    pnav = 4
1754    officers_only_actions = [_('Remove selected')]
1755
1756    @property
1757    def label(self):
1758        return _('${a}: Accommodation',
1759            mapping = {'a':self.context.__parent__.display_fullname})
1760
1761    def update(self):
1762        super(AccommodationManageFormPage, self).update()
1763        datatable.need()
1764        warning.need()
1765        return
1766
1767    @jsaction(_('Remove selected'))
1768    def delBedTickets(self, **data):
1769        if getattr(self.request.principal, 'user_type', None) == 'student':
1770            self.flash(_('You are not allowed to remove bed tickets.'))
1771            self.redirect(self.url(self.context))
1772            return
1773        form = self.request.form
1774        if 'val_id' in form:
1775            child_id = form['val_id']
1776        else:
1777            self.flash(_('No bed ticket selected.'))
1778            self.redirect(self.url(self.context))
1779            return
1780        if not isinstance(child_id, list):
1781            child_id = [child_id]
1782        deleted = []
1783        for id in child_id:
1784            del self.context[id]
1785            deleted.append(id)
1786        if len(deleted):
1787            self.flash(_('Successfully removed: ${a}',
1788                mapping = {'a':', '.join(deleted)}))
1789            self.context.writeLogMessage(
1790                self,'removed: % s' % ', '.join(deleted))
1791        self.redirect(self.url(self.context))
1792        return
1793
1794    @property
1795    def selected_actions(self):
1796        if getattr(self.request.principal, 'user_type', None) == 'student':
1797            return [action for action in self.actions
1798                    if not action.label in self.officers_only_actions]
1799        return self.actions
1800
1801class BedTicketAddPage(KofaPage):
1802    """ Page to add an online payment ticket
1803    """
1804    grok.context(IStudentAccommodation)
1805    grok.name('add')
1806    grok.require('waeup.handleAccommodation')
1807    grok.template('enterpin')
1808    ac_prefix = 'HOS'
1809    label = _('Add bed ticket')
1810    pnav = 4
1811    buttonname = _('Create bed ticket')
1812    notice = ''
1813    with_ac = True
1814
1815    def update(self, SUBMIT=None):
1816        student = self.context.student
1817        students_utils = getUtility(IStudentsUtils)
1818        acc_details  = students_utils.getAccommodationDetails(student)
1819        if acc_details.get('expired', False):
1820            startdate = acc_details.get('startdate')
1821            enddate = acc_details.get('enddate')
1822            if startdate and enddate:
1823                tz = getUtility(IKofaUtils).tzinfo
1824                startdate = to_timezone(
1825                    startdate, tz).strftime("%d/%m/%Y %H:%M:%S")
1826                enddate = to_timezone(
1827                    enddate, tz).strftime("%d/%m/%Y %H:%M:%S")
1828                self.flash(_("Outside booking period: ${a} - ${b}",
1829                    mapping = {'a': startdate, 'b': enddate}))
1830            else:
1831                self.flash(_("Outside booking period."))
1832            self.redirect(self.url(self.context))
1833            return
1834        if not acc_details:
1835            self.flash(_("Your data are incomplete."))
1836            self.redirect(self.url(self.context))
1837            return
1838        if not student.state in acc_details['allowed_states']:
1839            self.flash(_("You are in the wrong registration state."))
1840            self.redirect(self.url(self.context))
1841            return
1842        if student['studycourse'].current_session != acc_details[
1843            'booking_session']:
1844            self.flash(
1845                _('Your current session does not match accommodation session.'))
1846            self.redirect(self.url(self.context))
1847            return
1848        if str(acc_details['booking_session']) in self.context.keys():
1849            self.flash(
1850                _('You already booked a bed space in current ' \
1851                    + 'accommodation session.'))
1852            self.redirect(self.url(self.context))
1853            return
1854        if self.with_ac:
1855            self.ac_series = self.request.form.get('ac_series', None)
1856            self.ac_number = self.request.form.get('ac_number', None)
1857        if SUBMIT is None:
1858            return
1859        if self.with_ac:
1860            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
1861            code = get_access_code(pin)
1862            if not code:
1863                self.flash(_('Activation code is invalid.'))
1864                return
1865        # Search and book bed
1866        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
1867        entries = cat.searchResults(
1868            owner=(student.student_id,student.student_id))
1869        if len(entries):
1870            # If bed space has been manually allocated use this bed
1871            bed = [entry for entry in entries][0]
1872            # Safety belt for paranoids: Does this bed really exist on portal?
1873            # XXX: Can be remove if nobody complains.
1874            if bed.__parent__.__parent__ is None:
1875                self.flash(_('System error: Please contact the adminsitrator.'))
1876                self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1877                return
1878        else:
1879            # else search for other available beds
1880            entries = cat.searchResults(
1881                bed_type=(acc_details['bt'],acc_details['bt']))
1882            available_beds = [
1883                entry for entry in entries if entry.owner == NOT_OCCUPIED]
1884            if available_beds:
1885                students_utils = getUtility(IStudentsUtils)
1886                bed = students_utils.selectBed(available_beds)
1887                # Safety belt for paranoids: Does this bed really exist in portal?
1888                # XXX: Can be remove if nobody complains.
1889                if bed.__parent__.__parent__ is None:
1890                    self.flash(_('System error: Please contact the adminsitrator.'))
1891                    self.context.writeLogMessage(self, 'fatal error: %s' % bed.bed_id)
1892                    return
1893                bed.bookBed(student.student_id)
1894            else:
1895                self.flash(_('There is no free bed in your category ${a}.',
1896                    mapping = {'a':acc_details['bt']}))
1897                return
1898        if self.with_ac:
1899            # Mark pin as used (this also fires a pin related transition)
1900            if code.state == USED:
1901                self.flash(_('Activation code has already been used.'))
1902                return
1903            else:
1904                comment = _(u'invalidated')
1905                # Here we know that the ac is in state initialized so we do not
1906                # expect an exception, but the owner might be different
1907                if not invalidate_accesscode(
1908                    pin,comment,self.context.student.student_id):
1909                    self.flash(_('You are not the owner of this access code.'))
1910                    return
1911        # Create bed ticket
1912        bedticket = createObject(u'waeup.BedTicket')
1913        if self.with_ac:
1914            bedticket.booking_code = pin
1915        bedticket.booking_session = acc_details['booking_session']
1916        bedticket.bed_type = acc_details['bt']
1917        bedticket.bed = bed
1918        hall_title = bed.__parent__.hostel_name
1919        coordinates = bed.coordinates[1:]
1920        block, room_nr, bed_nr = coordinates
1921        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
1922            'a':hall_title, 'b':block,
1923            'c':room_nr, 'd':bed_nr,
1924            'e':bed.bed_type})
1925        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1926        bedticket.bed_coordinates = translate(
1927            bc, 'waeup.kofa',target_language=portal_language)
1928        self.context.addBedTicket(bedticket)
1929        self.context.writeLogMessage(self, 'booked: %s' % bed.bed_id)
1930        self.flash(_('Bed ticket created and bed booked: ${a}',
1931            mapping = {'a':bedticket.display_coordinates}))
1932        self.redirect(self.url(self.context))
1933        return
1934
1935class BedTicketDisplayFormPage(KofaDisplayFormPage):
1936    """ Page to display bed tickets
1937    """
1938    grok.context(IBedTicket)
1939    grok.name('index')
1940    grok.require('waeup.handleAccommodation')
1941    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
1942    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1943    pnav = 4
1944
1945    @property
1946    def label(self):
1947        return _('Bed Ticket for Session ${a}',
1948            mapping = {'a':self.context.getSessionString()})
1949
1950class ExportPDFBedTicketSlipPage(UtilityView, grok.View):
1951    """Deliver a PDF slip of the context.
1952    """
1953    grok.context(IBedTicket)
1954    grok.name('bed_allocation_slip.pdf')
1955    grok.require('waeup.handleAccommodation')
1956    form_fields = grok.AutoFields(IBedTicket).omit('bed_coordinates')
1957    form_fields['booking_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
1958    prefix = 'form'
1959    omit_fields = (
1960        'password', 'suspended', 'phone', 'adm_code', 'suspended_comment')
1961
1962    @property
1963    def title(self):
1964        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1965        return translate(_('Bed Allocation Data'), 'waeup.kofa',
1966            target_language=portal_language)
1967
1968    @property
1969    def label(self):
1970        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
1971        #return translate(_('Bed Allocation: '),
1972        #    'waeup.kofa', target_language=portal_language) \
1973        #    + ' %s' % self.context.bed_coordinates
1974        return translate(_('Bed Allocation Slip'),
1975            'waeup.kofa', target_language=portal_language) \
1976            + ' %s' % self.context.getSessionString()
1977
1978    def render(self):
1979        studentview = StudentBasePDFFormPage(self.context.student,
1980            self.request, self.omit_fields)
1981        students_utils = getUtility(IStudentsUtils)
1982        return students_utils.renderPDF(
1983            self, 'bed_allocation_slip.pdf',
1984            self.context.student, studentview)
1985
1986class BedTicketRelocationPage(UtilityView, grok.View):
1987    """ Callback view
1988    """
1989    grok.context(IBedTicket)
1990    grok.name('relocate')
1991    grok.require('waeup.manageHostels')
1992
1993    # Relocate student if student parameters have changed or the bed_type
1994    # of the bed has changed
1995    def update(self):
1996        student = self.context.student
1997        students_utils = getUtility(IStudentsUtils)
1998        acc_details  = students_utils.getAccommodationDetails(student)
1999        if self.context.bed != None and \
2000              'reserved' in self.context.bed.bed_type:
2001            self.flash(_("Students in reserved beds can't be relocated."))
2002            self.redirect(self.url(self.context))
2003            return
2004        if acc_details['bt'] == self.context.bed_type and \
2005                self.context.bed != None and \
2006                self.context.bed.bed_type == self.context.bed_type:
2007            self.flash(_("Student can't be relocated."))
2008            self.redirect(self.url(self.context))
2009            return
2010        # Search a bed
2011        cat = queryUtility(ICatalog, name='beds_catalog', default=None)
2012        entries = cat.searchResults(
2013            owner=(student.student_id,student.student_id))
2014        if len(entries) and self.context.bed == None:
2015            # If booking has been cancelled but other bed space has been
2016            # manually allocated after cancellation use this bed
2017            new_bed = [entry for entry in entries][0]
2018        else:
2019            # Search for other available beds
2020            entries = cat.searchResults(
2021                bed_type=(acc_details['bt'],acc_details['bt']))
2022            available_beds = [
2023                entry for entry in entries if entry.owner == NOT_OCCUPIED]
2024            if available_beds:
2025                students_utils = getUtility(IStudentsUtils)
2026                new_bed = students_utils.selectBed(available_beds)
2027                new_bed.bookBed(student.student_id)
2028            else:
2029                self.flash(_('There is no free bed in your category ${a}.',
2030                    mapping = {'a':acc_details['bt']}))
2031                self.redirect(self.url(self.context))
2032                return
2033        # Release old bed if exists
2034        if self.context.bed != None:
2035            self.context.bed.owner = NOT_OCCUPIED
2036            notify(grok.ObjectModifiedEvent(self.context.bed))
2037        # Alocate new bed
2038        self.context.bed_type = acc_details['bt']
2039        self.context.bed = new_bed
2040        hall_title = new_bed.__parent__.hostel_name
2041        coordinates = new_bed.coordinates[1:]
2042        block, room_nr, bed_nr = coordinates
2043        bc = _('${a}, Block ${b}, Room ${c}, Bed ${d} (${e})', mapping = {
2044            'a':hall_title, 'b':block,
2045            'c':room_nr, 'd':bed_nr,
2046            'e':new_bed.bed_type})
2047        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
2048        self.context.bed_coordinates = translate(
2049            bc, 'waeup.kofa',target_language=portal_language)
2050        self.context.writeLogMessage(self, 'relocated: %s' % new_bed.bed_id)
2051        self.flash(_('Student relocated: ${a}',
2052            mapping = {'a':self.context.display_coordinates}))
2053        self.redirect(self.url(self.context))
2054        return
2055
2056    def render(self):
2057        return
2058
2059class StudentHistoryPage(KofaPage):
2060    """ Page to display student clearance data
2061    """
2062    grok.context(IStudent)
2063    grok.name('history')
2064    grok.require('waeup.viewStudent')
2065    grok.template('studenthistory')
2066    pnav = 4
2067
2068    @property
2069    def label(self):
2070        return _('${a}: History', mapping = {'a':self.context.display_fullname})
2071
2072# Pages for students only
2073
2074class StudentBaseEditFormPage(KofaEditFormPage):
2075    """ View to edit student base data
2076    """
2077    grok.context(IStudent)
2078    grok.name('edit_base')
2079    grok.require('waeup.handleStudent')
2080    form_fields = grok.AutoFields(IStudentBase).select(
2081        'email', 'phone')
2082    label = _('Edit base data')
2083    pnav = 4
2084
2085    @action(_('Save'), style='primary')
2086    def save(self, **data):
2087        msave(self, **data)
2088        return
2089
2090class StudentChangePasswordPage(KofaEditFormPage):
2091    """ View to manage student base data
2092    """
2093    grok.context(IStudent)
2094    grok.name('change_password')
2095    grok.require('waeup.handleStudent')
2096    grok.template('change_password')
2097    label = _('Change password')
2098    pnav = 4
2099
2100    @action(_('Save'), style='primary')
2101    def save(self, **data):
2102        form = self.request.form
2103        password = form.get('change_password', None)
2104        password_ctl = form.get('change_password_repeat', None)
2105        if password:
2106            validator = getUtility(IPasswordValidator)
2107            errors = validator.validate_password(password, password_ctl)
2108            if not errors:
2109                IUserAccount(self.context).setPassword(password)
2110                self.context.writeLogMessage(self, 'saved: password')
2111                self.flash(_('Password changed.'))
2112            else:
2113                self.flash( ' '.join(errors))
2114        return
2115
2116class StudentFilesUploadPage(KofaPage):
2117    """ View to upload files by student
2118    """
2119    grok.context(IStudent)
2120    grok.name('change_portrait')
2121    grok.require('waeup.uploadStudentFile')
2122    grok.template('filesuploadpage')
2123    label = _('Upload portrait')
2124    pnav = 4
2125
2126    def update(self):
2127        if self.context.student.state != ADMITTED:
2128            emit_lock_message(self)
2129            return
2130        super(StudentFilesUploadPage, self).update()
2131        return
2132
2133class StartClearancePage(KofaPage):
2134    grok.context(IStudent)
2135    grok.name('start_clearance')
2136    grok.require('waeup.handleStudent')
2137    grok.template('enterpin')
2138    label = _('Start clearance')
2139    ac_prefix = 'CLR'
2140    notice = ''
2141    pnav = 4
2142    buttonname = _('Start clearance now')
2143    with_ac = True
2144
2145    @property
2146    def all_required_fields_filled(self):
2147        if self.context.email and self.context.phone:
2148            return True
2149        return False
2150
2151    @property
2152    def portrait_uploaded(self):
2153        store = getUtility(IExtFileStore)
2154        if store.getFileByContext(self.context, attr=u'passport.jpg'):
2155            return True
2156        return False
2157
2158    def update(self, SUBMIT=None):
2159        if not self.context.state == ADMITTED:
2160            self.flash(_("Wrong state"))
2161            self.redirect(self.url(self.context))
2162            return
2163        if not self.portrait_uploaded:
2164            self.flash(_("No portrait uploaded."))
2165            self.redirect(self.url(self.context, 'change_portrait'))
2166            return
2167        if not self.all_required_fields_filled:
2168            self.flash(_("Not all required fields filled."))
2169            self.redirect(self.url(self.context, 'edit_base'))
2170            return
2171        if self.with_ac:
2172            self.ac_series = self.request.form.get('ac_series', None)
2173            self.ac_number = self.request.form.get('ac_number', None)
2174        if SUBMIT is None:
2175            return
2176        if self.with_ac:
2177            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2178            code = get_access_code(pin)
2179            if not code:
2180                self.flash(_('Activation code is invalid.'))
2181                return
2182            if code.state == USED:
2183                self.flash(_('Activation code has already been used.'))
2184                return
2185            # Mark pin as used (this also fires a pin related transition)
2186            # and fire transition start_clearance
2187            comment = _(u"invalidated")
2188            # Here we know that the ac is in state initialized so we do not
2189            # expect an exception, but the owner might be different
2190            if not invalidate_accesscode(pin, comment, self.context.student_id):
2191                self.flash(_('You are not the owner of this access code.'))
2192                return
2193            self.context.clr_code = pin
2194        IWorkflowInfo(self.context).fireTransition('start_clearance')
2195        self.flash(_('Clearance process has been started.'))
2196        self.redirect(self.url(self.context,'cedit'))
2197        return
2198
2199class StudentClearanceEditFormPage(StudentClearanceManageFormPage):
2200    """ View to edit student clearance data by student
2201    """
2202    grok.context(IStudent)
2203    grok.name('cedit')
2204    grok.require('waeup.handleStudent')
2205    label = _('Edit clearance data')
2206
2207    @property
2208    def form_fields(self):
2209        if self.context.is_postgrad:
2210            form_fields = grok.AutoFields(IPGStudentClearance).omit(
2211                'clearance_locked', 'clr_code', 'officer_comment')
2212        else:
2213            form_fields = grok.AutoFields(IUGStudentClearance).omit(
2214                'clearance_locked', 'clr_code', 'officer_comment')
2215        return form_fields
2216
2217    def update(self):
2218        if self.context.clearance_locked:
2219            emit_lock_message(self)
2220            return
2221        return super(StudentClearanceEditFormPage, self).update()
2222
2223    @action(_('Save'), style='primary')
2224    def save(self, **data):
2225        self.applyData(self.context, **data)
2226        self.flash(_('Clearance form has been saved.'))
2227        return
2228
2229    def dataNotComplete(self):
2230        """To be implemented in the customization package.
2231        """
2232        return False
2233
2234    @action(_('Save and request clearance'), style='primary')
2235    def requestClearance(self, **data):
2236        self.applyData(self.context, **data)
2237        if self.dataNotComplete():
2238            self.flash(self.dataNotComplete())
2239            return
2240        self.flash(_('Clearance form has been saved.'))
2241        if self.context.clr_code:
2242            self.redirect(self.url(self.context, 'request_clearance'))
2243        else:
2244            # We bypass the request_clearance page if student
2245            # has been imported in state 'clearance started' and
2246            # no clr_code was entered before.
2247            state = IWorkflowState(self.context).getState()
2248            if state != CLEARANCE:
2249                # This shouldn't happen, but the application officer
2250                # might have forgotten to lock the form after changing the state
2251                self.flash(_('This form cannot be submitted. Wrong state!'))
2252                return
2253            IWorkflowInfo(self.context).fireTransition('request_clearance')
2254            self.flash(_('Clearance has been requested.'))
2255            self.redirect(self.url(self.context))
2256        return
2257
2258class RequestClearancePage(KofaPage):
2259    grok.context(IStudent)
2260    grok.name('request_clearance')
2261    grok.require('waeup.handleStudent')
2262    grok.template('enterpin')
2263    label = _('Request clearance')
2264    notice = _('Enter the CLR access code used for starting clearance.')
2265    ac_prefix = 'CLR'
2266    pnav = 4
2267    buttonname = _('Request clearance now')
2268    with_ac = True
2269
2270    def update(self, SUBMIT=None):
2271        if self.with_ac:
2272            self.ac_series = self.request.form.get('ac_series', None)
2273            self.ac_number = self.request.form.get('ac_number', None)
2274        if SUBMIT is None:
2275            return
2276        if self.with_ac:
2277            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2278            if self.context.clr_code and self.context.clr_code != pin:
2279                self.flash(_("This isn't your CLR access code."))
2280                return
2281        state = IWorkflowState(self.context).getState()
2282        if state != CLEARANCE:
2283            # This shouldn't happen, but the application officer
2284            # might have forgotten to lock the form after changing the state
2285            self.flash(_('This form cannot be submitted. Wrong state!'))
2286            return
2287        IWorkflowInfo(self.context).fireTransition('request_clearance')
2288        self.flash(_('Clearance has been requested.'))
2289        self.redirect(self.url(self.context))
2290        return
2291
2292class StartSessionPage(KofaPage):
2293    grok.context(IStudentStudyCourse)
2294    grok.name('start_session')
2295    grok.require('waeup.handleStudent')
2296    grok.template('enterpin')
2297    label = _('Start session')
2298    ac_prefix = 'SFE'
2299    notice = ''
2300    pnav = 4
2301    buttonname = _('Start now')
2302    with_ac = True
2303
2304    def update(self, SUBMIT=None):
2305        if not self.context.is_current:
2306            emit_lock_message(self)
2307            return
2308        super(StartSessionPage, self).update()
2309        if not self.context.next_session_allowed:
2310            self.flash(_("You are not entitled to start session."))
2311            self.redirect(self.url(self.context))
2312            return
2313        if self.with_ac:
2314            self.ac_series = self.request.form.get('ac_series', None)
2315            self.ac_number = self.request.form.get('ac_number', None)
2316        if SUBMIT is None:
2317            return
2318        if self.with_ac:
2319            pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2320            code = get_access_code(pin)
2321            if not code:
2322                self.flash(_('Activation code is invalid.'))
2323                return
2324            # Mark pin as used (this also fires a pin related transition)
2325            if code.state == USED:
2326                self.flash(_('Activation code has already been used.'))
2327                return
2328            else:
2329                comment = _(u"invalidated")
2330                # Here we know that the ac is in state initialized so we do not
2331                # expect an error, but the owner might be different
2332                if not invalidate_accesscode(
2333                    pin,comment,self.context.student.student_id):
2334                    self.flash(_('You are not the owner of this access code.'))
2335                    return
2336        try:
2337            if self.context.student.state == CLEARED:
2338                IWorkflowInfo(self.context.student).fireTransition(
2339                    'pay_first_school_fee')
2340            elif self.context.student.state == RETURNING:
2341                IWorkflowInfo(self.context.student).fireTransition(
2342                    'pay_school_fee')
2343            elif self.context.student.state == PAID:
2344                IWorkflowInfo(self.context.student).fireTransition(
2345                    'pay_pg_fee')
2346        except ConstraintNotSatisfied:
2347            self.flash(_('An error occurred, please contact the system administrator.'))
2348            return
2349        self.flash(_('Session started.'))
2350        self.redirect(self.url(self.context))
2351        return
2352
2353class AddStudyLevelFormPage(KofaEditFormPage):
2354    """ Page for students to add current study levels
2355    """
2356    grok.context(IStudentStudyCourse)
2357    grok.name('add')
2358    grok.require('waeup.handleStudent')
2359    grok.template('studyleveladdpage')
2360    form_fields = grok.AutoFields(IStudentStudyCourse)
2361    pnav = 4
2362
2363    @property
2364    def label(self):
2365        studylevelsource = StudyLevelSource().factory
2366        code = self.context.current_level
2367        title = studylevelsource.getTitle(self.context, code)
2368        return _('Add current level ${a}', mapping = {'a':title})
2369
2370    def update(self):
2371        if not self.context.is_current:
2372            emit_lock_message(self)
2373            return
2374        if self.context.student.state != PAID:
2375            emit_lock_message(self)
2376            return
2377        super(AddStudyLevelFormPage, self).update()
2378        return
2379
2380    @action(_('Create course list now'), style='primary')
2381    def addStudyLevel(self, **data):
2382        studylevel = createObject(u'waeup.StudentStudyLevel')
2383        studylevel.level = self.context.current_level
2384        studylevel.level_session = self.context.current_session
2385        try:
2386            self.context.addStudentStudyLevel(
2387                self.context.certificate,studylevel)
2388        except KeyError:
2389            self.flash(_('This level exists.'))
2390        except RequiredMissing:
2391            self.flash(_('Your data are incomplete'))
2392        self.redirect(self.url(self.context))
2393        return
2394
2395class StudyLevelEditFormPage(KofaEditFormPage):
2396    """ Page to edit the student study level data by students
2397    """
2398    grok.context(IStudentStudyLevel)
2399    grok.name('edit')
2400    grok.require('waeup.editStudyLevel')
2401    grok.template('studyleveleditpage')
2402    form_fields = grok.AutoFields(IStudentStudyLevel).omit(
2403        'level_session', 'level_verdict')
2404    pnav = 4
2405
2406    def update(self, ADD=None, course=None):
2407        if not self.context.__parent__.is_current:
2408            emit_lock_message(self)
2409            return
2410        if self.context.student.state != PAID or \
2411            not self.context.is_current_level:
2412            emit_lock_message(self)
2413            return
2414        super(StudyLevelEditFormPage, self).update()
2415        datatable.need()
2416        warning.need()
2417        if ADD is not None:
2418            if not course:
2419                self.flash(_('No valid course code entered.'))
2420                return
2421            cat = queryUtility(ICatalog, name='courses_catalog')
2422            result = cat.searchResults(code=(course, course))
2423            if len(result) != 1:
2424                self.flash(_('Course not found.'))
2425                return
2426            course = list(result)[0]
2427            addCourseTicket(self, course)
2428        return
2429
2430    @property
2431    def label(self):
2432        # Here we know that the cookie has been set
2433        lang = self.request.cookies.get('kofa.language')
2434        level_title = translate(self.context.level_title, 'waeup.kofa',
2435            target_language=lang)
2436        return _('Edit course list of ${a}',
2437            mapping = {'a':level_title})
2438
2439    @property
2440    def translated_values(self):
2441        return translated_values(self)
2442
2443    def _delCourseTicket(self, **data):
2444        form = self.request.form
2445        if 'val_id' in form:
2446            child_id = form['val_id']
2447        else:
2448            self.flash(_('No ticket selected.'))
2449            self.redirect(self.url(self.context, '@@edit'))
2450            return
2451        if not isinstance(child_id, list):
2452            child_id = [child_id]
2453        deleted = []
2454        for id in child_id:
2455            # Students are not allowed to remove core tickets
2456            if id in self.context and \
2457                self.context[id].removable_by_student:
2458                del self.context[id]
2459                deleted.append(id)
2460        if len(deleted):
2461            self.flash(_('Successfully removed: ${a}',
2462                mapping = {'a':', '.join(deleted)}))
2463            self.context.writeLogMessage(
2464                self,'removed: %s at %s' %
2465                (', '.join(deleted), self.context.level))
2466        self.redirect(self.url(self.context, u'@@edit'))
2467        return
2468
2469    @jsaction(_('Remove selected tickets'))
2470    def delCourseTicket(self, **data):
2471        self._delCourseTicket(**data)
2472        return
2473
2474    def _registerCourses(self, **data):
2475        if self.context.student.is_postgrad and \
2476            not self.context.student.is_special_postgrad:
2477            self.flash(_(
2478                "You are a postgraduate student, "
2479                "your course list can't bee registered."))
2480            self.redirect(self.url(self.context))
2481            return
2482        students_utils = getUtility(IStudentsUtils)
2483        max_credits = students_utils.maxCredits(self.context)
2484        if self.context.total_credits > max_credits:
2485            self.flash(_('Maximum credits of ${a} exceeded.',
2486                mapping = {'a':max_credits}))
2487            return
2488        IWorkflowInfo(self.context.student).fireTransition(
2489            'register_courses')
2490        self.flash(_('Course list has been registered.'))
2491        self.redirect(self.url(self.context))
2492        return
2493
2494    @action(_('Register course list'))
2495    def registerCourses(self, **data):
2496        self._registerCourses(**data)
2497        return
2498
2499class CourseTicketAddFormPage2(CourseTicketAddFormPage):
2500    """Add a course ticket by student.
2501    """
2502    grok.name('ctadd')
2503    grok.require('waeup.handleStudent')
2504    form_fields = grok.AutoFields(ICourseTicketAdd)
2505
2506    def update(self):
2507        if self.context.student.state != PAID or \
2508            not self.context.is_current_level:
2509            emit_lock_message(self)
2510            return
2511        super(CourseTicketAddFormPage2, self).update()
2512        return
2513
2514    @action(_('Add course ticket'))
2515    def addCourseTicket(self, **data):
2516        # Safety belt
2517        if self.context.student.state != PAID:
2518            return
2519        course = data['course']
2520        success = addCourseTicket(self, course)
2521        if success:
2522            self.redirect(self.url(self.context, u'@@edit'))
2523        return
2524
2525class SetPasswordPage(KofaPage):
2526    grok.context(IKofaObject)
2527    grok.name('setpassword')
2528    grok.require('waeup.Anonymous')
2529    grok.template('setpassword')
2530    label = _('Set password for first-time login')
2531    ac_prefix = 'PWD'
2532    pnav = 0
2533    set_button = _('Set')
2534
2535    def update(self, SUBMIT=None):
2536        self.reg_number = self.request.form.get('reg_number', None)
2537        self.ac_series = self.request.form.get('ac_series', None)
2538        self.ac_number = self.request.form.get('ac_number', None)
2539
2540        if SUBMIT is None:
2541            return
2542        hitlist = search(query=self.reg_number,
2543            searchtype='reg_number', view=self)
2544        if not hitlist:
2545            self.flash(_('No student found.'))
2546            return
2547        if len(hitlist) != 1:   # Cannot happen but anyway
2548            self.flash(_('More than one student found.'))
2549            return
2550        student = hitlist[0].context
2551        self.student_id = student.student_id
2552        student_pw = student.password
2553        pin = '%s-%s-%s' % (self.ac_prefix, self.ac_series, self.ac_number)
2554        code = get_access_code(pin)
2555        if not code:
2556            self.flash(_('Access code is invalid.'))
2557            return
2558        if student_pw and pin == student.adm_code:
2559            self.flash(_(
2560                'Password has already been set. Your Student Id is ${a}',
2561                mapping = {'a':self.student_id}))
2562            return
2563        elif student_pw:
2564            self.flash(
2565                _('Password has already been set. You are using the ' +
2566                'wrong Access Code.'))
2567            return
2568        # Mark pin as used (this also fires a pin related transition)
2569        # and set student password
2570        if code.state == USED:
2571            self.flash(_('Access code has already been used.'))
2572            return
2573        else:
2574            comment = _(u"invalidated")
2575            # Here we know that the ac is in state initialized so we do not
2576            # expect an exception
2577            invalidate_accesscode(pin,comment)
2578            IUserAccount(student).setPassword(self.ac_number)
2579            student.adm_code = pin
2580        self.flash(_('Password has been set. Your Student Id is ${a}',
2581            mapping = {'a':self.student_id}))
2582        return
2583
2584class StudentRequestPasswordPage(KofaAddFormPage):
2585    """Captcha'd registration page for applicants.
2586    """
2587    grok.name('requestpw')
2588    grok.require('waeup.Anonymous')
2589    grok.template('requestpw')
2590    form_fields = grok.AutoFields(IStudentRequestPW).select(
2591        'firstname','number','email')
2592    label = _('Request password for first-time login')
2593
2594    def update(self):
2595        # Handle captcha
2596        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2597        self.captcha_result = self.captcha.verify(self.request)
2598        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2599        return
2600
2601    def _redirect(self, email, password, student_id):
2602        # Forward only email to landing page in base package.
2603        self.redirect(self.url(self.context, 'requestpw_complete',
2604            data = dict(email=email)))
2605        return
2606
2607    def _pw_used(self):
2608        # XXX: False if password has not been used. We need an extra
2609        #      attribute which remembers if student logged in.
2610        return True
2611
2612    @action(_('Send login credentials to email address'), style='primary')
2613    def get_credentials(self, **data):
2614        if not self.captcha_result.is_valid:
2615            # Captcha will display error messages automatically.
2616            # No need to flash something.
2617            return
2618        number = data.get('number','')
2619        firstname = data.get('firstname','')
2620        cat = getUtility(ICatalog, name='students_catalog')
2621        results = list(
2622            cat.searchResults(reg_number=(number, number)))
2623        if not results:
2624            results = list(
2625                cat.searchResults(matric_number=(number, number)))
2626        if results:
2627            student = results[0]
2628            if getattr(student,'firstname',None) is None:
2629                self.flash(_('An error occurred.'))
2630                return
2631            elif student.firstname.lower() != firstname.lower():
2632                # Don't tell the truth here. Anonymous must not
2633                # know that a record was found and only the firstname
2634                # verification failed.
2635                self.flash(_('No student record found.'))
2636                return
2637            elif student.password is not None and self._pw_used:
2638                self.flash(_('Your password has already been set and used. '
2639                             'Please proceed to the login page.'))
2640                return
2641            # Store email address but nothing else.
2642            student.email = data['email']
2643            notify(grok.ObjectModifiedEvent(student))
2644        else:
2645            # No record found, this is the truth.
2646            self.flash(_('No student record found.'))
2647            return
2648
2649        kofa_utils = getUtility(IKofaUtils)
2650        password = kofa_utils.genPassword()
2651        mandate = PasswordMandate()
2652        mandate.params['password'] = password
2653        mandate.params['user'] = student
2654        site = grok.getSite()
2655        site['mandates'].addMandate(mandate)
2656        # Send email with credentials
2657        args = {'mandate_id':mandate.mandate_id}
2658        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2659        url_info = u'Confirmation link: %s' % mandate_url
2660        msg = _('You have successfully requested a password for the')
2661        if kofa_utils.sendCredentials(IUserAccount(student),
2662            password, url_info, msg):
2663            email_sent = student.email
2664        else:
2665            email_sent = None
2666        self._redirect(email=email_sent, password=password,
2667            student_id=student.student_id)
2668        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2669        self.context.logger.info(
2670            '%s - %s (%s) - %s' % (ob_class, number, student.student_id, email_sent))
2671        return
2672
2673class StudentRequestPasswordEmailSent(KofaPage):
2674    """Landing page after successful password request.
2675
2676    """
2677    grok.name('requestpw_complete')
2678    grok.require('waeup.Public')
2679    grok.template('requestpwmailsent')
2680    label = _('Your password request was successful.')
2681
2682    def update(self, email=None, student_id=None, password=None):
2683        self.email = email
2684        self.password = password
2685        self.student_id = student_id
2686        return
2687
2688class FilterStudentsInDepartmentPage(KofaPage):
2689    """Page that filters and lists students.
2690    """
2691    grok.context(IDepartment)
2692    grok.require('waeup.showStudents')
2693    grok.name('students')
2694    grok.template('filterstudentspage')
2695    pnav = 1
2696    session_label = _('Current Session')
2697    level_label = _('Current Level')
2698
2699    def label(self):
2700        return 'Students in %s' % self.context.longtitle()
2701
2702    def _set_session_values(self):
2703        vocab_terms = academic_sessions_vocab.by_value.values()
2704        self.sessions = sorted(
2705            [(x.title, x.token) for x in vocab_terms], reverse=True)
2706        self.sessions += [('All Sessions', 'all')]
2707        return
2708
2709    def _set_level_values(self):
2710        vocab_terms = course_levels.by_value.values()
2711        self.levels = sorted(
2712            [(x.title, x.token) for x in vocab_terms])
2713        self.levels += [('All Levels', 'all')]
2714        return
2715
2716    def _searchCatalog(self, session, level):
2717        if level not in (10, 999, None):
2718            start_level = 100 * (level // 100)
2719            end_level = start_level + 90
2720        else:
2721            start_level = end_level = level
2722        cat = queryUtility(ICatalog, name='students_catalog')
2723        students = cat.searchResults(
2724            current_session=(session, session),
2725            current_level=(start_level, end_level),
2726            depcode=(self.context.code, self.context.code)
2727            )
2728        hitlist = []
2729        for student in students:
2730            hitlist.append(StudentQueryResultItem(student, view=self))
2731        return hitlist
2732
2733    def update(self, SHOW=None, session=None, level=None):
2734        datatable.need()
2735        self.parent_url = self.url(self.context.__parent__)
2736        self._set_session_values()
2737        self._set_level_values()
2738        self.hitlist = []
2739        self.session_default = session
2740        self.level_default = level
2741        if SHOW is not None:
2742            if session != 'all':
2743                self.session = int(session)
2744                self.session_string = '%s %s/%s' % (
2745                    self.session_label, self.session, self.session+1)
2746            else:
2747                self.session = None
2748                self.session_string = _('in any session')
2749            if level != 'all':
2750                self.level = int(level)
2751                self.level_string = '%s %s' % (self.level_label, self.level)
2752            else:
2753                self.level = None
2754                self.level_string = _('at any level')
2755            self.hitlist = self._searchCatalog(self.session, self.level)
2756            if not self.hitlist:
2757                self.flash(_('No student found.'))
2758        return
2759
2760class FilterStudentsInCertificatePage(FilterStudentsInDepartmentPage):
2761    """Page that filters and lists students.
2762    """
2763    grok.context(ICertificate)
2764
2765    def label(self):
2766        return 'Students studying %s' % self.context.longtitle()
2767
2768    def _searchCatalog(self, session, level):
2769        if level not in (10, 999, None):
2770            start_level = 100 * (level // 100)
2771            end_level = start_level + 90
2772        else:
2773            start_level = end_level = level
2774        cat = queryUtility(ICatalog, name='students_catalog')
2775        students = cat.searchResults(
2776            current_session=(session, session),
2777            current_level=(start_level, end_level),
2778            certcode=(self.context.code, self.context.code)
2779            )
2780        hitlist = []
2781        for student in students:
2782            hitlist.append(StudentQueryResultItem(student, view=self))
2783        return hitlist
2784
2785class FilterStudentsInCoursePage(FilterStudentsInDepartmentPage):
2786    """Page that filters and lists students.
2787    """
2788    grok.context(ICourse)
2789
2790    session_label = _('Session')
2791    level_label = _('Level')
2792
2793    def label(self):
2794        return 'Students registered for %s' % self.context.longtitle()
2795
2796    def _searchCatalog(self, session, level):
2797        if level not in (10, 999, None):
2798            start_level = 100 * (level // 100)
2799            end_level = start_level + 90
2800        else:
2801            start_level = end_level = level
2802        cat = queryUtility(ICatalog, name='coursetickets_catalog')
2803        coursetickets = cat.searchResults(
2804            session=(session, session),
2805            level=(start_level, end_level),
2806            code=(self.context.code, self.context.code)
2807            )
2808        hitlist = []
2809        for ticket in coursetickets:
2810            hitlist.append(StudentQueryResultItem(ticket.student, view=self))
2811        return list(set(hitlist))
2812
2813class ExportJobContainerOverview(KofaPage):
2814    """Page that lists active student data export jobs and provides links
2815    to discard or download CSV files.
2816
2817    """
2818    grok.context(VirtualExportJobContainer)
2819    grok.require('waeup.showStudents')
2820    grok.name('index.html')
2821    grok.template('exportjobsindex')
2822    label = _('Student Data Exports')
2823    pnav = 1
2824
2825    def update(self, CREATE=None, DISCARD=None, job_id=None):
2826        if CREATE:
2827            self.redirect(self.url('@@exportconfig'))
2828            return
2829        if DISCARD and job_id:
2830            entry = self.context.entry_from_job_id(job_id)
2831            self.context.delete_export_entry(entry)
2832            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2833            self.context.logger.info(
2834                '%s - discarded: job_id=%s' % (ob_class, job_id))
2835            self.flash(_('Discarded export') + ' %s' % job_id)
2836        self.entries = doll_up(self, user=self.request.principal.id)
2837        return
2838
2839class ExportJobContainerJobConfig(KofaPage):
2840    """Page that configures a students export job.
2841
2842    This is a baseclass.
2843    """
2844    grok.baseclass()
2845    grok.name('exportconfig')
2846    grok.require('waeup.showStudents')
2847    grok.template('exportconfig')
2848    label = _('Configure student data export')
2849    pnav = 1
2850    redirect_target = ''
2851
2852    def _set_session_values(self):
2853        vocab_terms = academic_sessions_vocab.by_value.values()
2854        self.sessions = sorted(
2855            [(x.title, x.token) for x in vocab_terms], reverse=True)
2856        self.sessions += [(_('All Sessions'), 'all')]
2857        return
2858
2859    def _set_level_values(self):
2860        vocab_terms = course_levels.by_value.values()
2861        self.levels = sorted(
2862            [(x.title, x.token) for x in vocab_terms])
2863        self.levels += [(_('All Levels'), 'all')]
2864        return
2865
2866    def _set_mode_values(self):
2867        utils = getUtility(IKofaUtils)
2868        self.modes = sorted([(value, key) for key, value in
2869                      utils.STUDY_MODES_DICT.items()])
2870        self.modes +=[(_('All Modes'), 'all')]
2871        return
2872
2873    def _set_exporter_values(self):
2874        # We provide all student exporters, nothing else, yet.
2875        exporters = []
2876        for name in EXPORTER_NAMES:
2877            util = getUtility(ICSVExporter, name=name)
2878            exporters.append((util.title, name),)
2879        self.exporters = exporters
2880
2881    @property
2882    def depcode(self):
2883        return None
2884
2885    @property
2886    def certcode(self):
2887        return None
2888
2889    def update(self, START=None, session=None, level=None, mode=None,
2890               exporter=None):
2891        self._set_session_values()
2892        self._set_level_values()
2893        self._set_mode_values()
2894        self._set_exporter_values()
2895        if START is None:
2896            return
2897        if session == 'all':
2898            session=None
2899        if level == 'all':
2900            level = None
2901        if mode == 'all':
2902            mode = None
2903        if (mode, level, session,
2904            self.depcode, self.certcode) == (None, None, None, None, None):
2905            # Export all students including those without certificate
2906            job_id = self.context.start_export_job(exporter,
2907                                          self.request.principal.id)
2908        else:
2909            job_id = self.context.start_export_job(exporter,
2910                                          self.request.principal.id,
2911                                          current_session=session,
2912                                          current_level=level,
2913                                          current_mode=mode,
2914                                          depcode=self.depcode,
2915                                          certcode=self.certcode)
2916        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2917        self.context.logger.info(
2918            '%s - exported: %s (%s, %s, %s, %s, %s), job_id=%s'
2919            % (ob_class, exporter, session, level, mode, self.depcode,
2920            self.certcode, job_id))
2921        self.flash(_('Export started for students with') +
2922                   ' current_session=%s, current_level=%s, study_mode=%s' % (
2923                   session, level, mode))
2924        self.redirect(self.url(self.redirect_target))
2925        return
2926
2927class ExportJobContainerDownload(ExportCSVView):
2928    """Page that downloads a students export csv file.
2929
2930    """
2931    grok.context(VirtualExportJobContainer)
2932    grok.require('waeup.showStudents')
2933
2934class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
2935    """Page that configures a students export job in datacenter.
2936
2937    """
2938    grok.context(IDataCenter)
2939    redirect_target = '@@export'
2940
2941class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
2942    """Page that configures a students export job in departments.
2943
2944    """
2945    grok.context(VirtualDepartmentExportJobContainer)
2946
2947    @property
2948    def depcode(self):
2949        return self.context.__parent__.code
2950
2951class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
2952    """Page that configures a students export job for certificates.
2953
2954    """
2955    grok.context(VirtualCertificateExportJobContainer)
2956    grok.template('exportconfig_certificate')
2957
2958    @property
2959    def certcode(self):
2960        return self.context.__parent__.code
2961
2962class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
2963    """Page that configures a students export job for courses.
2964
2965    In contrast to department or certificate student data exports the
2966    coursetickets_catalog is searched here. Therefore the update
2967    method from the base class is customized.
2968    """
2969    grok.context(VirtualCourseExportJobContainer)
2970    grok.template('exportconfig_course')
2971
2972    def _set_exporter_values(self):
2973        # We provide only two exporters.
2974        exporters = []
2975        for name in ('students', 'coursetickets'):
2976            util = getUtility(ICSVExporter, name=name)
2977            exporters.append((util.title, name),)
2978        self.exporters = exporters
2979
2980    def update(self, START=None, session=None, level=None, mode=None,
2981               exporter=None):
2982        self._set_session_values()
2983        self._set_level_values()
2984        self._set_mode_values()
2985        self._set_exporter_values()
2986        if START is None:
2987            return
2988        if session == 'all':
2989            session = None
2990        if level == 'all':
2991            level = None
2992        job_id = self.context.start_export_job(exporter,
2993                                      self.request.principal.id,
2994                                      # Use a different catalog and
2995                                      # pass different keywords than
2996                                      # for the (default) students_catalog
2997                                      catalog='coursetickets',
2998                                      session=session,
2999                                      level=level,
3000                                      code=self.context.__parent__.code)
3001        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3002        self.context.logger.info(
3003            '%s - exported: %s (%s, %s, %s), job_id=%s'
3004            % (ob_class, exporter, session, level,
3005            self.context.__parent__.code, job_id))
3006        self.flash(_('Export started for course tickets with') +
3007                   ' level_session=%s, level=%s' % (
3008                   session, level))
3009        self.redirect(self.url(self.redirect_target))
3010        return
Note: See TracBrowser for help on using the repository browser.