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

Last change on this file since 17064 was 17064, checked in by Henrik Bettermann, 2 years ago

Enable translation of email subject.

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