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

Last change on this file since 14269 was 14251, checked in by Henrik Bettermann, 8 years ago

Raise customized error message.

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