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

Last change on this file since 8860 was 8853, checked in by Henrik Bettermann, 13 years ago

StudentRequestPasswordPage?: Create mandate and send link by email instead of setting password directly.

Show link to StudentRequestPasswordPage? on login page.

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