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

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

Implement CreateAllStudentsPage? which creates students from all admitted students in a container.

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