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

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

Adjust student base data section on slips. Display certificate code and full name.

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