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

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

Enable translation of email subject.

  • Property svn:keywords set to Id
File size: 16.5 KB
Line 
1## $Id: utils.py 17067 2022-08-17 16:09:23Z 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"""General helper utilities for Kofa.
19"""
20import grok
21import psutil
22import string
23import pytz
24import decimal
25from copy import deepcopy
26from random import SystemRandom as r
27from zope.i18n import translate
28from waeup.kofa.interfaces import IKofaUtils
29from waeup.kofa.interfaces import MessageFactory as _
30from waeup.kofa.smtp import send_mail as send_mail_internally
31from waeup.kofa.utils.helpers import get_sorted_preferred
32from waeup.kofa.utils.degrees import DEGREES_DICT
33
34
35def send_mail(from_name, from_addr,
36              rcpt_name, rcpt_addr,
37              subject, body, config,
38              cc=None, bcc=None):
39    """Wrapper for the real SMTP functionality in :mod:`waeup.kofa.smtp`.
40
41    Merely here to stay compatible with lots of calls to this place.
42    """
43    mail_id = send_mail_internally(
44        from_name, from_addr, rcpt_name, rcpt_addr,
45        subject, body, config, cc, bcc)
46    return True
47
48
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'),
54    (1, _('Nigeria'), '234'),
55    (99, _('U.S.'), '1'),
56    ]
57
58
59def sorted_phone_prefixes(data=INT_PHONE_PREFIXES, request=None):
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([
78        ('%s (+%s)' % (x[1], x[2]), '+%s' % x[2])
79        for x in sorted(data)
80        ])
81
82
83class KofaUtils(grok.GlobalUtility):
84    """A collection of parameters and methods subject to customization.
85    """
86    grok.implements(IKofaUtils)
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.
91    PORTAL_LANGUAGE = 'en'
92
93    DEGREES_DICT = DEGREES_DICT
94
95    PREFERRED_LANGUAGES_DICT = {
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'),
102        }
103
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)
108
109    EXAM_SUBJECTS_DICT = {
110        'math': 'Mathematics',
111        'computer_science': 'Computer Science',
112        }
113
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        )
121
122    INST_TYPES_DICT = {
123        'none': '',
124        'faculty': 'Faculty of',
125        'department': 'Department of',
126        'school': 'School of',
127        'office': 'Office for',
128        'centre': 'Centre for',
129        'centre_of': 'Centre of',
130        'institute': 'Institute of',
131        'school_for': 'School for',
132        'college': 'College of',
133        'directorate': 'Directorate of',
134        }
135
136    STUDY_MODES_DICT = {
137        'transfer': 'Transfer',
138        'transferred': 'Transferred',
139        'ug_ft': 'Undergraduate Full-Time',
140        'ug_pt': 'Undergraduate Part-Time',
141        'pg_ft': 'Postgraduate Full-Time',
142        'pg_pt': 'Postgraduate Part-Time',
143        }
144
145    DISABLE_PAYMENT_GROUP_DICT = {
146        'sf_all': 'School Fee - All Students',
147        }
148
149    APP_CATS_DICT = {
150        'basic': 'Basic Application',
151        'no': 'no application',
152        'pg': 'Postgraduate',
153        'sandwich': 'Sandwich',
154        'cest': 'Part-Time, Diploma, Certificate'
155        }
156
157    SEMESTER_DICT = {
158        1: '1st Semester',
159        2: '2nd Semester',
160        3: 'Combined',
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',
168        }
169
170    COURSE_CATEGORY_DICT = {
171        }
172
173    SPECIAL_HANDLING_DICT = {
174        'regular': 'Regular Hostel',
175        'blocked': 'Blocked Hostel',
176        'pg': 'Postgraduate Hostel'
177        }
178
179    SPECIAL_APP_DICT = {
180        'transcript': 'Transcript Fee Payment',
181        'clearance': 'Acceptance Fee',
182        }
183
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',
191        'application': 'Application Fee',
192        'app_balance': 'Application Fee Balance',
193        'transcript': 'Transcript Fee',
194        'late_registration': 'Late Course Registration Fee',
195        'combi': 'Combi Payment',
196        'donation': 'Donation',
197        }
198
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
205    def selectable_payment_categories(self, student):
206        return self.PAYMENT_CATEGORIES
207
208    def selectable_payment_options(self, student):
209        return self.PAYMENT_OPTIONS
210
211    PREVIOUS_PAYMENT_CATEGORIES = deepcopy(PAYMENT_CATEGORIES)
212
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
220    BALANCE_PAYMENT_CATEGORIES = {
221        'schoolfee': 'School Fee',
222        }
223
224    APPLICANT_BALANCE_PAYMENT_CATEGORIES = {
225        'donation': 'Donation',
226        }
227
228    COMBI_PAYMENT_CATEGORIES = {
229        'gown': 'Gown Hire Fee',
230        'transcript': 'Transcript Fee',
231        'late_registration': 'Late Course Registration Fee',
232        }
233
234    MODE_GROUPS = {
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',),
240        }
241
242    VERDICTS_DICT = {
243        '0': _('(not yet)'),
244        'A': 'Successful student',
245        'B': 'Student with carryover courses',
246        'C': 'Student on probation',
247        }
248
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,
254    #: `cpu-load` = CPU load in percent.
255    SYSTEM_MAX_LOAD = {
256        'swap-mem': None,
257        'virt-mem': None,
258        'cpu-load': 100.0,
259        }
260
261    #: Maximum number of files listed in `finished` subfolder
262    MAX_FILES = 100
263
264    #: Maximum size in Bytes of passport images in the applicants and
265    #: students section
266    MAX_PASSPORT_SIZE = 50 * 1024
267
268    #: Temporary passwords and parents password validity period
269    TEMP_PASSWORD_MINUTES = 10
270
271    def sendContactForm(self, from_name, from_addr, rcpt_name, rcpt_addr,
272                        from_username, usertype, portal, body, subject,
273                        bcc_to=None):
274        """Send an email with data provided by forms.
275        """
276        config = grok.getSite()['configuration']
277        text = _(u"""${e}
278
279---
280${a} (id: ${b})
281${d}
282""")
283        text = _(text, mapping={
284            'a': from_name,
285            'b': from_username,
286            'c': usertype,
287            'd': portal,
288            'e': body})
289        body = translate(text, 'waeup.kofa',
290            target_language=self.PORTAL_LANGUAGE)
291        if not (from_addr and rcpt_addr):
292            return False
293        return send_mail(
294            from_name, from_addr, rcpt_name, rcpt_addr,
295            subject, body, config, None, bcc_to)
296
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
303    @property
304    def tzinfo(self):
305        """Time zone of the university.
306        """
307        # For Nigeria: pytz.timezone('Africa/Lagos')
308        # For Germany: pytz.timezone('Europe/Berlin')
309        return pytz.utc
310
311    def fullname(self, firstname, lastname, middlename=None):
312        """Construct fullname.
313        """
314        # We do not necessarily have the middlename attribute
315        if middlename:
316            name = '%s %s %s' % (firstname, middlename, lastname)
317        else:
318            name = '%s %s' % (firstname, lastname)
319        if '<' in name:
320            return 'XXX'
321        return string.capwords(
322            name.replace('-', ' - ')).replace(' - ', '-')
323
324    def genPassword(self, length=4, chars=string.letters + string.digits):
325        """Generate a random password.
326        """
327        return ''.join([
328            r().choice(string.uppercase) +
329            r().choice(string.lowercase) +
330            r().choice(string.digits) for i in range(length)])
331
332    def sendCredentials(self, user, password=None, url_info=None, msg=None):
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.
336        """
337        subject = _('Your Kofa credentials')
338        text = _(u"""Dear ${a},
339
340${b}
341Student Registration and Information Portal of
342${c}.
343
344Your user name: ${d}
345Your password: ${e}
346${f}
347
348Please remember your user name and keep
349your password secret!
350
351Please also note that passwords are case-sensitive.
352
353Regards
354""")
355        config = grok.getSite()['configuration']
356        from_name = config.name_admin
357        from_addr = config.email_admin
358        rcpt_name = user.title
359        rcpt_addr = user.email
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})
367
368        body = translate(text, 'waeup.kofa',
369            target_language=self.PORTAL_LANGUAGE)
370        return send_mail(
371            from_name, from_addr, rcpt_name, rcpt_addr,
372            subject, body, config)
373
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
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
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
450The candidate with Id ${b} and name ${c} applied to
451the ${d} to study ${e} for the ${f} session.
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:
455
456${h}
457
458Thank You
459
460The Secretary
461School of Postgraduate Studies
462${d}
463""")
464        from_name = config.name_admin
465        from_addr = config.email_admin
466        rcpt_name = referee.name
467        rcpt_addr = referee.email
468        session = '%s/%s' % (
469            applicant.__parent__.year, applicant.__parent__.year+1)
470        text = _(text, mapping={
471            'a': rcpt_name,
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            })
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
487    def getPaymentItem(self, payment):
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.
491        """
492        return payment.p_item
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
503        for (key, func) in (
504            ('swap-mem', psutil.swap_memory),
505            ('virt-mem', psutil.virtual_memory),
506            ):
507            max_val = max_values.get(key, None)
508            if max_val is None:
509                continue
510            mem_val = func()
511            if isinstance(max_val, float):
512                # percents
513                if max_val < 0.0:
514                    max_val = 100.0 + max_val
515                if mem_val.percent > max_val:
516                    return False
517            else:
518                # number of bytes
519                if max_val < 0:
520                    max_val = mem_val.total + max_val
521                if mem_val.used > max_val:
522                    return False
523        return True
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
530        return None
531
532    def format_float(self, value, prec):
533        # >>> 4.6 * 100
534        # 459.99999999999994
535        value = decimal.Decimal(str(value))
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.