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

Last change on this file since 11433 was 11254, checked in by uli, 11 years ago

Merge changes from uli-diazo-themed back into trunk. If this works, then a miracle happened.

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