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

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

Denote session in button.

  • Property svn:keywords set to Id
File size: 133.7 KB
Line 
1## $Id: browser.py 14286 2016-11-17 21:27:49Z 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_scores(self, form):
3135        ob_class = self.__implemented__.__name__.replace('waeup.kofa.', '')
3136        error = ''
3137        if 'UPDATE_FILE' in form:
3138            if form['uploadfile']:
3139                try:
3140                    formvals = self._extract_uploadfile(form['uploadfile'])
3141                except:
3142                    self.flash(
3143                        _('Uploaded file contains illegal data. Ignored'),
3144                        type="danger")
3145                    return False
3146            else:
3147                self.flash(
3148                    _('No file provided.'), type="danger")
3149                return False
3150        else:
3151            formvals = dict(zip(form['sids'], form['scores']))
3152        for ticket in self.editable_tickets:
3153            score = ticket.score
3154            sid = ticket.student.student_id
3155            if sid not in formvals:
3156                continue
3157            if formvals[sid] == '':
3158                score = None
3159            else:
3160                try:
3161                    score = int(formvals[sid])
3162                except ValueError:
3163                    error += '%s, ' % ticket.student.display_fullname
3164            if ticket.score != score:
3165                ticket.score = score
3166                ticket.student.__parent__.logger.info(
3167                    '%s - %s %s/%s score updated (%s)' % (
3168                        ob_class, ticket.student.student_id,
3169                        ticket.level, ticket.code, score)
3170                    )
3171        if error:
3172            self.flash(
3173                _('Error: Score(s) of following students have not been '
3174                    'updated (only integers are allowed): %s.' % error.strip(', ')),
3175                type="danger")
3176        return True
3177
3178    def update(self,  *args, **kw):
3179        form = self.request.form
3180        self.current_academic_session = grok.getSite()[
3181            'configuration'].current_academic_session
3182        if self.context.__parent__.__parent__.score_editing_disabled:
3183            self.flash(_('Score editing disabled.'), type="warning")
3184            self.redirect(self.url(self.context))
3185            return
3186        if not self.current_academic_session:
3187            self.flash(_('Current academic session not set.'), type="warning")
3188            self.redirect(self.url(self.context))
3189            return
3190        self.session_title = academic_sessions_vocab.getTerm(
3191            self.current_academic_session).title
3192        self.tickets = self._searchCatalog(self.current_academic_session)
3193        if not self.tickets:
3194            self.flash(_('No student found.'), type="warning")
3195            self.redirect(self.url(self.context))
3196            return
3197        self.editable_tickets = [
3198            ticket for ticket in self.tickets if ticket.editable_by_lecturer]
3199        if not 'UPDATE_TABLE' in form and not 'UPDATE_FILE' in form:
3200            return
3201        if not self.editable_tickets:
3202            return
3203        success = self._update_scores(form)
3204        if success:
3205            self.flash(_('You successfully updated course results.'))
3206        return
3207
3208
3209class DownloadScoresView(UtilityView, grok.View):
3210    """View that exports scores.
3211    """
3212    grok.context(ICourse)
3213    grok.require('waeup.editScores')
3214    grok.name('download_scores')
3215
3216    def update(self):
3217        self.current_academic_session = grok.getSite()[
3218            'configuration'].current_academic_session
3219        if self.context.__parent__.__parent__.score_editing_disabled:
3220            self.flash(_('Score editing disabled.'), type="warning")
3221            self.redirect(self.url(self.context))
3222            return
3223        if not self.current_academic_session:
3224            self.flash(_('Current academic session not set.'), type="warning")
3225            self.redirect(self.url(self.context))
3226            return
3227        site = grok.getSite()
3228        exporter = getUtility(ICSVExporter, name='lecturer')
3229        self.csv = exporter.export_filtered(site, filepath=None,
3230                                 catalog='coursetickets',
3231                                 session=self.current_academic_session,
3232                                 level=None,
3233                                 code=self.context.code)
3234        return
3235
3236    def render(self):
3237        filename = 'results_%s_%s.csv' % (
3238            self.context.code, self.current_academic_session)
3239        self.response.setHeader(
3240            'Content-Type', 'text/csv; charset=UTF-8')
3241        self.response.setHeader(
3242            'Content-Disposition:', 'attachment; filename="%s' % filename)
3243        return self.csv
3244
3245class ExportPDFScoresSlip(UtilityView, grok.View,
3246    LocalRoleAssignmentUtilityView):
3247    """Deliver a PDF slip of course tickets for a lecturer.
3248    """
3249    grok.context(ICourse)
3250    grok.name('coursetickets.pdf')
3251    grok.require('waeup.editScores')
3252
3253    def table_data(self, session):
3254        cat = queryUtility(ICatalog, name='coursetickets_catalog')
3255        coursetickets = cat.searchResults(
3256            session=(session, session),
3257            code=(self.context.code, self.context.code)
3258            )
3259        header = [[_('Matric No.'),
3260                   _('Reg. No.'),
3261                   _('Fullname'),
3262                   _('Status'),
3263                   _('Course of Studies'),
3264                   _('Level'),
3265                   _('Score') ],]
3266        tickets = []
3267        for ticket in list(coursetickets):
3268            row = [ticket.student.matric_number,
3269                  ticket.student.reg_number,
3270                  ticket.student.display_fullname,
3271                  ticket.student.translated_state,
3272                  ticket.student.certcode,
3273                  ticket.level,
3274                  ticket.score]
3275            tickets.append(row)
3276        return header + sorted(tickets, key=lambda value: value[0])
3277
3278    def render(self):
3279        session = grok.getSite()['configuration'].current_academic_session
3280        lecturers = [i['user_title'] for i in self.getUsersWithLocalRoles()
3281                     if i['local_role'] == 'waeup.local.Lecturer']
3282        lecturers =  ', '.join(lecturers)
3283        students_utils = getUtility(IStudentsUtils)
3284        return students_utils.renderPDFCourseticketsOverview(
3285            self, session, self.table_data(session), lecturers)
3286
3287class ExportJobContainerOverview(KofaPage):
3288    """Page that lists active student data export jobs and provides links
3289    to discard or download CSV files.
3290
3291    """
3292    grok.context(VirtualExportJobContainer)
3293    grok.require('waeup.showStudents')
3294    grok.name('index.html')
3295    grok.template('exportjobsindex')
3296    label = _('Student Data Exports')
3297    pnav = 1
3298    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3299
3300    def update(self, CREATE=None, DISCARD=None, job_id=None):
3301        if CREATE:
3302            self.redirect(self.url('@@exportconfig'))
3303            return
3304        if DISCARD and job_id:
3305            entry = self.context.entry_from_job_id(job_id)
3306            self.context.delete_export_entry(entry)
3307            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3308            self.context.logger.info(
3309                '%s - discarded: job_id=%s' % (ob_class, job_id))
3310            self.flash(_('Discarded export') + ' %s' % job_id)
3311        self.entries = doll_up(self, user=self.request.principal.id)
3312        return
3313
3314class ExportJobContainerJobConfig(KofaPage):
3315    """Page that configures a students export job.
3316
3317    This is a baseclass.
3318    """
3319    grok.baseclass()
3320    grok.name('exportconfig')
3321    grok.require('waeup.showStudents')
3322    grok.template('exportconfig')
3323    label = _('Configure student data export')
3324    pnav = 1
3325    redirect_target = ''
3326    doclink = DOCLINK + '/datacenter/export.html#student-data-exporters'
3327
3328    def _set_session_values(self):
3329        vocab_terms = academic_sessions_vocab.by_value.values()
3330        self.sessions = sorted(
3331            [(x.title, x.token) for x in vocab_terms], reverse=True)
3332        self.sessions += [(_('All Sessions'), 'all')]
3333        return
3334
3335    def _set_level_values(self):
3336        vocab_terms = course_levels.by_value.values()
3337        self.levels = sorted(
3338            [(x.title, x.token) for x in vocab_terms])
3339        self.levels += [(_('All Levels'), 'all')]
3340        return
3341
3342    def _set_mode_values(self):
3343        utils = getUtility(IKofaUtils)
3344        self.modes = sorted([(value, key) for key, value in
3345                      utils.STUDY_MODES_DICT.items()])
3346        self.modes +=[(_('All Modes'), 'all')]
3347        return
3348
3349    def _set_exporter_values(self):
3350        # We provide all student exporters, nothing else, yet.
3351        # Bursary or Department Officers don't have the general exportData
3352        # permission and are only allowed to export bursary or payments
3353        # overview data respectively. This is the only place where
3354        # waeup.exportBursaryData and waeup.exportPaymentsOverview
3355        # are used.
3356        exporters = []
3357        if not checkPermission('waeup.exportData', self.context):
3358            if checkPermission('waeup.exportBursaryData', self.context):
3359                exporters += [('Bursary Data', 'bursary')]
3360            if checkPermission('waeup.exportPaymentsOverview', self.context):
3361                exporters += [('Student Payments Overview', 'paymentsoverview')]
3362            self.exporters = exporters
3363            return
3364        STUDENT_EXPORTER_NAMES = getUtility(
3365            IStudentsUtils).STUDENT_EXPORTER_NAMES
3366        for name in STUDENT_EXPORTER_NAMES:
3367            util = getUtility(ICSVExporter, name=name)
3368            exporters.append((util.title, name),)
3369        self.exporters = exporters
3370        return
3371
3372    @property
3373    def faccode(self):
3374        return None
3375
3376    @property
3377    def depcode(self):
3378        return None
3379
3380    @property
3381    def certcode(self):
3382        return None
3383
3384    def update(self, START=None, session=None, level=None, mode=None,
3385               payments_start=None, payments_end=None,
3386               exporter=None):
3387        self._set_session_values()
3388        self._set_level_values()
3389        self._set_mode_values()
3390        self._set_exporter_values()
3391        if START is None:
3392            return
3393        ena = exports_not_allowed(self)
3394        if ena:
3395            self.flash(ena, type='danger')
3396            return
3397        if payments_start or payments_end:
3398            date_format = '%d/%m/%Y'
3399            try:
3400                datetime.strptime(payments_start, date_format)
3401                datetime.strptime(payments_end, date_format)
3402            except ValueError:
3403                self.flash(_('Payment dates do not match format d/m/Y.'),
3404                           type="danger")
3405                return
3406        if session == 'all':
3407            session=None
3408        if level == 'all':
3409            level = None
3410        if mode == 'all':
3411            mode = None
3412        if payments_start == '':
3413            payments_start = None
3414        if payments_end == '':
3415            payments_end = None
3416        if (mode,
3417            level,
3418            session,
3419            self.faccode,
3420            self.depcode,
3421            self.certcode) == (None, None, None, None, None, None):
3422            # Export all students including those without certificate
3423            if payments_start:
3424                job_id = self.context.start_export_job(exporter,
3425                                              self.request.principal.id,
3426                                              payments_start = payments_start,
3427                                              payments_end = payments_end)
3428            else:
3429                job_id = self.context.start_export_job(exporter,
3430                                              self.request.principal.id)
3431        else:
3432            if payments_start:
3433                job_id = self.context.start_export_job(exporter,
3434                                              self.request.principal.id,
3435                                              current_session=session,
3436                                              current_level=level,
3437                                              current_mode=mode,
3438                                              faccode=self.faccode,
3439                                              depcode=self.depcode,
3440                                              certcode=self.certcode,
3441                                              payments_start = payments_start,
3442                                              payments_end = payments_end)
3443            else:
3444                job_id = self.context.start_export_job(exporter,
3445                                              self.request.principal.id,
3446                                              current_session=session,
3447                                              current_level=level,
3448                                              current_mode=mode,
3449                                              faccode=self.faccode,
3450                                              depcode=self.depcode,
3451                                              certcode=self.certcode)
3452        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3453        self.context.logger.info(
3454            '%s - exported: %s (%s, %s, %s, %s, %s, %s, %s, %s), job_id=%s'
3455            % (ob_class, exporter, session, level, mode, self.faccode,
3456            self.depcode, self.certcode, payments_start, payments_end, job_id))
3457        self.flash(_('Export started for students with') +
3458                   ' current_session=%s, current_level=%s, study_mode=%s' % (
3459                   session, level, mode))
3460        self.redirect(self.url(self.redirect_target))
3461        return
3462
3463class ExportJobContainerDownload(ExportCSVView):
3464    """Page that downloads a students export csv file.
3465
3466    """
3467    grok.context(VirtualExportJobContainer)
3468    grok.require('waeup.showStudents')
3469
3470class DatacenterExportJobContainerJobConfig(ExportJobContainerJobConfig):
3471    """Page that configures a students export job in datacenter.
3472
3473    """
3474    grok.context(IDataCenter)
3475    redirect_target = '@@export'
3476
3477class DatacenterExportJobContainerSelectStudents(ExportJobContainerJobConfig):
3478    """Page that configures a students export job in datacenter.
3479
3480    """
3481    grok.name('exportselected')
3482    grok.context(IDataCenter)
3483    redirect_target = '@@export'
3484    grok.template('exportselected')
3485    label = _('Configure student data export')
3486
3487    def update(self, START=None, students=None, exporter=None):
3488        self._set_exporter_values()
3489        if START is None:
3490            return
3491        ena = exports_not_allowed(self)
3492        if ena:
3493            self.flash(ena, type='danger')
3494            return
3495        try:
3496            ids = students.replace(',', ' ').split()
3497        except:
3498            self.flash(sys.exc_info()[1])
3499            self.redirect(self.url(self.redirect_target))
3500            return
3501        job_id = self.context.start_export_job(
3502            exporter, self.request.principal.id, selected=ids)
3503        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3504        self.context.logger.info(
3505            '%s - selected students exported: %s, job_id=%s' %
3506            (ob_class, exporter, job_id))
3507        self.flash(_('Export of selected students started.'))
3508        self.redirect(self.url(self.redirect_target))
3509        return
3510
3511class FacultiesExportJobContainerJobConfig(ExportJobContainerJobConfig):
3512    """Page that configures a students export job in facultiescontainer.
3513
3514    """
3515    grok.context(VirtualFacultiesExportJobContainer)
3516
3517
3518class FacultyExportJobContainerJobConfig(ExportJobContainerJobConfig):
3519    """Page that configures a students export job in faculties.
3520
3521    """
3522    grok.context(VirtualFacultyExportJobContainer)
3523
3524    @property
3525    def faccode(self):
3526        return self.context.__parent__.code
3527
3528class DepartmentExportJobContainerJobConfig(ExportJobContainerJobConfig):
3529    """Page that configures a students export job in departments.
3530
3531    """
3532    grok.context(VirtualDepartmentExportJobContainer)
3533
3534    @property
3535    def depcode(self):
3536        return self.context.__parent__.code
3537
3538class CertificateExportJobContainerJobConfig(ExportJobContainerJobConfig):
3539    """Page that configures a students export job for certificates.
3540
3541    """
3542    grok.context(VirtualCertificateExportJobContainer)
3543    grok.template('exportconfig_certificate')
3544
3545    @property
3546    def certcode(self):
3547        return self.context.__parent__.code
3548
3549class CourseExportJobContainerJobConfig(ExportJobContainerJobConfig):
3550    """Page that configures a students export job for courses.
3551
3552    In contrast to department or certificate student data exports the
3553    coursetickets_catalog is searched here. Therefore the update
3554    method from the base class is customized.
3555    """
3556    grok.context(VirtualCourseExportJobContainer)
3557    grok.template('exportconfig_course')
3558
3559    def _set_exporter_values(self):
3560        # We provide only the 'coursetickets' and 'lecturer' exporter
3561        # but can add more.
3562        exporters = []
3563        for name in ('coursetickets', 'lecturer'):
3564            util = getUtility(ICSVExporter, name=name)
3565            exporters.append((util.title, name),)
3566        self.exporters = exporters
3567
3568    def _set_session_values(self):
3569        # We allow only current academic session
3570        academic_session = grok.getSite()['configuration'].current_academic_session
3571        if not academic_session:
3572            self.sessions = []
3573            return
3574        x = academic_sessions_vocab.getTerm(academic_session)
3575        self.sessions = [(x.title, x.token)]
3576        return
3577
3578    def update(self, START=None, session=None, level=None, mode=None,
3579               exporter=None):
3580        self._set_session_values()
3581        self._set_level_values()
3582        self._set_mode_values()
3583        self._set_exporter_values()
3584        if not self.sessions:
3585            self.flash(
3586                _('Academic session not set. '
3587                  'Please contact the administrator.'),
3588                type='danger')
3589            self.redirect(self.url(self.context))
3590            return
3591        if START is None:
3592            return
3593        ena = exports_not_allowed(self)
3594        if ena:
3595            self.flash(ena, type='danger')
3596            return
3597        if session == 'all':
3598            session = None
3599        if level == 'all':
3600            level = None
3601        job_id = self.context.start_export_job(exporter,
3602                                      self.request.principal.id,
3603                                      # Use a different catalog and
3604                                      # pass different keywords than
3605                                      # for the (default) students_catalog
3606                                      catalog='coursetickets',
3607                                      session=session,
3608                                      level=level,
3609                                      code=self.context.__parent__.code)
3610        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
3611        self.context.logger.info(
3612            '%s - exported: %s (%s, %s, %s), job_id=%s'
3613            % (ob_class, exporter, session, level,
3614            self.context.__parent__.code, job_id))
3615        self.flash(_('Export started for course tickets with') +
3616                   ' level_session=%s, level=%s' % (
3617                   session, level))
3618        self.redirect(self.url(self.redirect_target))
3619        return
Note: See TracBrowser for help on using the repository browser.