source: main/kofacustom.iuokada/trunk/src/kofacustom/iuokada/students/browser.py @ 17861

Last change on this file since 17861 was 17854, checked in by Henrik Bettermann, 4 months ago
  • Property svn:keywords set to Id
File size: 22.2 KB
RevLine 
[10765]1## $Id: browser.py 17854 2024-07-18 12:21:07Z henrik $
2##
3## Copyright (C) 2012 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##
18import grok
[16160]19import os
[10765]20from zope.i18n import translate
21from zope.schema.interfaces import ConstraintNotSatisfied
22from zope.component import getUtility
[16499]23from zope.security import checkPermission
[16249]24from zope.formlib.textwidgets import BytesDisplayWidget
[10765]25from hurry.workflow.interfaces import IWorkflowInfo
[16623]26from waeup.kofa.interfaces import (
27    REQUESTED, ADMITTED, CLEARANCE, REQUESTED, CLEARED,
[16259]28    IExtFileStore, IKofaUtils, academic_sessions_vocab)
[10765]29from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
[15802]30from waeup.kofa.browser.layout import (
31    action, jsaction, UtilityView, KofaEditFormPage)
[10765]32from waeup.kofa.students.browser import (
33    StudyLevelEditFormPage, StudyLevelDisplayFormPage,
[13062]34    StudentBasePDFFormPage, ExportPDFCourseRegistrationSlip,
[10765]35    CourseTicketDisplayFormPage, StudentTriggerTransitionFormPage,
[16015]36    StartClearancePage, BalancePaymentAddFormPage,
[16252]37    ExportPDFAdmissionSlip, ExportPDFPersonalDataSlip,
[16499]38    PaymentsManageFormPage,
[10765]39    msave, emit_lock_message)
[15802]40from waeup.kofa.students.interfaces import (
41    IStudentsUtils, ICourseTicket, IStudent)
[16127]42from waeup.kofa.students.vocabularies import StudyLevelSource
[10765]43from waeup.kofa.students.workflow import FORBIDDEN_POSTGRAD_TRANS
44from kofacustom.nigeria.students.browser import (
45    NigeriaOnlinePaymentDisplayFormPage,
[15704]46    NigeriaStudentBaseDisplayFormPage,
[10765]47    NigeriaStudentBaseManageFormPage,
48    NigeriaStudentClearanceEditFormPage,
49    NigeriaOnlinePaymentAddFormPage,
[13062]50    NigeriaExportPDFPaymentSlip,
51    NigeriaExportPDFClearanceSlip,
[15696]52    NigeriaExportPDFCourseRegistrationSlip,
[15704]53    NigeriaStudentBaseEditFormPage,
[15722]54    NigeriaBedTicketAddPage,
[15999]55    NigeriaAccommodationManageFormPage,
56    NigeriaAccommodationDisplayFormPage,
[16249]57    NigeriaStudentPersonalManageFormPage,
58    NigeriaStudentPersonalEditFormPage,
59    NigeriaStudentPersonalDisplayFormPage
[10765]60    )
[15563]61from kofacustom.iuokada.students.interfaces import (
[10765]62    ICustomStudentOnlinePayment, ICustomStudentStudyCourse,
[16249]63    ICustomStudentStudyLevel, ICustomStudentBase, ICustomStudent,
64    ICustomStudentPersonal, ICustomStudentPersonalEdit)
[15563]65from kofacustom.iuokada.interfaces import MessageFactory as _
[10765]66
[15704]67class CustomStudentBaseDisplayFormPage(NigeriaStudentBaseDisplayFormPage):
68    """ Page to display student base data
69    """
70    form_fields = grok.AutoFields(ICustomStudentBase).omit(
71        'password', 'suspended', 'suspended_comment', 'flash_notice')
72
73class CustomStudentBaseManageFormPage(NigeriaStudentBaseManageFormPage):
74    """ View to manage student base data
75    """
76    form_fields = grok.AutoFields(ICustomStudentBase).omit(
[15870]77        'student_id', 'adm_code', 'suspended',
78        'financially_cleared_by', 'financial_clearance_date')
[15704]79
80class StudentBaseEditFormPage(NigeriaStudentBaseEditFormPage):
81    """ View to edit student base data
82    """
[16259]83    @property
84    def form_fields(self):
85        form_fields = grok.AutoFields(ICustomStudentBase).select(
[16272]86            'email', 'email2', 'parents_email', 'phone',)
[16259]87        if not self.context.state in (ADMITTED, CLEARANCE):
88            form_fields['parents_email'].for_display = True
89        return form_fields
[15704]90
[15696]91class CustomExportPDFCourseRegistrationSlip(
92    NigeriaExportPDFCourseRegistrationSlip):
93    """Deliver a PDF slip of the context.
94    """
95
96    def _signatures(self):
97        return (
98                ['Student Signature'],
99                ['HoD / Course Adviser Signature'],
100                ['College Officer Signature'],
101                ['Dean Signature']
102                )
103
104    #def _sigsInFooter(self):
105    #    return (_('Student'),
106    #            _('HoD / Course Adviser'),
107    #            _('College Officer'),
108    #            _('Dean'),
109    #            )
[15720]110    #    return ()
111
[16249]112class CustomStudentPersonalDisplayFormPage(NigeriaStudentPersonalDisplayFormPage):
113    """ Page to display student personal data
114    """
115    form_fields = grok.AutoFields(ICustomStudentPersonal)
116    form_fields['perm_address'].custom_widget = BytesDisplayWidget
117    form_fields['postal_address'].custom_widget = BytesDisplayWidget
118    form_fields['hostel_address'].custom_widget = BytesDisplayWidget
119    form_fields['father_address'].custom_widget = BytesDisplayWidget
120    form_fields['mother_address'].custom_widget = BytesDisplayWidget
121    form_fields['guardian_address'].custom_widget = BytesDisplayWidget
122    form_fields[
123        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
124
125
126class CustomStudentPersonalEditFormPage(NigeriaStudentPersonalEditFormPage):
127    """ Page to edit personal data
128    """
[16259]129    form_fields = grok.AutoFields(ICustomStudentPersonalEdit).omit(
130        'personal_updated')
[16249]131
[16258]132    def update(self):
[16298]133        #if not self.context.is_fresh:
134        #    self.flash('Not allowed.', type="danger")
135        #    self.redirect(self.url(self.context))
136        #    return
[16409]137        if not self.context.minimumStudentPayments():
138            self.flash('Please make 40% of your tution fee payments first.',
[16264]139                       type="warning")
140            self.redirect(self.url(self.context, 'view_personal'))
141            return
[16258]142        super(CustomStudentPersonalEditFormPage, self).update()
143        return
144
145
[16249]146class CustomStudentPersonalManageFormPage(NigeriaStudentPersonalManageFormPage):
[16409]147    """ Page to manage personal data
[16249]148    """
149    form_fields = grok.AutoFields(ICustomStudentPersonal)
150    form_fields['personal_updated'].for_display = True
151    form_fields[
152        'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
153
[16252]154class CustomExportPDFPersonalDataSlip(ExportPDFPersonalDataSlip):
155    """Deliver a PDF base and personal data slip.
156    """
[16292]157    grok.name('course_registration_clearance.pdf')
[16252]158    omit_fields = (
159        'phone', 'email',
160        'suspended',
161        'adm_code', 'suspended_comment',
162        'current_level',
163        'flash_notice', 'entry_session',
164        'parents_email')
165
166    form_fields = grok.AutoFields(ICustomStudentPersonal)
167
[16292]168    def _signatures(self):
169        return ([('I certify that the above named student has satisfied the financial requirements for registration.', 'Name and Signature of Bursary Staff', '<br><br>')],
170                [('I certify that the credentials of the student have been screened by me and the student is hereby cleared.', 'Name and Signature of Registry Staff', '<br><br>')],
171                [('I certify that the above named student has registered with the Library.', 'Name and Signature of Library Staff', '<br><br>')],
172                [('I certify that the above named student has been registered with the college. ', 'Name and Signature of College Officer', '<br><br>')],
173                [('I certify that the above named student has completed his/her ICT registration. ', 'Name and Signature of ICT Staff', '<br><br>')],
174                [('Eligibility/Congratulation Station', 'Name and Signature of Registrar', '')],
175                )
176
[16296]177    @property
178    def tabletitle(self):
179        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
180        tabletitle = []
[16297]181        session = self.context.student.current_session
182        tabletitle.append('_PB_Successful %s/%s Session Payments' %(session, session+1))
[16296]183        return tabletitle
184
[16255]185    def render(self):
[16410]186        if not self.context.minimumStudentPayments():
187            self.redirect(self.url(self.context))
188            return
[16255]189        studentview = StudentBasePDFFormPage(self.context.student,
190            self.request, self.omit_fields)
191        students_utils = getUtility(IStudentsUtils)
192
[16296]193        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
194        P_ID = translate(_('Payment Id'), 'waeup.kofa', target_language=portal_language)
195        #CD = translate(_('Creation Date'), 'waeup.kofa', target_language=portal_language)
196        PD = translate(_('Payment Date'), 'waeup.kofa', target_language=portal_language)
197        CAT = translate(_('Payment Category'), 'waeup.kofa', target_language=portal_language)
198        ITEM = translate(_('Payment Item'), 'waeup.kofa', target_language=portal_language)
199        AMT = translate(_('Amount (Naira)'), 'waeup.kofa', target_language=portal_language)
200        SSS = translate(_('Payment Session'), 'waeup.kofa', target_language=portal_language)
201        tabledata = []
202        tableheader = []
203        tabledata.append(sorted(
204            [value for value in self.context['payments'].values()
205             if value.p_state in ('paid', 'waived', 'scholarship')
206             and value.p_session >= value.student.current_session],
207             key=lambda value: value.p_session))
208        tableheader.append([(P_ID,'p_id', 4.2),
209                         #(CD,'creation_date', 3),
210                         (PD,'formatted_p_date', 3),
211                         (CAT,'category', 3),
212                         (ITEM, 'p_item', 3),
213                         (AMT, 'amount_auth', 2),
214                         (SSS, 'p_session', 2),
215                         ])
216
[16292]217        #watermark_path = os.path.join(
218        #    os.path.dirname(__file__), 'static', 'watermark.pdf')
219        #watermark = open(watermark_path, 'rb')
220        #file_path = os.path.join(
221        #    os.path.dirname(__file__), 'static', 'biodataPage2.pdf')
222        #file = open(file_path, 'rb')
223        #mergefiles = [file,]
224
[16255]225        return students_utils.renderPDF(
[16292]226            self, 'course_registration_clearance.pdf',
[16255]227            self.context.student, studentview,
228            omit_fields=self.omit_fields,
[16292]229            signatures=self._signatures(),
[16296]230
231            tableheader=tableheader,
232            tabledata=tabledata,
233
[16292]234            pagebreak=True,
235        #    mergefiles=mergefiles,
236        #    watermark=watermark
237            )
[16255]238
[15999]239class CustomAccommodationDisplayFormPage(NigeriaAccommodationDisplayFormPage):
240    """ Page to view bed tickets.
241    """
242    with_hostel_selection = True
243
[15722]244class CustomAccommodationManageFormPage(NigeriaAccommodationManageFormPage):
245    """ Page to manage bed tickets.
246    This manage form page is for both students and students officers.
247    """
248    with_hostel_selection = True
249
[15720]250class CustomBedTicketAddPage(NigeriaBedTicketAddPage):
251    """ Page to add a bed ticket
252    """
253    with_ac = False
[15802]254    with_bedselection = True
255
[16499]256class CustomPaymentsManageFormPage(PaymentsManageFormPage):
257    """ Page to manage the student payments. This manage form page is for
258    both students and students officers. IUOkada does not allow students
259    to remove any payment ticket.
260    """
[16507]261    grok.template('paymentsmanagepage')
262
[17137]263    def _schoolfee_payments_made(self):
[16507]264        cs = self.context.student.current_session
[17572]265        es = self.context.student.entry_session
[16507]266        SF_PAYMENTS = ('schoolfee', 'schoolfee40', 'secondinstal', 'clearance')
[17137]267        sf_paid = dict()
[17546]268        try:
269            certificate = self.context.student['studycourse'].certificate
270        except (AttributeError, TypeError):
271            return sf_paid, 0
[17572]272        session = es
[17546]273        # Initiliaze sf_paid dict with items
[17572]274        # session: [school fee in session, amount paid in session, amount due]
[17546]275        sf_paid[session] = [getattr(certificate, 'school_fee_1', 0.0), 0.0, 0.0]
276        while session < cs + 1:
[17137]277            session += 1
[17546]278            sf_paid[session]= [getattr(certificate, 'school_fee_2', 0.0), 0.0, 0.0]
[17137]279        sessions = sf_paid.keys()
[16632]280        brought_fwd = 0.0
[17546]281        # Collect all payments made or brought forward
[16632]282        for ticket in self.context.values():
283            amt = ticket.net_amt
284            if not amt:
285                amt = ticket.amount_auth
[16507]286            if ticket.p_category in SF_PAYMENTS and \
287                ticket.p_state == 'paid' and \
[16627]288                ticket.p_session in sessions:
[17546]289                sf_paid[ticket.p_session][1] += amt
[16631]290            if ticket.p_state != 'paid' and\
291               ticket.p_category == 'brought_fwd':
292                  brought_fwd += ticket.amount_auth
[17572]293        # Calculate due, first for the entry session
294        # amount due in session =  brought forward from previous sessions + school fees in session - amount paid in session
295        session = es
[17574]296        try:
297            sf_paid[session][2] = brought_fwd + sf_paid[session][0] - sf_paid[session][1]
298        except TypeError:
299            return dict(), brought_fwd
[17572]300        while session < cs + 1:
301            session += 1
[17546]302            #   amount due in session =  brought forward from previous session + school fee in session - amount paid in session
303            sf_paid[session][2] = sf_paid[session-1][2] + sf_paid[session][0] - sf_paid[session][1]
304        return sorted(sf_paid.items(), key=lambda value: value[0]), brought_fwd
[16507]305
[17584]306    #def update(self):
307    #    super(CustomPaymentsManageFormPage, self).update()
308    #    self.sfp_made = self._schoolfee_payments_made()
309    #    return
[17137]310
311    @property
312    def manage_payments_allowed(self):
313        return checkPermission('waeup.manageStudent', self.context)
314
[15802]315class StudentGetMatricNumberPage(UtilityView, grok.View):
316    """ Construct and set the matriculation number.
317    """
318    grok.context(IStudent)
319    grok.name('get_matric_number')
[15805]320    grok.require('waeup.manageStudent')
[15802]321
322    def update(self):
323        students_utils = getUtility(IStudentsUtils)
324        msg, mnumber = students_utils.setMatricNumber(self.context)
325        if msg:
326            self.flash(msg, type="danger")
327        else:
328            self.flash(_('Matriculation number %s assigned.' % mnumber))
329            self.context.writeLogMessage(self, '%s assigned' % mnumber)
330        self.redirect(self.url(self.context))
331        return
332
333    def render(self):
[15859]334        return
335
336class SwitchLibraryAccessView(UtilityView, grok.View):
337    """ Switch the library attribute
338    """
339    grok.context(ICustomStudent)
340    grok.name('switch_library_access')
341    grok.require('waeup.switchLibraryAccess')
342
343    def update(self):
344        if self.context.library:
345            self.context.library = False
346            self.context.writeLogMessage(self, 'library access disabled')
347            self.flash(_('Library access disabled'))
348        else:
349            self.context.library = True
350            self.context.writeLogMessage(self, 'library access enabled')
351            self.flash(_('Library access enabled'))
352        self.redirect(self.url(self.context))
353        return
354
355    def render(self):
356        return
357
358class ExportLibIdCard(UtilityView, grok.View):
359    """Deliver an id card for the library.
360    """
361    grok.context(ICustomStudent)
362    grok.name('lib_idcard.pdf')
363    grok.require('waeup.viewStudent')
364    prefix = 'form'
365
366    label = u"Library Clearance"
367
368    omit_fields = (
369        'suspended', 'email', 'phone',
370        'adm_code', 'suspended_comment',
371        'date_of_birth',
372        'current_mode', 'certificate',
373        'entry_session',
374        'flash_notice')
375
376    form_fields = []
377
378    def _sigsInFooter(self):
379        isStudent = getattr(
380            self.request.principal, 'user_type', None) == 'student'
381        if isStudent:
382            return ''
383        return (_("Date, Reader's Signature"),
384                _("Date, Circulation Librarian's Signature"),
385                )
386
387    def update(self):
388        if not self.context.library:
389            self.flash(_('Forbidden!'), type="danger")
390            self.redirect(self.url(self.context))
391        return
392
393    @property
394    def note(self):
395        return """
396<br /><br /><br /><br /><font size='12'>
397This is to certify that the bearer whose photograph and other details appear
398 overleaf is a registered user of the <b>University Library</b>.
399 The card is not transferable. A replacement fee is charged for a loss,
400 mutilation or otherwise. If found, please, return to the University Library,
401 Igbinedion University, Okada.
402</font>
403
404"""
405        return
406
407    def render(self):
408        studentview = StudentBasePDFFormPage(self.context.student,
409            self.request, self.omit_fields)
410        students_utils = getUtility(IStudentsUtils)
411        return students_utils.renderPDF(
412            self, 'lib_idcard',
413            self.context.student, studentview,
414            omit_fields=self.omit_fields,
415            sigs_in_footer=self._sigsInFooter(),
[15937]416            note=self.note)
417
418class CustomStartClearancePage(StartClearancePage):
[16015]419    with_ac = False
420
421class CustomBalancePaymentAddFormPage(BalancePaymentAddFormPage):
[16127]422    grok.require('waeup.payStudent')
423
[17033]424from kofacustom.iuokada.students.admission_letter_ug import (
425    ADML_UG_1, ADML_UG_2, ADML_UG_3, ADML_UG_2_MEDICAL, ADML_UG_2_PHARMACY,
426    BASIC_MEDICAL_ONLY, BASIC_PHARMACY_ONLY)
427from kofacustom.iuokada.students.admission_letter_pg import ADML_PG
428from kofacustom.iuokada.students.admission_letter_pt import ADML_PT
429from kofacustom.iuokada.students.admission_letter_jupeb import ADML_JUPEB
[17679]430from kofacustom.iuokada.students.admission_letter_cdl import ADML_CDL
[17033]431
[16127]432class CustomExportPDFAdmissionSlip(ExportPDFAdmissionSlip):
433    """Deliver a PDF Admission slip.
434    """
435
[16238]436    omit_fields = ('date_of_birth',
437                   #'current_level',
[16489]438                   #'current_mode',
[16238]439                   #'entry_session'
440                   )
[16127]441
442    @property
443    def session(self):
444        return academic_sessions_vocab.getTerm(
445            self.context.entry_session).title
446
447    @property
448    def level(self):
449        studylevelsource = StudyLevelSource()
450        return studylevelsource.factory.getTitle(
451            self.context['studycourse'].certificate, self.context.current_level)
452
453    @property
454    def label(self):
455        return 'OFFER OF PROVISIONAL ADMISSION \nFOR %s SESSION' % self.session
456
457    @property
[16238]458    def pre_text_ug(self):
[16127]459        return (
460            'Following your performance in the screening exercise '
461            'for the %s academic session, I am pleased to inform '
462            'you that you have been offered provisional admission into the '
463            'Igbinedion University, Okada as follows:' % self.session)
464
465    @property
[16238]466    def pre_text_pg(self):
467        return (
468            'I am pleased to inform you that your application for admission'
469            ' into the Igbinedion University, Okada was successful. You have'
470            ' been admitted as follows:')
471
[17679]472    @property
473    def pre_text_cdl(self):
474        return (
475            'I am pleased to inform you that you have been offered provisional'
476            ' admission into the Igbinedion University, '
477            'Centre for Distance Learning as follows:')
478
[16127]479    def render(self):
480        students_utils = getUtility(IStudentsUtils)
[16160]481        watermark_path = os.path.join(
482            os.path.dirname(__file__), 'static', 'watermark.pdf')
483        watermark = open(watermark_path, 'rb')
[17679]484        if grok.getSite().__name__ == 'iuokada-cdl':
485            return students_utils.renderPDFAdmissionLetter(self,
486                self.context.student, omit_fields=self.omit_fields,
487                pre_text=self.pre_text_cdl, post_text=ADML_CDL,
488                watermark=watermark)
[16603]489        if self.context.is_jupeb:
490            return students_utils.renderPDFAdmissionLetter(self,
491                self.context.student, omit_fields=self.omit_fields,
[17033]492                pre_text=self.pre_text_ug, post_text=ADML_JUPEB,
[16603]493                watermark=watermark)
[16492]494        if self.context.current_mode.endswith('_pt'):
495            return students_utils.renderPDFAdmissionLetter(self,
496                self.context.student, omit_fields=self.omit_fields,
[17033]497                pre_text=self.pre_text_ug, post_text=ADML_PT,
[16492]498                watermark=watermark)
[16238]499        if self.context.is_postgrad:
500            file_path = os.path.join(
501                os.path.dirname(__file__), 'static', 'admission_letter_pg.pdf')
502            file = open(file_path, 'rb')
503            mergefiles = [file,]
504            return students_utils.renderPDFAdmissionLetter(self,
505                self.context.student, omit_fields=self.omit_fields,
[17033]506                pre_text=self.pre_text_pg, post_text=ADML_PG,
[16238]507                mergefiles=mergefiles,
508                watermark=watermark)
[16160]509        file_path = os.path.join(
[16238]510            os.path.dirname(__file__), 'static', 'admission_letter_ug.pdf')
[16160]511        file = open(file_path, 'rb')
512        mergefiles = [file,]
[16584]513        if self.context.certcode in ('BBMS', 'MBBS') and self.context.current_level == 100:
[17853]514            # Changed on 18/07/24
[17854]515            #post_text = BASIC_MEDICAL_ONLY + ADML_UG_1 + ADML_UG_2_MEDICAL + ADML_UG_3                 
[17853]516            post_text = ADML_UG_1 + ADML_UG_2_MEDICAL + ADML_UG_3
[16603]517        elif self.context.certcode in ('BPHARM',) and self.context.current_level == 100:
[17853]518            # Changed on 18/07/24
519            #post_text = BASIC_PHARMACY_ONLY + ADML_UG_1 + ADML_UG_2_MEDICAL + ADML_UG_3
520            post_text = ADML_UG_1 + ADML_UG_2_MEDICAL + ADML_UG_3
[16486]521        else:
[17033]522            post_text = ADML_UG_1 + ADML_UG_2 + ADML_UG_3
[16127]523        return students_utils.renderPDFAdmissionLetter(self,
524            self.context.student, omit_fields=self.omit_fields,
[16486]525            pre_text=self.pre_text_ug, post_text=post_text,
[16180]526            mergefiles=mergefiles,
527            watermark=watermark)
[16233]528
529class CustomOnlinePaymentDisplayFormPage(NigeriaOnlinePaymentDisplayFormPage):
530    """ Page to view an online payment ticket. We do not omit provider_amt.
531    """
532    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
[16285]533        'gateway_amt', 'thirdparty_amt', 'p_item','p_combi', 'provider_amt')
[16233]534    form_fields[
535        'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
536    form_fields[
537        'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
538
539class CustomExportPDFPaymentSlip(NigeriaExportPDFPaymentSlip):
540    """Deliver a PDF slip of the context. We do not omit provider_amt.
541    """
542    form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
543        'gateway_amt', 'thirdparty_amt', 'p_item',
[16285]544        'p_split_data','p_combi', 'provider_amt')
[16233]545    form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
546    form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
Note: See TracBrowser for help on using the repository browser.