source: main/waeup.kofa/trunk/src/waeup/kofa/utils/utils.py @ 17959

Last change on this file since 17959 was 17823, checked in by Henrik Bettermann, 5 months ago

Make _set_exporter_values customizable.

  • Property svn:keywords set to Id
File size: 18.7 KB
RevLine 
[7358]1## $Id: utils.py 17823 2024-06-27 09:18:42Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
[7819]18"""General helper utilities for Kofa.
[7358]19"""
20import grok
[11815]21import psutil
[7365]22import string
[8181]23import pytz
[15476]24import decimal
[9866]25from copy import deepcopy
[8181]26from random import SystemRandom as r
[7734]27from zope.i18n import translate
[17823]28from zope.security import checkPermission
[7819]29from waeup.kofa.interfaces import IKofaUtils
[7811]30from waeup.kofa.interfaces import MessageFactory as _
31from waeup.kofa.smtp import send_mail as send_mail_internally
[7969]32from waeup.kofa.utils.helpers import get_sorted_preferred
[13617]33from waeup.kofa.utils.degrees import DEGREES_DICT
[7358]34
[11814]35
36def send_mail(from_name, from_addr,
37              rcpt_name, rcpt_addr,
[16299]38              subject, body, config,
39              cc=None, bcc=None):
[7811]40    """Wrapper for the real SMTP functionality in :mod:`waeup.kofa.smtp`.
[7382]41
[7471]42    Merely here to stay compatible with lots of calls to this place.
[7400]43    """
[7471]44    mail_id = send_mail_internally(
45        from_name, from_addr, rcpt_name, rcpt_addr,
[16299]46        subject, body, config, cc, bcc)
[7399]47    return True
48
[11814]49
[7874]50#: A list of phone prefixes (order num, country, prefix).
51#: Items with same order num will be sorted alphabetically.
52#: The lower the order num, the higher the precedence.
53INT_PHONE_PREFIXES = [
54    (99, _('Germany'), '49'),
[11814]55    (1, _('Nigeria'), '234'),
[7874]56    (99, _('U.S.'), '1'),
57    ]
58
[11814]59
60def sorted_phone_prefixes(data=INT_PHONE_PREFIXES, request=None):
[7874]61    """Sorted tuples of phone prefixes.
62
63    Ordered as shown above and formatted for use in select boxes.
64
65    If request is given, we'll try to translate all country names in
66    order to sort alphabetically correctly.
67
68    XXX: This is a function (and not a constant) as different
69    languages might give different orders. This is not tested yet.
70
71    XXX: If we really want to use alphabetic ordering here, we might
72    think about caching results of translations.
73    """
74    if request is not None:
75        data = [
76            (x, translate(y, context=request), z)
77            for x, y, z in data]
78    return tuple([
[11814]79        ('%s (+%s)' % (x[1], x[2]), '+%s' % x[2])
[7874]80        for x in sorted(data)
81        ])
82
[11814]83
[7831]84class KofaUtils(grok.GlobalUtility):
[7678]85    """A collection of parameters and methods subject to customization.
[7358]86    """
[7831]87    grok.implements(IKofaUtils)
[13132]88
89    #: This the only place where we define the portal language
90    #: which is used for the translation of system messages
91    #: (e.g. object histories) pdf slips.
[7744]92    PORTAL_LANGUAGE = 'en'
[7358]93
[13617]94    DEGREES_DICT = DEGREES_DICT
95
[7701]96    PREFERRED_LANGUAGES_DICT = {
[11814]97        'en': (1, u'English'),
98        'fr': (2, u'Français'),
99        'de': (3, u'Deutsch'),
100        'ha': (4, u'Hausa'),
101        'yo': (5, u'Yoruba'),
102        'ig': (6, u'Igbo'),
[7701]103        }
104
[17094]105    MONTHS_DICT = {
[17211]106        '1': _('January'),
107        '2': _('February'),
108        '3': _('March'),
109        '4': _('April'),
110        '5': _('May'),
111        '6': _('June'),
112        '7': _('July'),
113        '8': _('August'),
114        '9': _('September'),
115        '10': _('October'),
116        '11': _('November'),
117        '12': _('December'),
[17094]118        }
119
[7874]120    #: A function to return
121    @classmethod
122    def sorted_phone_prefixes(cls, data=INT_PHONE_PREFIXES, request=None):
123        return sorted_phone_prefixes(data, request)
[7871]124
[7841]125    EXAM_SUBJECTS_DICT = {
[7843]126        'math': 'Mathematics',
127        'computer_science': 'Computer Science',
[7841]128        }
[7836]129
[7917]130    #: Exam grades. The tuple is sorted as it should be displayed in
131    #: select boxes.
132    EXAM_GRADES = (
133        ('A', 'Best'),
134        ('B', 'Better'),
135        ('C', 'Good'),
136        )
[7836]137
[7841]138    INST_TYPES_DICT = {
[8084]139        'none': '',
[7681]140        'faculty': 'Faculty of',
141        'department': 'Department of',
142        'school': 'School of',
143        'office': 'Office for',
144        'centre': 'Centre for',
[16811]145        'centre_of': 'Centre of',
[7681]146        'institute': 'Institute of',
147        'school_for': 'School for',
[8084]148        'college': 'College of',
[10302]149        'directorate': 'Directorate of',
[7681]150        }
151
[7841]152    STUDY_MODES_DICT = {
[9131]153        'transfer': 'Transfer',
[16828]154        'transferred': 'Transferred',
[7843]155        'ug_ft': 'Undergraduate Full-Time',
156        'ug_pt': 'Undergraduate Part-Time',
[7993]157        'pg_ft': 'Postgraduate Full-Time',
158        'pg_pt': 'Postgraduate Part-Time',
[7681]159        }
160
[11451]161    DISABLE_PAYMENT_GROUP_DICT = {
162        'sf_all': 'School Fee - All Students',
163        }
164
[7841]165    APP_CATS_DICT = {
[7843]166        'basic': 'Basic Application',
[7681]167        'no': 'no application',
168        'pg': 'Postgraduate',
169        'sandwich': 'Sandwich',
170        'cest': 'Part-Time, Diploma, Certificate'
171        }
172
[7841]173    SEMESTER_DICT = {
[10437]174        1: '1st Semester',
175        2: '2nd Semester',
[7681]176        3: 'Combined',
[16812]177        4: '1st Term',
178        5: '2nd Term',
179        6: '3rd Term',
180        9: 'N/A',
181        11: 'Module I',
182        12: 'Module II',
183        13: 'Module III',
[7681]184        }
185
[14638]186    COURSE_CATEGORY_DICT = {
187        }
188
[9400]189    SPECIAL_HANDLING_DICT = {
190        'regular': 'Regular Hostel',
191        'blocked': 'Blocked Hostel',
[10831]192        'pg': 'Postgraduate Hostel'
[9400]193        }
194
[10831]195    SPECIAL_APP_DICT = {
196        'transcript': 'Transcript Fee Payment',
[11575]197        'clearance': 'Acceptance Fee',
[10831]198        }
199
[9405]200    PAYMENT_CATEGORIES = {
201        'schoolfee': 'School Fee',
202        'clearance': 'Acceptance Fee',
203        'bed_allocation': 'Bed Allocation Fee',
204        'hostel_maintenance': 'Hostel Maintenance Fee',
205        'transfer': 'Transfer Fee',
206        'gown': 'Gown Hire Fee',
[9866]207        'application': 'Application Fee',
[15553]208        'app_balance': 'Application Fee Balance',
[10449]209        'transcript': 'Transcript Fee',
[15664]210        'late_registration': 'Late Course Registration Fee',
[15685]211        'combi': 'Combi Payment',
[17016]212        'donation': 'Donation',
[9405]213        }
214
[16431]215    #: If PAYMENT_OPTIONS is empty, payment option fields won't show up.
216    PAYMENT_OPTIONS = {
217        #'credit_card': 'Credit Card',
218        #'debit_card': 'Debit Card',
219        }
220
[15432]221    def selectable_payment_categories(self, student):
222        return self.PAYMENT_CATEGORIES
[9730]223
[16431]224    def selectable_payment_options(self, student):
225        return self.PAYMENT_OPTIONS
226
[15432]227    PREVIOUS_PAYMENT_CATEGORIES = deepcopy(PAYMENT_CATEGORIES)
[9862]228
[12564]229    REPORTABLE_PAYMENT_CATEGORIES = {
230        'schoolfee': 'School Fee',
231        'clearance': 'Acceptance Fee',
232        'hostel_maintenance': 'Hostel Maintenance Fee',
233        'gown': 'Gown Hire Fee',
234        }
235
[9868]236    BALANCE_PAYMENT_CATEGORIES = {
[9867]237        'schoolfee': 'School Fee',
238        }
[9864]239
[17016]240    APPLICANT_BALANCE_PAYMENT_CATEGORIES = {
241        'donation': 'Donation',
242        }
243
[15664]244    COMBI_PAYMENT_CATEGORIES = {
245        'gown': 'Gown Hire Fee',
246        'transcript': 'Transcript Fee',
247        'late_registration': 'Late Course Registration Fee',
248        }
249
[9649]250    MODE_GROUPS = {
[11814]251        'All': ('all',),
252        'Undergraduate Full-Time': ('ug_ft',),
253        'Undergraduate Part-Time': ('ug_pt',),
254        'Postgraduate Full-Time': ('pg_ft',),
255        'Postgraduate Part-Time': ('pg_pt',),
[9649]256        }
257
[13125]258    VERDICTS_DICT = {
259        '0': _('(not yet)'),
260        'A': 'Successful student',
261        'B': 'Student with carryover courses',
262        'C': 'Student on probation',
263        }
264
[11800]265    #: Set positive number for allowed max, negative for required min
266    #: avail.
267    #: Use integer for bytes value, float for percent
268    #: value. `cpu-load`, of course, accepts float values only.
269    #: `swap-mem` = Swap Memory, `virt-mem` = Virtual Memory,
[11969]270    #: `cpu-load` = CPU load in percent.
[11800]271    SYSTEM_MAX_LOAD = {
272        'swap-mem': None,
273        'virt-mem': None,
274        'cpu-load': 100.0,
275        }
276
[15430]277    #: Maximum number of files listed in `finished` subfolder
278    MAX_FILES = 100
279
[15833]280    #: Maximum size in Bytes of passport images in the applicants and
281    #: students section
282    MAX_PASSPORT_SIZE = 50 * 1024
283
[15609]284    #: Temporary passwords and parents password validity period
285    TEMP_PASSWORD_MINUTES = 10
286
[17376]287    def sortCertificates(self, context, resultset):
288        """Sort already filtered certificates in `CertificateSource`.
289        """
290        return sorted(resultset, key=lambda value: value.code)
291
292    def getCertTitle(self, context, value):
[17798]293        """Compose the titles in `CertificateSource.
[17376]294        """
295        return "%s - %s" % (value.code, value.title)
296
[17798]297    def getCertLongTitle(self, value):
298        """Compose certificate's longtitle.
299        """
300        return "%s (%s)" % (value.title, value.code)
301
[11814]302    def sendContactForm(self, from_name, from_addr, rcpt_name, rcpt_addr,
[16299]303                        from_username, usertype, portal, body, subject,
304                        bcc_to=None):
[7358]305        """Send an email with data provided by forms.
306        """
307        config = grok.getSite()['configuration']
[16213]308        text = _(u"""${e}
[7358]309
[16213]310---
311${a} (id: ${b})
312${d}
[7734]313""")
[11814]314        text = _(text, mapping={
315            'a': from_name,
316            'b': from_username,
317            'c': usertype,
318            'd': portal,
319            'e': body})
[7811]320        body = translate(text, 'waeup.kofa',
[7734]321            target_language=self.PORTAL_LANGUAGE)
[8436]322        if not (from_addr and rcpt_addr):
323            return False
[7400]324        return send_mail(
[11814]325            from_name, from_addr, rcpt_name, rcpt_addr,
[16299]326            subject, body, config, None, bcc_to)
[7359]327
[15964]328    def getUsers(self):
329        users = sorted(
330            grok.getSite()['users'].items(), key=lambda x: x[1].title)
331        for key, val in users:
332            yield(dict(name=key, val="%s (%s)" % (val.title, val.name)))
333
[8181]334    @property
335    def tzinfo(self):
[13124]336        """Time zone of the university.
337        """
[8181]338        # For Nigeria: pytz.timezone('Africa/Lagos')
[9543]339        # For Germany: pytz.timezone('Europe/Berlin')
[8181]340        return pytz.utc
341
[11814]342    def fullname(self, firstname, lastname, middlename=None):
[13124]343        """Construct fullname.
[7477]344        """
[7359]345        # We do not necessarily have the middlename attribute
346        if middlename:
[8603]347            name = '%s %s %s' % (firstname, middlename, lastname)
[7359]348        else:
[8603]349            name = '%s %s' % (firstname, lastname)
[13492]350        if '<' in name:
351            return 'XXX'
[11814]352        return string.capwords(
353            name.replace('-', ' - ')).replace(' - ', '-')
[7365]354
[15287]355    def genPassword(self, length=4, chars=string.letters + string.digits):
[7477]356        """Generate a random password.
357        """
[15287]358        return ''.join([
359            r().choice(string.uppercase) +
360            r().choice(string.lowercase) +
361            r().choice(string.digits) for i in range(length)])
[7365]362
[8853]363    def sendCredentials(self, user, password=None, url_info=None, msg=None):
[13124]364        """Send credentials as email. Input is the user for which credentials
365        are sent and the password. Method returns True or False to indicate
366        successful operation.
[7365]367        """
[17064]368        subject = _('Your Kofa credentials')
[7734]369        text = _(u"""Dear ${a},
[7365]370
[7734]371${b}
[7365]372Student Registration and Information Portal of
[7734]373${c}.
[7365]374
[7734]375Your user name: ${d}
376Your password: ${e}
[8853]377${f}
[7365]378
379Please remember your user name and keep
380your password secret!
381
[7382]382Please also note that passwords are case-sensitive.
383
[7365]384Regards
[7734]385""")
[7399]386        config = grok.getSite()['configuration']
387        from_name = config.name_admin
[7402]388        from_addr = config.email_admin
[7407]389        rcpt_name = user.title
390        rcpt_addr = user.email
[11814]391        text = _(text, mapping={
392            'a': rcpt_name,
393            'b': msg,
394            'c': config.name,
395            'd': user.name,
396            'e': password,
397            'f': url_info})
[7734]398
[7811]399        body = translate(text, 'waeup.kofa',
[7734]400            target_language=self.PORTAL_LANGUAGE)
[7399]401        return send_mail(
[11814]402            from_name, from_addr, rcpt_name, rcpt_addr,
403            subject, body, config)
[9987]404
[16551]405    def informNewStudent(self, user, pw, login_url, rpw_url):
406        """Inform student that a new student account has been created.
407        """
[17067]408        subject = _('Your new Kofa student account')
[16551]409        text = _(u"""Dear ${a},
410
411Your student record of the Student Registration and Information Portal of
412${b} has been created for you.
413
414Your user name: ${c}
415Your password: ${d}
416Login: ${e}
417
418Or request a new secure password here: ${f}
419
420Regards
421""")
422        config = grok.getSite()['configuration']
423        from_name = config.name_admin
424        from_addr = config.email_admin
425        rcpt_name = user.title
426        rcpt_addr = user.email
427
428        text = _(text, mapping={
429            'a': rcpt_name,
430            'b': config.name,
431            'c': user.name,
432            'd': pw,
433            'e': login_url,
434            'f': rpw_url
435            })
436        body = translate(text, 'waeup.kofa',
437            target_language=self.PORTAL_LANGUAGE)
438        return send_mail(
439            from_name, from_addr, rcpt_name, rcpt_addr,
440            subject, body, config)
441
442
[16976]443    def informApplicant(self, applicant):
444        """Inform applicant that the application form was successfully
445        submitted.
446        """
447        if not getattr(applicant.__parent__, 'send_email', False):
448            return
449        subject = 'Your application form was successfully submitted'
450        text = _(u"""Dear ${a},
451
452Your application ${b} has been successfully submitted to ${c}.
453
454Regards
455""")
456        config = grok.getSite()['configuration']
457        from_name = config.name_admin
458        from_addr = config.email_admin
459        rcpt_name = applicant.display_fullname
460        rcpt_addr = applicant.email
461        session = '%s/%s' % (
462            applicant.__parent__.year, applicant.__parent__.year+1)
463        text = _(text, mapping={
464            'a': rcpt_name,
465            'b': applicant.applicant_id,
466            'c': config.name,
467            })
468        body = translate(text, 'waeup.kofa',
469            target_language=self.PORTAL_LANGUAGE)
470        return send_mail(
471            from_name, from_addr, rcpt_name, rcpt_addr,
472            subject, body, config)
473
[14014]474    def inviteReferee(self, referee, applicant, url_info=None):
475        """Send invitation email to referee.
476        """
477        config = grok.getSite()['configuration']
478        subject = 'Request for referee report from %s' % config.name
479        text = _(u"""Dear ${a},
480
[14040]481The candidate with Id ${b} and name ${c} applied to
482the ${d} to study ${e} for the ${f} session.
[16777]483The candidate has listed you as referee. You are, therefore, required to,
484kindly, provide your referral remarks on or before ${g}. Please use the
485following form:
[14014]486
[14040]487${h}
488
489Thank You
490
491The Secretary
[16777]492School of Postgraduate Studies
[14040]493${d}
[14014]494""")
495        from_name = config.name_admin
496        from_addr = config.email_admin
497        rcpt_name = referee.name
498        rcpt_addr = referee.email
[14040]499        session = '%s/%s' % (
500            applicant.__parent__.year, applicant.__parent__.year+1)
[14014]501        text = _(text, mapping={
502            'a': rcpt_name,
[14040]503            'b': applicant.applicant_id,
504            'c': applicant.display_fullname,
505            'd': config.name,
506            'e': applicant.course1.title,
507            'f': session,
508            'g': applicant.__parent__.enddate,
509            'h': url_info,
510            })
[14014]511
512        body = translate(text, 'waeup.kofa',
513            target_language=self.PORTAL_LANGUAGE)
514        return send_mail(
515            from_name, from_addr, rcpt_name, rcpt_addr,
516            subject, body, config)
517
[9987]518    def getPaymentItem(self, payment):
[13124]519        """Return payment item. This method can be used to customize the
520        `display_item` property attribute, e.g. in order to hide bed coordinates
521        if maintenance fee is not paid.
[9987]522        """
523        return payment.p_item
[11815]524
525    def expensive_actions_allowed(self, type=None, request=None):
526        """Tell, whether expensive actions are currently allowed.
527        Check system load/health (or other external circumstances) and
528        locally set values to see, whether expensive actions should be
529        allowed (`True`) or better avoided (`False`).
530        Use this to allow or forbid exports, report generations, or
531        similar actions.
532        """
533        max_values = self.SYSTEM_MAX_LOAD
[11816]534        for (key, func) in (
535            ('swap-mem', psutil.swap_memory),
[11818]536            ('virt-mem', psutil.virtual_memory),
[11816]537            ):
538            max_val = max_values.get(key, None)
539            if max_val is None:
540                continue
541            mem_val = func()
[11815]542            if isinstance(max_val, float):
[11816]543                # percents
[11821]544                if max_val < 0.0:
545                    max_val = 100.0 + max_val
[11816]546                if mem_val.percent > max_val:
[11815]547                    return False
548            else:
[11816]549                # number of bytes
[11821]550                if max_val < 0:
551                    max_val = mem_val.total + max_val
[11816]552                if mem_val.used > max_val:
[11815]553                    return False
554        return True
[13198]555
556    def export_disabled_message(self):
557        export_disabled_message = grok.getSite()[
558            'configuration'].export_disabled_message
559        if export_disabled_message:
560            return export_disabled_message
[14473]561        return None
562
563    def format_float(self, value, prec):
[15476]564        # >>> 4.6 * 100
565        # 459.99999999999994
566        value = decimal.Decimal(str(value))
[14473]567        # cut floating point value
568        value = int(pow(10, prec)*value) / (1.0*pow(10, prec))
569        return '{:{width}.{prec}f}'.format(value, width=0, prec=prec)
[17823]570
571    def collect_exporters(self, context):
572        # Used for `ExportJobContainerJobConfig`.
573        # We provide all student exporters, nothing else, yet.
574        # Bursary, Department or Accommodation Officers don't
575        # have the general exportData
576        # permission and are only allowed to export bursary, payments
577        # overview or accommodation data respectively.
578        # This is the only place where waeup.exportAccommodationData,
579        # waeup.exportBursaryData and waeup.exportPaymentsOverview
580        # are used.
581        exporters = []
582        if not checkPermission('waeup.exportData', context):
583            if checkPermission('waeup.exportBursaryData', context):
584                exporters += [('Bursary Data', 'bursary')]
585            if checkPermission('waeup.exportPaymentsOverview', context):
586                exporters += [('School Fee Payments Overview',
587                               'sfpaymentsoverview'),
588                              ('Session Payments Overview',
589                               'sessionpaymentsoverview')]
590            if checkPermission('waeup.exportAccommodationData', context):
591                exporters += [('Bed Tickets', 'bedtickets'),
592                              ('Accommodation Payments',
593                               'accommodationpayments')]
594            return exporters
Note: See TracBrowser for help on using the repository browser.