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

Last change on this file since 17833 was 17833, checked in by Henrik Bettermann, 4 months ago

Fix webservices.

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