source: main/waeup.kofa/trunk/src/waeup/kofa/applicants/browser.py @ 10895

Last change on this file since 10895 was 10895, checked in by Henrik Bettermann, 11 years ago

Flash message if student record is created.

  • Property svn:keywords set to Id
File size: 45.5 KB
Line 
1## $Id: browser.py 10895 2014-01-12 18:44:14Z 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 basic applicants and related components.
19"""
20import os
21import sys
22import grok
23from datetime import datetime, date
24from zope.event import notify
25from zope.component import getUtility, createObject, getAdapter
26from zope.catalog.interfaces import ICatalog
27from zope.i18n import translate
28from hurry.workflow.interfaces import (
29    IWorkflowInfo, IWorkflowState, InvalidTransitionError)
30from waeup.kofa.applicants.interfaces import (
31    IApplicant, IApplicantEdit, IApplicantsRoot,
32    IApplicantsContainer, IApplicantsContainerAdd,
33    MAX_UPLOAD_SIZE, IApplicantOnlinePayment, IApplicantsUtils,
34    IApplicantRegisterUpdate, ISpecialApplicant
35    )
36from waeup.kofa.applicants.container import (
37    ApplicantsContainer, VirtualApplicantsExportJobContainer)
38from waeup.kofa.applicants.applicant import search
39from waeup.kofa.applicants.workflow import (
40    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
41from waeup.kofa.browser import (
42#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
43    DEFAULT_PASSPORT_IMAGE_PATH)
44from waeup.kofa.browser.layout import (
45    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
46from waeup.kofa.browser.interfaces import ICaptchaManager
47from waeup.kofa.browser.breadcrumbs import Breadcrumb
48from waeup.kofa.browser.resources import toggleall
49from waeup.kofa.browser.layout import (
50    NullValidator, jsaction, action, UtilityView, JSAction)
51from waeup.kofa.browser.pages import (
52    add_local_role, del_local_roles, doll_up, ExportCSVView)
53from waeup.kofa.browser.resources import datepicker, tabs, datatable, warning
54from waeup.kofa.interfaces import (
55    IKofaObject, ILocalRolesAssignable, IExtFileStore, IPDF,
56    IFileStoreNameChooser, IPasswordValidator, IUserAccount, IKofaUtils)
57from waeup.kofa.interfaces import MessageFactory as _
58from waeup.kofa.permissions import get_users_with_local_roles
59from waeup.kofa.students.interfaces import IStudentsUtils
60from waeup.kofa.utils.helpers import string_from_bytes, file_size, now
61from waeup.kofa.widgets.datewidget import (
62    FriendlyDateDisplayWidget,
63    FriendlyDatetimeDisplayWidget)
64from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
65
66grok.context(IKofaObject) # Make IKofaObject the default context
67
68class SubmitJSAction(JSAction):
69
70    msg = _('\'You can not edit your application records after final submission.'
71            ' You really want to submit?\'')
72
73class submitaction(grok.action):
74
75    def __call__(self, success):
76        action = SubmitJSAction(self.label, success=success, **self.options)
77        self.actions.append(action)
78        return action
79
80class ApplicantsRootPage(KofaDisplayFormPage):
81    grok.context(IApplicantsRoot)
82    grok.name('index')
83    grok.require('waeup.Public')
84    form_fields = grok.AutoFields(IApplicantsRoot)
85    form_fields['description'].custom_widget = HTMLDisplayWidget
86    label = _('Application Section')
87    pnav = 3
88
89    def update(self):
90        super(ApplicantsRootPage, self).update()
91        return
92
93    @property
94    def introduction(self):
95        # Here we know that the cookie has been set
96        lang = self.request.cookies.get('kofa.language')
97        html = self.context.description_dict.get(lang,'')
98        if html == '':
99            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
100            html = self.context.description_dict.get(portal_language,'')
101        return html
102
103    @property
104    def containers(self):
105        if self.layout.isAuthenticated():
106            return self.context.values()
107        return [value for value in self.context.values() if not value.hidden]
108
109class ApplicantsSearchPage(KofaPage):
110    grok.context(IApplicantsRoot)
111    grok.name('search')
112    grok.require('waeup.viewApplication')
113    label = _('Find applicants')
114    search_button = _('Find applicant')
115    pnav = 3
116
117    def update(self, *args, **kw):
118        datatable.need()
119        form = self.request.form
120        self.results = []
121        if 'searchterm' in form and form['searchterm']:
122            self.searchterm = form['searchterm']
123            self.searchtype = form['searchtype']
124        elif 'old_searchterm' in form:
125            self.searchterm = form['old_searchterm']
126            self.searchtype = form['old_searchtype']
127        else:
128            if 'search' in form:
129                self.flash(_('Empty search string'))
130            return
131        self.results = search(query=self.searchterm,
132            searchtype=self.searchtype, view=self)
133        if not self.results:
134            self.flash(_('No applicant found.'))
135        return
136
137class ApplicantsRootManageFormPage(KofaEditFormPage):
138    grok.context(IApplicantsRoot)
139    grok.name('manage')
140    grok.template('applicantsrootmanagepage')
141    form_fields = grok.AutoFields(IApplicantsRoot)
142    label = _('Manage application section')
143    pnav = 3
144    grok.require('waeup.manageApplication')
145    taboneactions = [_('Save')]
146    tabtwoactions = [_('Add applicants container'), _('Remove selected')]
147    tabthreeactions1 = [_('Remove selected local roles')]
148    tabthreeactions2 = [_('Add local role')]
149    subunits = _('Applicants Containers')
150
151    def update(self):
152        tabs.need()
153        datatable.need()
154        warning.need()
155        self.tab1 = self.tab2 = self.tab3 = ''
156        qs = self.request.get('QUERY_STRING', '')
157        if not qs:
158            qs = 'tab1'
159        setattr(self, qs, 'active')
160        return super(ApplicantsRootManageFormPage, self).update()
161
162    def getLocalRoles(self):
163        roles = ILocalRolesAssignable(self.context)
164        return roles()
165
166    def getUsers(self):
167        """Get a list of all users.
168        """
169        for key, val in grok.getSite()['users'].items():
170            url = self.url(val)
171            yield(dict(url=url, name=key, val=val))
172
173    def getUsersWithLocalRoles(self):
174        return get_users_with_local_roles(self.context)
175
176    @jsaction(_('Remove selected'))
177    def delApplicantsContainers(self, **data):
178        form = self.request.form
179        if 'val_id' in form:
180            child_id = form['val_id']
181        else:
182            self.flash(_('No container selected!'))
183            self.redirect(self.url(self.context, '@@manage')+'?tab2')
184            return
185        if not isinstance(child_id, list):
186            child_id = [child_id]
187        deleted = []
188        for id in child_id:
189            try:
190                del self.context[id]
191                deleted.append(id)
192            except:
193                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
194                        id, sys.exc_info()[0], sys.exc_info()[1]))
195        if len(deleted):
196            self.flash(_('Successfully removed: ${a}',
197                mapping = {'a':', '.join(deleted)}))
198        self.redirect(self.url(self.context, '@@manage')+'?tab2')
199        return
200
201    @action(_('Add applicants container'), validator=NullValidator)
202    def addApplicantsContainer(self, **data):
203        self.redirect(self.url(self.context, '@@add'))
204        return
205
206    @action(_('Add local role'), validator=NullValidator)
207    def addLocalRole(self, **data):
208        return add_local_role(self,3, **data)
209
210    @action(_('Remove selected local roles'))
211    def delLocalRoles(self, **data):
212        return del_local_roles(self,3,**data)
213
214    def _description(self):
215        view = ApplicantsRootPage(
216            self.context,self.request)
217        view.setUpWidgets()
218        return view.widgets['description']()
219
220    @action(_('Save'), style='primary')
221    def save(self, **data):
222        self.applyData(self.context, **data)
223        self.context.description_dict = self._description()
224        self.flash(_('Form has been saved.'))
225        return
226
227class ApplicantsContainerAddFormPage(KofaAddFormPage):
228    grok.context(IApplicantsRoot)
229    grok.require('waeup.manageApplication')
230    grok.name('add')
231    grok.template('applicantscontaineraddpage')
232    label = _('Add applicants container')
233    pnav = 3
234
235    form_fields = grok.AutoFields(
236        IApplicantsContainerAdd).omit('code').omit('title')
237
238    def update(self):
239        datepicker.need() # Enable jQuery datepicker in date fields.
240        return super(ApplicantsContainerAddFormPage, self).update()
241
242    @action(_('Add applicants container'))
243    def addApplicantsContainer(self, **data):
244        year = data['year']
245        code = u'%s%s' % (data['prefix'], year)
246        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
247        title = apptypes_dict[data['prefix']][0]
248        title = u'%s %s/%s' % (title, year, year + 1)
249        if code in self.context.keys():
250            self.flash(
251                _('An applicants container for the same application type and entrance year exists already in the database.'))
252            return
253        # Add new applicants container...
254        container = createObject(u'waeup.ApplicantsContainer')
255        self.applyData(container, **data)
256        container.code = code
257        container.title = title
258        self.context[code] = container
259        self.flash(_('Added:') + ' "%s".' % code)
260        self.redirect(self.url(self.context, u'@@manage'))
261        return
262
263    @action(_('Cancel'), validator=NullValidator)
264    def cancel(self, **data):
265        self.redirect(self.url(self.context, '@@manage'))
266
267class ApplicantsRootBreadcrumb(Breadcrumb):
268    """A breadcrumb for applicantsroot.
269    """
270    grok.context(IApplicantsRoot)
271    title = _(u'Applicants')
272
273class ApplicantsContainerBreadcrumb(Breadcrumb):
274    """A breadcrumb for applicantscontainers.
275    """
276    grok.context(IApplicantsContainer)
277
278
279class ApplicantsExportsBreadcrumb(Breadcrumb):
280    """A breadcrumb for exports.
281    """
282    grok.context(VirtualApplicantsExportJobContainer)
283    title = _(u'Applicant Data Exports')
284    target = None
285
286class ApplicantBreadcrumb(Breadcrumb):
287    """A breadcrumb for applicants.
288    """
289    grok.context(IApplicant)
290
291    @property
292    def title(self):
293        """Get a title for a context.
294        """
295        return self.context.application_number
296
297class OnlinePaymentBreadcrumb(Breadcrumb):
298    """A breadcrumb for payments.
299    """
300    grok.context(IApplicantOnlinePayment)
301
302    @property
303    def title(self):
304        return self.context.p_id
305
306class ApplicantsStatisticsPage(KofaDisplayFormPage):
307    """Some statistics about applicants in a container.
308    """
309    grok.context(IApplicantsContainer)
310    grok.name('statistics')
311    grok.require('waeup.viewApplicationStatistics')
312    grok.template('applicantcontainerstatistics')
313
314    @property
315    def label(self):
316        return "%s" % self.context.title
317
318class ApplicantsContainerPage(KofaDisplayFormPage):
319    """The standard view for regular applicant containers.
320    """
321    grok.context(IApplicantsContainer)
322    grok.name('index')
323    grok.require('waeup.Public')
324    grok.template('applicantscontainerpage')
325    pnav = 3
326
327    @property
328    def form_fields(self):
329        form_fields = grok.AutoFields(IApplicantsContainer).omit('title')
330        form_fields['description'].custom_widget = HTMLDisplayWidget
331        form_fields[
332            'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
333        form_fields[
334            'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
335        if self.request.principal.id == 'zope.anybody':
336            form_fields = form_fields.omit(
337                'code', 'prefix', 'year', 'mode', 'hidden',
338                'strict_deadline', 'application_category')
339        return form_fields
340
341    @property
342    def introduction(self):
343        # Here we know that the cookie has been set
344        lang = self.request.cookies.get('kofa.language')
345        html = self.context.description_dict.get(lang,'')
346        if html == '':
347            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
348            html = self.context.description_dict.get(portal_language,'')
349        return html
350
351    @property
352    def label(self):
353        return "%s" % self.context.title
354
355class ApplicantsContainerManageFormPage(KofaEditFormPage):
356    grok.context(IApplicantsContainer)
357    grok.name('manage')
358    grok.template('applicantscontainermanagepage')
359    form_fields = grok.AutoFields(IApplicantsContainer)
360    taboneactions = [_('Save'),_('Cancel')]
361    tabtwoactions = [_('Remove selected'),_('Cancel'),
362        _('Create students from selected')]
363    tabthreeactions1 = [_('Remove selected local roles')]
364    tabthreeactions2 = [_('Add local role')]
365    # Use friendlier date widget...
366    grok.require('waeup.manageApplication')
367
368    @property
369    def label(self):
370        return _('Manage applicants container')
371
372    pnav = 3
373
374    @property
375    def showApplicants(self):
376        if len(self.context) < 5000:
377            return True
378        return False
379
380    def update(self):
381        datepicker.need() # Enable jQuery datepicker in date fields.
382        tabs.need()
383        toggleall.need()
384        self.tab1 = self.tab2 = self.tab3 = ''
385        qs = self.request.get('QUERY_STRING', '')
386        if not qs:
387            qs = 'tab1'
388        setattr(self, qs, 'active')
389        warning.need()
390        datatable.need()  # Enable jQurey datatables for contents listing
391        return super(ApplicantsContainerManageFormPage, self).update()
392
393    def getLocalRoles(self):
394        roles = ILocalRolesAssignable(self.context)
395        return roles()
396
397    def getUsers(self):
398        """Get a list of all users.
399        """
400        for key, val in grok.getSite()['users'].items():
401            url = self.url(val)
402            yield(dict(url=url, name=key, val=val))
403
404    def getUsersWithLocalRoles(self):
405        return get_users_with_local_roles(self.context)
406
407    def _description(self):
408        view = ApplicantsContainerPage(
409            self.context,self.request)
410        view.setUpWidgets()
411        return view.widgets['description']()
412
413    @action(_('Save'), style='primary')
414    def save(self, **data):
415        changed_fields = self.applyData(self.context, **data)
416        if changed_fields:
417            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
418        else:
419            changed_fields = []
420        self.context.description_dict = self._description()
421        # Always refresh title. So we can change titles
422        # if APP_TYPES_DICT has been edited.
423        apptypes_dict = getUtility(IApplicantsUtils).APP_TYPES_DICT
424        title = apptypes_dict[self.context.prefix][0]
425        #self.context.title = u'%s %s/%s' % (
426        #    title, self.context.year, self.context.year + 1)
427        self.flash(_('Form has been saved.'))
428        fields_string = ' + '.join(changed_fields)
429        self.context.writeLogMessage(self, 'saved: % s' % fields_string)
430        return
431
432    @jsaction(_('Remove selected'))
433    def delApplicant(self, **data):
434        form = self.request.form
435        if 'val_id' in form:
436            child_id = form['val_id']
437        else:
438            self.flash(_('No applicant selected!'))
439            self.redirect(self.url(self.context, '@@manage')+'?tab2')
440            return
441        if not isinstance(child_id, list):
442            child_id = [child_id]
443        deleted = []
444        for id in child_id:
445            try:
446                del self.context[id]
447                deleted.append(id)
448            except:
449                self.flash(_('Could not delete:') + ' %s: %s: %s' % (
450                        id, sys.exc_info()[0], sys.exc_info()[1]))
451        if len(deleted):
452            self.flash(_('Successfully removed: ${a}',
453                mapping = {'a':', '.join(deleted)}))
454        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
455        return
456
457    @action(_('Create students from selected'))
458    def createStudents(self, **data):
459        form = self.request.form
460        if 'val_id' in form:
461            child_id = form['val_id']
462        else:
463            self.flash(_('No applicant selected!'))
464            self.redirect(self.url(self.context, '@@manage')+'?tab2')
465            return
466        if not isinstance(child_id, list):
467            child_id = [child_id]
468        created = []
469        for id in child_id:
470            success, msg = self.context[id].createStudent(view=self)
471            if success:
472                created.append(id)
473        if len(created):
474            self.flash(_('${a} students successfully created.',
475                mapping = {'a': len(created)}))
476        else:
477            self.flash(_('No student could be created.'))
478        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
479        return
480
481    @action(_('Cancel'), validator=NullValidator)
482    def cancel(self, **data):
483        self.redirect(self.url(self.context))
484        return
485
486    @action(_('Add local role'), validator=NullValidator)
487    def addLocalRole(self, **data):
488        return add_local_role(self,3, **data)
489
490    @action(_('Remove selected local roles'))
491    def delLocalRoles(self, **data):
492        return del_local_roles(self,3,**data)
493
494class ApplicantAddFormPage(KofaAddFormPage):
495    """Add-form to add an applicant.
496    """
497    grok.context(IApplicantsContainer)
498    grok.require('waeup.manageApplication')
499    grok.name('addapplicant')
500    #grok.template('applicantaddpage')
501    form_fields = grok.AutoFields(IApplicant).select(
502        'firstname', 'middlename', 'lastname',
503        'email', 'phone')
504    label = _('Add applicant')
505    pnav = 3
506
507    @action(_('Create application record'))
508    def addApplicant(self, **data):
509        applicant = createObject(u'waeup.Applicant')
510        self.applyData(applicant, **data)
511        self.context.addApplicant(applicant)
512        self.flash(_('Applicant record created.'))
513        self.redirect(
514            self.url(self.context[applicant.application_number], 'index'))
515        return
516
517class ApplicantDisplayFormPage(KofaDisplayFormPage):
518    """A display view for applicant data.
519    """
520    grok.context(IApplicant)
521    grok.name('index')
522    grok.require('waeup.viewApplication')
523    grok.template('applicantdisplaypage')
524    label = _('Applicant')
525    pnav = 3
526    hide_hint = False
527
528    @property
529    def form_fields(self):
530        if self.context.special:
531            form_fields = grok.AutoFields(ISpecialApplicant)
532        else:
533            form_fields = grok.AutoFields(IApplicant).omit(
534                'locked', 'course_admitted', 'password', 'suspended')
535        return form_fields
536
537    @property
538    def target(self):
539        return getattr(self.context.__parent__, 'prefix', None)
540
541    @property
542    def separators(self):
543        return getUtility(IApplicantsUtils).SEPARATORS_DICT
544
545    def update(self):
546        self.passport_url = self.url(self.context, 'passport.jpg')
547        # Mark application as started if applicant logs in for the first time
548        usertype = getattr(self.request.principal, 'user_type', None)
549        if usertype == 'applicant' and \
550            IWorkflowState(self.context).getState() == INITIALIZED:
551            IWorkflowInfo(self.context).fireTransition('start')
552        if usertype == 'applicant' and self.context.state == 'created':
553            msg = _(
554                '\n <strong>Congratulations!</strong>' +
555                ' You have been provisionally admitted' +
556                ' and your student record has been created for you.' +
557                ' Logout again and proceed to the login page of the portal.' +
558                ' Then enter your new student credentials:' +
559                ' user name= ${a}, password = ${b}.' +
560                ' Change your password when you have logged in.',
561                mapping = {
562                    'a':self.context.student_id,
563                    'b':self.context.application_number}
564                )
565            self.flash(msg)
566        return
567
568    @property
569    def hasPassword(self):
570        if self.context.password:
571            return _('set')
572        return _('unset')
573
574    @property
575    def label(self):
576        container_title = self.context.__parent__.title
577        return _('${a} <br /> Application Record ${b}', mapping = {
578            'a':container_title, 'b':self.context.application_number})
579
580    def getCourseAdmitted(self):
581        """Return link, title and code in html format to the certificate
582           admitted.
583        """
584        course_admitted = self.context.course_admitted
585        if getattr(course_admitted, '__parent__',None):
586            url = self.url(course_admitted)
587            title = course_admitted.title
588            code = course_admitted.code
589            return '<a href="%s">%s - %s</a>' %(url,code,title)
590        return ''
591
592class ApplicantBaseDisplayFormPage(ApplicantDisplayFormPage):
593    grok.context(IApplicant)
594    grok.name('base')
595    form_fields = grok.AutoFields(IApplicant).select(
596        'applicant_id','email', 'course1')
597
598class CreateStudentPage(UtilityView, grok.View):
599    """Create a student object from applicant data.
600    """
601    grok.context(IApplicant)
602    grok.name('createstudent')
603    grok.require('waeup.manageStudent')
604
605    def update(self):
606        msg = self.context.createStudent(view=self)[1]
607        self.flash(msg)
608        self.redirect(self.url(self.context))
609        return
610
611    def render(self):
612        return
613
614class CreateAllStudentsPage(UtilityView, grok.View):
615    """Create all student objects from applicant data
616    in a container.
617
618    This is a hidden page, no link or button will
619    be provided and only PortalManagers can do this.
620    """
621    #grok.context(IApplicantsContainer)
622    grok.name('createallstudents')
623    grok.require('waeup.managePortal')
624
625    def update(self):
626        cat = getUtility(ICatalog, name='applicants_catalog')
627        results = list(cat.searchResults(state=(ADMITTED, ADMITTED)))
628        created = []
629        container_only = False
630        applicants_root = grok.getSite()['applicants']
631        if isinstance(self.context, ApplicantsContainer):
632            container_only = True
633        for result in results:
634            if container_only and result.__parent__ is not self.context:
635                continue
636            success, msg = result.createStudent(view=self)
637            if success:
638                created.append(result.applicant_id)
639            else:
640                ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
641                applicants_root.logger.info(
642                    '%s - %s - %s' % (ob_class, result.applicant_id, msg))
643        if len(created):
644            self.flash(_('${a} students successfully created.',
645                mapping = {'a': len(created)}))
646        else:
647            self.flash(_('No student could be created.'))
648        self.redirect(self.url(self.context))
649        return
650
651    def render(self):
652        return
653
654class ApplicationFeePaymentAddPage(UtilityView, grok.View):
655    """ Page to add an online payment ticket
656    """
657    grok.context(IApplicant)
658    grok.name('addafp')
659    grok.require('waeup.payApplicant')
660    factory = u'waeup.ApplicantOnlinePayment'
661
662    def update(self):
663        for key in self.context.keys():
664            ticket = self.context[key]
665            if ticket.p_state == 'paid':
666                  self.flash(
667                      _('This type of payment has already been made.'))
668                  self.redirect(self.url(self.context))
669                  return
670        applicants_utils = getUtility(IApplicantsUtils)
671        container = self.context.__parent__
672        payment = createObject(self.factory)
673        error = applicants_utils.setPaymentDetails(
674            container, payment, self.context)
675        if error is not None:
676            self.flash(error)
677            self.redirect(self.url(self.context))
678            return
679        self.context[payment.p_id] = payment
680        self.flash(_('Payment ticket created.'))
681        self.redirect(self.url(payment))
682        return
683
684    def render(self):
685        return
686
687
688class OnlinePaymentDisplayFormPage(KofaDisplayFormPage):
689    """ Page to view an online payment ticket
690    """
691    grok.context(IApplicantOnlinePayment)
692    grok.name('index')
693    grok.require('waeup.viewApplication')
694    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
695    form_fields[
696        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
697    form_fields[
698        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
699    pnav = 3
700
701    @property
702    def label(self):
703        return _('${a}: Online Payment Ticket ${b}', mapping = {
704            'a':self.context.__parent__.display_fullname,
705            'b':self.context.p_id})
706
707class OnlinePaymentApprovePage(UtilityView, grok.View):
708    """ Approval view
709    """
710    grok.context(IApplicantOnlinePayment)
711    grok.name('approve')
712    grok.require('waeup.managePortal')
713
714    def update(self):
715        success, msg, log = self.context.approveApplicantPayment()
716        if log is not None:
717            applicant = self.context.__parent__
718            # Add log message to applicants.log
719            applicant.writeLogMessage(self, log)
720            # Add log message to payments.log
721            self.context.logger.info(
722                '%s,%s,%s,%s,%s,,,,,,' % (
723                applicant.applicant_id,
724                self.context.p_id, self.context.p_category,
725                self.context.amount_auth, self.context.r_code))
726        self.flash(msg)
727        return
728
729    def render(self):
730        self.redirect(self.url(self.context, '@@index'))
731        return
732
733class ExportPDFPaymentSlipPage(UtilityView, grok.View):
734    """Deliver a PDF slip of the context.
735    """
736    grok.context(IApplicantOnlinePayment)
737    grok.name('payment_slip.pdf')
738    grok.require('waeup.viewApplication')
739    form_fields = grok.AutoFields(IApplicantOnlinePayment).omit('p_item')
740    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
741    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
742    prefix = 'form'
743    note = None
744
745    @property
746    def title(self):
747        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
748        return translate(_('Payment Data'), 'waeup.kofa',
749            target_language=portal_language)
750
751    @property
752    def label(self):
753        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
754        return translate(_('Online Payment Slip'),
755            'waeup.kofa', target_language=portal_language) \
756            + ' %s' % self.context.p_id
757
758    def render(self):
759        #if self.context.p_state != 'paid':
760        #    self.flash(_('Ticket not yet paid.'))
761        #    self.redirect(self.url(self.context))
762        #    return
763        applicantview = ApplicantBaseDisplayFormPage(self.context.__parent__,
764            self.request)
765        students_utils = getUtility(IStudentsUtils)
766        return students_utils.renderPDF(self,'payment_slip.pdf',
767            self.context.__parent__, applicantview, note=self.note)
768
769class ExportPDFPageApplicationSlip(UtilityView, grok.View):
770    """Deliver a PDF slip of the context.
771    """
772    grok.context(IApplicant)
773    grok.name('application_slip.pdf')
774    grok.require('waeup.viewApplication')
775    prefix = 'form'
776
777    def update(self):
778        if self.context.state in ('initialized', 'started', 'paid'):
779            self.flash(
780                _('Please pay and submit before trying to download the application slip.'))
781            return self.redirect(self.url(self.context))
782        return
783
784    def render(self):
785        pdfstream = getAdapter(self.context, IPDF, name='application_slip')(
786            view=self)
787        self.response.setHeader(
788            'Content-Type', 'application/pdf')
789        return pdfstream
790
791def handle_img_upload(upload, context, view):
792    """Handle upload of applicant image.
793
794    Returns `True` in case of success or `False`.
795
796    Please note that file pointer passed in (`upload`) most probably
797    points to end of file when leaving this function.
798    """
799    size = file_size(upload)
800    if size > MAX_UPLOAD_SIZE:
801        view.flash(_('Uploaded image is too big!'))
802        return False
803    dummy, ext = os.path.splitext(upload.filename)
804    ext.lower()
805    if ext != '.jpg':
806        view.flash(_('jpg file extension expected.'))
807        return False
808    upload.seek(0) # file pointer moved when determining size
809    store = getUtility(IExtFileStore)
810    file_id = IFileStoreNameChooser(context).chooseName()
811    store.createFile(file_id, upload)
812    return True
813
814class ApplicantManageFormPage(KofaEditFormPage):
815    """A full edit view for applicant data.
816    """
817    grok.context(IApplicant)
818    grok.name('manage')
819    grok.require('waeup.manageApplication')
820    grok.template('applicanteditpage')
821    manage_applications = True
822    pnav = 3
823    display_actions = [[_('Save'), _('Final Submit')],
824        [_('Add online payment ticket'),_('Remove selected tickets')]]
825
826    @property
827    def form_fields(self):
828        if self.context.special:
829            form_fields = grok.AutoFields(ISpecialApplicant)
830            form_fields['applicant_id'].for_display = True
831        else:
832            form_fields = grok.AutoFields(IApplicant)
833            form_fields['student_id'].for_display = True
834            form_fields['applicant_id'].for_display = True
835        return form_fields
836
837    @property
838    def target(self):
839        return getattr(self.context.__parent__, 'prefix', None)
840
841    @property
842    def separators(self):
843        return getUtility(IApplicantsUtils).SEPARATORS_DICT
844
845    def update(self):
846        datepicker.need() # Enable jQuery datepicker in date fields.
847        warning.need()
848        super(ApplicantManageFormPage, self).update()
849        self.wf_info = IWorkflowInfo(self.context)
850        self.max_upload_size = string_from_bytes(MAX_UPLOAD_SIZE)
851        self.upload_success = None
852        upload = self.request.form.get('form.passport', None)
853        if upload:
854            # We got a fresh upload, upload_success is
855            # either True or False
856            self.upload_success = handle_img_upload(
857                upload, self.context, self)
858            if self.upload_success:
859                self.context.writeLogMessage(self, 'saved: passport')
860        return
861
862    @property
863    def label(self):
864        container_title = self.context.__parent__.title
865        return _('${a} <br /> Application Form ${b}', mapping = {
866            'a':container_title, 'b':self.context.application_number})
867
868    def getTransitions(self):
869        """Return a list of dicts of allowed transition ids and titles.
870
871        Each list entry provides keys ``name`` and ``title`` for
872        internal name and (human readable) title of a single
873        transition.
874        """
875        allowed_transitions = [t for t in self.wf_info.getManualTransitions()
876            if not t[0] == 'pay']
877        return [dict(name='', title=_('No transition'))] +[
878            dict(name=x, title=y) for x, y in allowed_transitions]
879
880    @action(_('Save'), style='primary')
881    def save(self, **data):
882        form = self.request.form
883        password = form.get('password', None)
884        password_ctl = form.get('control_password', None)
885        if password:
886            validator = getUtility(IPasswordValidator)
887            errors = validator.validate_password(password, password_ctl)
888            if errors:
889                self.flash( ' '.join(errors))
890                return
891        if self.upload_success is False:  # False is not None!
892            # Error during image upload. Ignore other values.
893            return
894        changed_fields = self.applyData(self.context, **data)
895        # Turn list of lists into single list
896        if changed_fields:
897            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
898        else:
899            changed_fields = []
900        if password:
901            # Now we know that the form has no errors and can set password ...
902            IUserAccount(self.context).setPassword(password)
903            changed_fields.append('password')
904        fields_string = ' + '.join(changed_fields)
905        trans_id = form.get('transition', None)
906        if trans_id:
907            self.wf_info.fireTransition(trans_id)
908        self.flash(_('Form has been saved.'))
909        if fields_string:
910            self.context.writeLogMessage(self, 'saved: % s' % fields_string)
911        return
912
913    def unremovable(self, ticket):
914        return False
915
916    # This method is also used by the ApplicantEditFormPage
917    def delPaymentTickets(self, **data):
918        form = self.request.form
919        if 'val_id' in form:
920            child_id = form['val_id']
921        else:
922            self.flash(_('No payment selected.'))
923            self.redirect(self.url(self.context))
924            return
925        if not isinstance(child_id, list):
926            child_id = [child_id]
927        deleted = []
928        for id in child_id:
929            # Applicants are not allowed to remove used payment tickets
930            if not self.unremovable(self.context[id]):
931                try:
932                    del self.context[id]
933                    deleted.append(id)
934                except:
935                    self.flash(_('Could not delete:') + ' %s: %s: %s' % (
936                            id, sys.exc_info()[0], sys.exc_info()[1]))
937        if len(deleted):
938            self.flash(_('Successfully removed: ${a}',
939                mapping = {'a':', '.join(deleted)}))
940            self.context.writeLogMessage(
941                self, 'removed: % s' % ', '.join(deleted))
942        return
943
944    # We explicitely want the forms to be validated before payment tickets
945    # can be created. If no validation is requested, use
946    # 'validator=NullValidator' in the action directive
947    @action(_('Add online payment ticket'))
948    def addPaymentTicket(self, **data):
949        self.redirect(self.url(self.context, '@@addafp'))
950        return
951
952    @jsaction(_('Remove selected tickets'))
953    def removePaymentTickets(self, **data):
954        self.delPaymentTickets(**data)
955        self.redirect(self.url(self.context) + '/@@manage')
956        return
957
958    # Not used in base package
959    def file_exists(self, attr):
960        file = getUtility(IExtFileStore).getFileByContext(
961            self.context, attr=attr)
962        if file:
963            return True
964        else:
965            return False
966
967class ApplicantEditFormPage(ApplicantManageFormPage):
968    """An applicant-centered edit view for applicant data.
969    """
970    grok.context(IApplicantEdit)
971    grok.name('edit')
972    grok.require('waeup.handleApplication')
973    grok.template('applicanteditpage')
974    manage_applications = False
975    submit_state = PAID
976
977    @property
978    def form_fields(self):
979        if self.context.special:
980            form_fields = grok.AutoFields(ISpecialApplicant)
981            form_fields['applicant_id'].for_display = True
982        else:
983            form_fields = grok.AutoFields(IApplicantEdit).omit(
984                'locked', 'course_admitted', 'student_id',
985                'suspended'
986                )
987            form_fields['applicant_id'].for_display = True
988            form_fields['reg_number'].for_display = True
989        return form_fields
990
991    @property
992    def display_actions(self):
993        state = IWorkflowState(self.context).getState()
994        actions = [[],[]]
995        if state == STARTED:
996            actions = [[_('Save')],
997                [_('Add online payment ticket'),_('Remove selected tickets')]]
998        elif state == PAID:
999            actions = [[_('Save'), _('Final Submit')],
1000                [_('Remove selected tickets')]]
1001        return actions
1002
1003    def unremovable(self, ticket):
1004        state = IWorkflowState(self.context).getState()
1005        return ticket.r_code or state in (INITIALIZED, SUBMITTED)
1006
1007    def emit_lock_message(self):
1008        self.flash(_('The requested form is locked (read-only).'))
1009        self.redirect(self.url(self.context))
1010        return
1011
1012    def update(self):
1013        if self.context.locked or (
1014            self.context.__parent__.expired and
1015            self.context.__parent__.strict_deadline):
1016            self.emit_lock_message()
1017            return
1018        super(ApplicantEditFormPage, self).update()
1019        return
1020
1021    def dataNotComplete(self):
1022        store = getUtility(IExtFileStore)
1023        if not store.getFileByContext(self.context, attr=u'passport.jpg'):
1024            return _('No passport picture uploaded.')
1025        if not self.request.form.get('confirm_passport', False):
1026            return _('Passport picture confirmation box not ticked.')
1027        return False
1028
1029    # We explicitely want the forms to be validated before payment tickets
1030    # can be created. If no validation is requested, use
1031    # 'validator=NullValidator' in the action directive
1032    @action(_('Add online payment ticket'))
1033    def addPaymentTicket(self, **data):
1034        self.redirect(self.url(self.context, '@@addafp'))
1035        return
1036
1037    @jsaction(_('Remove selected tickets'))
1038    def removePaymentTickets(self, **data):
1039        self.delPaymentTickets(**data)
1040        self.redirect(self.url(self.context) + '/@@edit')
1041        return
1042
1043    @action(_('Save'), style='primary')
1044    def save(self, **data):
1045        if self.upload_success is False:  # False is not None!
1046            # Error during image upload. Ignore other values.
1047            return
1048        if data.get('course1', 1) == data.get('course2', 2):
1049            self.flash(_('1st and 2nd choice must be different.'))
1050            return
1051        self.applyData(self.context, **data)
1052        self.flash(_('Form has been saved.'))
1053        return
1054
1055    @submitaction(_('Final Submit'))
1056    def finalsubmit(self, **data):
1057        if self.upload_success is False:  # False is not None!
1058            return # error during image upload. Ignore other values
1059        if self.dataNotComplete():
1060            self.flash(self.dataNotComplete())
1061            return
1062        self.applyData(self.context, **data)
1063        state = IWorkflowState(self.context).getState()
1064        # This shouldn't happen, but the application officer
1065        # might have forgotten to lock the form after changing the state
1066        if state != self.submit_state:
1067            self.flash(_('The form cannot be submitted. Wrong state!'))
1068            return
1069        IWorkflowInfo(self.context).fireTransition('submit')
1070        # application_date is used in export files for sorting.
1071        # We can thus store utc.
1072        self.context.application_date = datetime.utcnow()
1073        self.flash(_('Form has been submitted.'))
1074        self.redirect(self.url(self.context))
1075        return
1076
1077class PassportImage(grok.View):
1078    """Renders the passport image for applicants.
1079    """
1080    grok.name('passport.jpg')
1081    grok.context(IApplicant)
1082    grok.require('waeup.viewApplication')
1083
1084    def render(self):
1085        # A filename chooser turns a context into a filename suitable
1086        # for file storage.
1087        image = getUtility(IExtFileStore).getFileByContext(self.context)
1088        self.response.setHeader(
1089            'Content-Type', 'image/jpeg')
1090        if image is None:
1091            # show placeholder image
1092            return open(DEFAULT_PASSPORT_IMAGE_PATH, 'rb').read()
1093        return image
1094
1095class ApplicantRegistrationPage(KofaAddFormPage):
1096    """Captcha'd registration page for applicants.
1097    """
1098    grok.context(IApplicantsContainer)
1099    grok.name('register')
1100    grok.require('waeup.Anonymous')
1101    grok.template('applicantregister')
1102
1103    @property
1104    def form_fields(self):
1105        form_fields = None
1106        if self.context.mode == 'update':
1107            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
1108                'firstname','reg_number','email')
1109        else: #if self.context.mode == 'create':
1110            form_fields = grok.AutoFields(IApplicantEdit).select(
1111                'firstname', 'middlename', 'lastname', 'email', 'phone')
1112        return form_fields
1113
1114    @property
1115    def label(self):
1116        return _('Apply for ${a}',
1117            mapping = {'a':self.context.title})
1118
1119    def update(self):
1120        if self.context.expired:
1121            self.flash(_('Outside application period.'))
1122            self.redirect(self.url(self.context))
1123            return
1124        # Handle captcha
1125        self.captcha = getUtility(ICaptchaManager).getCaptcha()
1126        self.captcha_result = self.captcha.verify(self.request)
1127        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
1128        return
1129
1130    def _redirect(self, email, password, applicant_id):
1131        # Forward only email to landing page in base package.
1132        self.redirect(self.url(self.context, 'registration_complete',
1133            data = dict(email=email)))
1134        return
1135
1136    @action(_('Send login credentials to email address'), style='primary')
1137    def register(self, **data):
1138        if not self.captcha_result.is_valid:
1139            # Captcha will display error messages automatically.
1140            # No need to flash something.
1141            return
1142        if self.context.mode == 'create':
1143            # Add applicant
1144            applicant = createObject(u'waeup.Applicant')
1145            self.applyData(applicant, **data)
1146            self.context.addApplicant(applicant)
1147            applicant.reg_number = applicant.applicant_id
1148            notify(grok.ObjectModifiedEvent(applicant))
1149        elif self.context.mode == 'update':
1150            # Update applicant
1151            reg_number = data.get('reg_number','')
1152            firstname = data.get('firstname','')
1153            cat = getUtility(ICatalog, name='applicants_catalog')
1154            results = list(
1155                cat.searchResults(reg_number=(reg_number, reg_number)))
1156            if results:
1157                applicant = results[0]
1158                if getattr(applicant,'firstname',None) is None:
1159                    self.flash(_('An error occurred.'))
1160                    return
1161                elif applicant.firstname.lower() != firstname.lower():
1162                    # Don't tell the truth here. Anonymous must not
1163                    # know that a record was found and only the firstname
1164                    # verification failed.
1165                    self.flash(_('No application record found.'))
1166                    return
1167                elif applicant.password is not None and \
1168                    applicant.state != INITIALIZED:
1169                    self.flash(_('Your password has already been set and used. '
1170                                 'Please proceed to the login page.'))
1171                    return
1172                # Store email address but nothing else.
1173                applicant.email = data['email']
1174                notify(grok.ObjectModifiedEvent(applicant))
1175            else:
1176                # No record found, this is the truth.
1177                self.flash(_('No application record found.'))
1178                return
1179        else:
1180            # Does not happen but anyway ...
1181            return
1182        kofa_utils = getUtility(IKofaUtils)
1183        password = kofa_utils.genPassword()
1184        IUserAccount(applicant).setPassword(password)
1185        # Send email with credentials
1186        login_url = self.url(grok.getSite(), 'login')
1187        url_info = u'Login: %s' % login_url
1188        msg = _('You have successfully been registered for the')
1189        if kofa_utils.sendCredentials(IUserAccount(applicant),
1190            password, url_info, msg):
1191            email_sent = applicant.email
1192        else:
1193            email_sent = None
1194        self._redirect(email=email_sent, password=password,
1195            applicant_id=applicant.applicant_id)
1196        return
1197
1198class ApplicantRegistrationEmailSent(KofaPage):
1199    """Landing page after successful registration.
1200
1201    """
1202    grok.name('registration_complete')
1203    grok.require('waeup.Public')
1204    grok.template('applicantregemailsent')
1205    label = _('Your registration was successful.')
1206
1207    def update(self, email=None, applicant_id=None, password=None):
1208        self.email = email
1209        self.password = password
1210        self.applicant_id = applicant_id
1211        return
1212
1213class ExportJobContainerOverview(KofaPage):
1214    """Page that lists active applicant data export jobs and provides links
1215    to discard or download CSV files.
1216
1217    """
1218    grok.context(VirtualApplicantsExportJobContainer)
1219    grok.require('waeup.manageApplication')
1220    grok.name('index.html')
1221    grok.template('exportjobsindex')
1222    label = _('Applicant Data Exports')
1223    pnav = 3
1224
1225    def update(self, CREATE=None, DISCARD=None, job_id=None):
1226        if CREATE:
1227            self.redirect(self.url('@@start_export'))
1228            return
1229        if DISCARD and job_id:
1230            entry = self.context.entry_from_job_id(job_id)
1231            self.context.delete_export_entry(entry)
1232            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1233            self.context.logger.info(
1234                '%s - discarded: job_id=%s' % (ob_class, job_id))
1235            self.flash(_('Discarded export') + ' %s' % job_id)
1236        self.entries = doll_up(self, user=self.request.principal.id)
1237        return
1238
1239class ExportJobContainerJobStart(KofaPage):
1240    """Page that starts an applicants export job.
1241
1242    """
1243    grok.context(VirtualApplicantsExportJobContainer)
1244    grok.require('waeup.manageApplication')
1245    grok.name('start_export')
1246
1247    def update(self):
1248        exporter = 'applicants'
1249        container_code = self.context.__parent__.code
1250        job_id = self.context.start_export_job(exporter,
1251                                      self.request.principal.id,
1252                                      container=container_code)
1253
1254        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1255        self.context.logger.info(
1256            '%s - exported: %s (%s), job_id=%s'
1257            % (ob_class, exporter, container_code, job_id))
1258        self.flash(_('Export started.'))
1259        self.redirect(self.url(self.context))
1260        return
1261
1262    def render(self):
1263        return
1264
1265class ExportJobContainerDownload(ExportCSVView):
1266    """Page that downloads a students export csv file.
1267
1268    """
1269    grok.context(VirtualApplicantsExportJobContainer)
1270    grok.require('waeup.manageApplication')
Note: See TracBrowser for help on using the repository browser.