source: main/waeup.kofa/trunk/src/waeup/kofa/students/webservices.py @ 16189

Last change on this file since 16189 was 16169, checked in by Henrik Bettermann, 4 years ago

Implement webservice get_unpaid_payments.

  • Property svn:keywords set to Id
File size: 17.2 KB
RevLine 
[10033]1## $Id: webservices.py 16169 2020-07-17 19:44:47Z 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
[11668]19import os
[10042]20import xmlrpclib
[16169]21from time import time
[11674]22from cStringIO import StringIO
[10038]23from zope.component import getUtility, queryUtility
[10033]24from zope.catalog.interfaces import ICatalog
[11674]25from waeup.kofa.interfaces import (
[16110]26    IUniversity, IExtFileStore, IFileStoreNameChooser, IKofaUtils,
[16112]27    GRADUATED, TRANSREL)
[15798]28from waeup.kofa.utils.helpers import get_fileformat, to_timezone
29from waeup.kofa.students.catalog import StudentsQuery
30from waeup.kofa.students.export import get_payments
[10033]31
[10502]32
[10041]33def get_student(students, identifier):
34    if identifier is None:
35        return None
36    student = students.get(identifier, None)
37    if student is None:
38        cat = queryUtility(ICatalog, name='students_catalog')
39        results = list(
40            cat.searchResults(matric_number=(identifier, identifier)))
41        if len(results) == 1:
42            student = results[0]
43        else:
44            results = list(
45                cat.searchResults(reg_number=(identifier, identifier)))
46            if len(results) == 1:
47                student = results[0]
48    return student
[10033]49
50#class XMLRPCPermission(grok.Permission):
51#    """Permission for using XMLRPC functions.
52#    """
53#    grok.name('waeup.xmlrpc')
54
55#class XMLRPCUsers2(grok.Role):
56#    """Usergroup 2
57#    """
58#    grok.name('waeup.xmlrpcusers2')
59#    grok.title('XMLRPC Users Group 2')
60#    grok.permissions('waeup.xmlrpc',)
61
[10502]62
[10033]63class StudentsXMLRPC(grok.XMLRPC):
64    """Student related XMLRPC webservices.
65
66    Please note, that XMLRPC does not support real keyword arguments
67    but positional arguments only.
68    """
69
70    grok.context(IUniversity)
71
72    @grok.require('waeup.xmlrpc')
73    def get_student_id(self, reg_number=None):
74        """Get the id of a student with registration number `reg_number`.
75
76        Returns the student id as string if successful, ``None`` else.
77        """
78        if reg_number is not None:
79            cat = getUtility(ICatalog, name='students_catalog')
80            result = list(
81                cat.searchResults(reg_number=(reg_number, reg_number),
82                                  _limit=1))
83            if not len(result):
84                return None
85            return result[0].student_id
86        return None
[10036]87
88    @grok.require('waeup.xmlrpc')
[10041]89    def get_courses_by_session(self, identifier=None, session=None):
[10043]90        """1. What COURSES are registered by student X in session Y?
[10036]91
92        """
[10041]93        students = self.context['students']
94        student = get_student(students, identifier)
95        if student is None:
[10036]96            return None
97        try:
98            session = int(session)
99        except (TypeError, ValueError):
100            pass
101        sessionsearch = True
102        if session in (None, ''):
103            sessionsearch = False
104        studycourse = student['studycourse']
105        coursetickets = {}
106        for level in studycourse.values():
107            if sessionsearch and level.level_session != session:
108                continue
109            for ct in level.values():
110                coursetickets.update(
111                    {"%s|%s" % (level.level, ct.code): ct.title})
112        if not coursetickets:
113            return None
114        return coursetickets
[10038]115
116    @grok.require('waeup.xmlrpc')
117    def get_students_by_course(self, course=None, session=None):
[10043]118        """2. What STUDENTS registered (student id / matric no)
[10044]119        for course Z in session Y and did they pay school fee?
[10038]120
121        """
122        try:
123            session = int(session)
124        except (TypeError, ValueError):
125            pass
126        sessionsearch = True
127        if session in (None, ''):
128            sessionsearch = False
129        cat = queryUtility(ICatalog, name='coursetickets_catalog')
130        if sessionsearch:
131            coursetickets = cat.searchResults(
132                session=(session, session),
133                code=(course, course))
134        else:
135            coursetickets = cat.searchResults(
136                code=(course, course))
137        if not coursetickets:
138            return None
139        hitlist = []
[10044]140        for c_ticket in coursetickets:
141            amount = 0
142            for p_ticket in c_ticket.student['payments'].values():
143                if p_ticket.p_state == 'paid' and \
144                    p_ticket.p_category == 'schoolfee' and \
145                    p_ticket.p_session == c_ticket.__parent__.level_session:
146                    amount = p_ticket.amount_auth
[10040]147            hitlist.append((
[10044]148                c_ticket.student.student_id,
149                c_ticket.student.matric_number,
150                c_ticket.__parent__.validated_by,
151                amount
152                ))
[10038]153        return list(set(hitlist))
[10042]154
155    @grok.require('waeup.xmlrpc')
156    def get_student_info(self, identifier=None):
[10043]157        """3a. Who is the student with matriculation number / student id
[10042]158
159        """
160        students = self.context['students']
161        student = get_student(students, identifier)
162        if student is None:
163            return None
164        return [student.display_fullname, student.certcode,
165            student.phone, student.email]
166
[16110]167    @grok.require('waeup.Public')
168    def get_grad_student(self, identifier=None, email=None):
[16112]169        """Check if student record exist, check email address and
170        retrieve registration state.
[16110]171        """
172        students = self.context['students']
173        student = get_student(students, identifier)
174        if student is None:
175            return None
176        correct_email = False
177        has_graduated = False
[16112]178        transcript_released = False
[16110]179        if student.email == email:
180            correct_email = True
181        if student.state == GRADUATED:
182            has_graduated = True
[16112]183        if student.state == TRANSREL:
184            transcript_released = True
185        return [correct_email, has_graduated, transcript_released]
[16110]186
[10042]187    @grok.require('waeup.xmlrpc')
188    def get_student_passport(self, identifier=None):
[10043]189        """3b. Get passport picture of student with
190        matriculation number / student id.
[10042]191
192        """
193        students = self.context['students']
194        student = get_student(students, identifier)
195        if student is None:
196            return None
197        img = getUtility(IExtFileStore).getFileByContext(
198            student, attr='passport.jpg')
199        return xmlrpclib.Binary(img.read())
[10043]200
201    @grok.require('waeup.xmlrpc')
202    def get_paid_sessions(self, identifier=None):
203        """6. Get list of SESSIONS school fees paid by student X.
204
205        """
206        students = self.context['students']
207        student = get_student(students, identifier)
208        if student is None:
209            return None
210        payments_dict = {}
211        for ticket in student['payments'].values():
212            if ticket.p_state == 'paid' and \
213                ticket.p_category == 'schoolfee':
214                payments_dict[str(ticket.p_session)] = ticket.amount_auth
215        if not payments_dict:
216            return None
217        return payments_dict
[10477]218
219    @grok.require('waeup.xmlrpc')
[10505]220    def check_student_credentials(self, username, password):
[10477]221        """Returns student data if username and password are valid,
222        None else.
223
224        We only query the students authenticator plugin in order not
225        to mix up with other credentials (admins, staff, etc.).
226
227        All additional checks performed by usual student
228        authentication apply. For instance for suspended students we
229        won't get a successful response but `None`.
230
231        This method can be used by CAS to authentify students for
232        external systems like moodle.
233        """
234        from zope.pluggableauth.interfaces import IAuthenticatorPlugin
235        auth = getUtility(IAuthenticatorPlugin, name='students')
236        creds = dict(login=username, password=password)
237        principal = auth.authenticateCredentials(creds)
238        if principal is None:
239            return None
240        return dict(email=principal.email, id=principal.id,
241                    type=principal.user_type,
242                    description=principal.description)
[10508]243
244    @grok.require('waeup.xmlrpc')
[10516]245    def get_student_moodle_data(self, identifier=None):
[10521]246        """Returns student data to update user data and enroll user
247        in Moodle courses.
[10508]248
249        """
250        students = self.context['students']
251        student = get_student(students, identifier)
252        if student is None:
253            return None
254        return dict(email=student.email,
255                    firstname=student.firstname,
256                    lastname=student.lastname,
257                    )
[11667]258
[11674]259    @grok.require('waeup.putBiometricData')
260    def put_student_fingerprints(self, identifier=None, fingerprints={}):
261        """Store fingerprint files for student identified by `identifier`.
262
263        `fingerprints` is expected to be a dict with strings
264        ``1``..``10`` as keys and binary data as values.
265
266        The keys ``1``..``10`` represent respective fingers: ``1`` is
267        the left thumb, ``10`` the little finger of right hand.
268
269        The binary data values are expected to be fingerprint minutiae
270        files as created by the libfprint library. With the std python
271        `xmlrpclib` client you can create such values with
272        `xmlrpclib.Binary(<BINARY_DATA_HERE>)`.
273
274        The following problems will raise errors:
275
276        - Invalid student identifiers (student does not exist or
277          unknown format of identifier)
278
279        - Fingerprint files that are not put into a dict.
280
281        - Fingerprint dicts that contain non-FPM files (or otherwise
282          invalid .fpm data).
283
284        Returns `True` in case of successful operation (at least one
285        fingerprint was stored), `False` otherwise.
286        """
287        result = False
288        students = self.context['students']
289        student = get_student(students, identifier)
290        if student is None:
291            raise xmlrpclib.Fault(
292                xmlrpclib.INVALID_METHOD_PARAMS,
293                "No such student: '%s'" % identifier)
294        if not isinstance(fingerprints, dict):
295            raise xmlrpclib.Fault(
296                xmlrpclib.INVALID_METHOD_PARAMS,
297                "Invalid fingerprint data: must be in dict")
298        for str_key, val in fingerprints.items():
299            num = 0
300            try:
301                num = int(str_key)
302            except ValueError:
303                pass
304            if num < 1 or num > 10:
305                continue
306            if not isinstance(val, xmlrpclib.Binary):
307                raise xmlrpclib.Fault(
308                    xmlrpclib.INVALID_METHOD_PARAMS,
309                    "Invalid data for finger %s" % num)
310            fmt = get_fileformat(None, val.data)
311            if fmt != 'fpm':
312                raise xmlrpclib.Fault(
313                    xmlrpclib.INVALID_METHOD_PARAMS,
314                    "Invalid file format for finger %s" % num)
315            file_store = getUtility(IExtFileStore)
316            file_id = IFileStoreNameChooser(student).chooseName(
[14637]317                attr='finger%s.fpm' % num)
[11674]318            file_store.createFile(file_id, StringIO(val.data))
[14681]319            student.writeLogMessage(self, 'fingerprint stored')
[11674]320            result = True
321        return result
322
[11667]323    @grok.require('waeup.getBiometricData')
324    def get_student_fingerprints(self, identifier=None):
325        """Returns student fingerprint data if available.
[11671]326
327        Result set is a dictionary with entries for ``email``,
328        ``firstname``, ``lastname``, ``img``, ``img_name``, and
329        ``fingerprints``.
330
331        Here ``img`` and ``img_name`` represent a photograph of the
332        student (binary image data and filename
333        respectively).
334
335        ``fingerprints`` is a dictionary itself with possible entries
336        ``1`` to ``10``, containing binary minutiae data
337        (i.e. fingerprint scans).
[11667]338        """
339        students = self.context['students']
340        student = get_student(students, identifier)
341        if student is None:
342            return dict()
343        result = dict(
344            email=student.email,
345            firstname=student.firstname,
346            lastname=student.lastname,
[11668]347            fingerprints={},
348            img_name=None,
[11667]349            img=None,
350            )
351        file_store = getUtility(IExtFileStore)
[11668]352        img = file_store.getFileByContext(student, attr='passport.jpg')
353        if img is not None:
354            result.update(
355                img=xmlrpclib.Binary(img.read()),
356                img_name=os.path.basename(img.name))
357
358        for num in [str(x + 1) for x in range(10)]:
[11671]359            fp_file = file_store.getFileByContext(
[14637]360                student, attr='finger%s.fpm' % num)
[11667]361            if fp_file is not None:
[11671]362                result['fingerprints'][num] = xmlrpclib.Binary(fp_file.read())
[11667]363        return result
[15798]364
365    @grok.require('waeup.xmlrpc')
366    def get_bursary_data(self,
367            current_session=None, current_level=None, certcode=None,
368            current_mode=None, depcode=None):
369        """Returns bursary data of a subset of students.
370        """
371        if not current_session:
372            current_session = None
373        if not current_level:
374            current_level = None
375        if not depcode:
376            depcode = None
377        if not certcode:
378            certcode = None
379        if not current_mode:
380            current_mode = None
381        hitlist = []
382        cat = queryUtility(ICatalog, name='students_catalog')
383        results = list(
384            cat.searchResults(
385                current_session=(current_session, current_session),
386                current_level=(current_level, current_level),
387                certcode=(certcode, certcode),
388                current_mode=(current_mode, current_mode),
389                depcode=(depcode, depcode),
390                ))
391        payments = get_payments(results)
392        tz = getUtility(IKofaUtils).tzinfo
393        for payment in payments:
[15811]394            hitlist.append(dict(
395                student_id=payment.student.student_id,
396                matric_number=payment.student.matric_number,
397                reg_number=payment.student.reg_number,
398                firstname=payment.student.firstname,
399                middlename=payment.student.middlename,
400                lastname=payment.student.lastname,
401                state=payment.student.state,
402                current_session=payment.student.current_session,
403                entry_session=payment.student.entry_session,
404                entry_mode=payment.student.entry_mode,
405                faccode=payment.student.faccode,
406                depcode=payment.student.depcode,
407                certcode=payment.student.certcode,
408                p_id=payment.p_id,
409                amount_auth=payment.amount_auth,
410                p_category=payment.p_category,
411                display_item=payment.display_item,
412                p_session=payment.p_session,
413                p_state=payment.p_state,
414                creation_date=str('%s#' % to_timezone(payment.creation_date, tz)),
415                payment_date=str('%s#' % to_timezone(payment.payment_date, tz)),
[15798]416                )
[15811]417              )
[15798]418        return hitlist
[16169]419
420    @grok.require('waeup.xmlrpc')
421    def get_unpaid_payments(self, days=3, company=None):
422        """Returns the p_ids of unpaid payment tickets which have
423        been created during the past days.
424        """
425        days_in_seconds = 86400 * int(days)
426        timestamp_now = ("%d" % int(time()*10000))[1:]
427        timestamp_now_minus_days = ("%d" % int((time()-days_in_seconds)*10000))[1:]
428        p_id_now = ("p%s" % timestamp_now)
429        p_id_minus_days = ("p%s" % timestamp_now_minus_days)
430        cat = getUtility(ICatalog, name='payments_catalog')
431        payments = list(
432            cat.searchResults(p_id=(p_id_minus_days, p_id_now),
433                              p_state=('unpaid', 'unpaid')))
434        hitlist = []
435        if company:
436            for payment in payments:
437                if company == getattr(payment, 'r_company', None):
438                    hitlist.append(dict(
439                        p_id=payment.p_id,
440                        amount_auth=payment.amount_auth,
441                        p_category=payment.p_category,
442                        display_item=payment.display_item,
443                        p_session=payment.p_session,
444                        p_state=payment.p_state,
445                        r_company=getattr(payment, 'r_company', None)
446                        )
447                      )
448            return hitlist
449        for payment in payments:
450            hitlist.append(dict(
451                p_id=payment.p_id,
452                amount_auth=payment.amount_auth,
453                p_category=payment.p_category,
454                display_item=payment.display_item,
455                p_session=payment.p_session,
456                p_state=payment.p_state,
457                r_company=getattr(payment, 'r_company', None)
458                )
459              )
460        return hitlist
Note: See TracBrowser for help on using the repository browser.