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

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

Change output of get_bursary_data.

  • Property svn:keywords set to Id
File size: 23.4 KB
RevLine 
[10033]1## $Id: webservices.py 17856 2024-07-19 18:16:04Z 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,
[17847]408            current_session=None, p_session=None, current_level=None, certcode=None,
409            current_mode=None, depcode=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
[17836]429        if not p_session:
430            p_session = None
431        else:
432            try:
433                p_session = int(p_session)
434            except (TypeError, ValueError):
435                hitlist.append(dict(error="Wrong parameters"))
436                return hitlist
[15798]437        if not depcode:
438            depcode = None
439        if not certcode:
440            certcode = None
441        if not current_mode:
442            current_mode = None
443        cat = queryUtility(ICatalog, name='students_catalog')
444        results = list(
445            cat.searchResults(
446                current_session=(current_session, current_session),
447                current_level=(current_level, current_level),
448                certcode=(certcode, certcode),
449                current_mode=(current_mode, current_mode),
450                depcode=(depcode, depcode),
451                ))
[16269]452        payments = get_payments(results, paysession=p_session)
[15798]453        tz = getUtility(IKofaUtils).tzinfo
454        for payment in payments:
[16269]455            hitlist.append(dict(
456                student_id=payment.student.student_id,
457                matric_number=payment.student.matric_number,
458                reg_number=payment.student.reg_number,
459                firstname=payment.student.firstname,
460                middlename=payment.student.middlename,
461                lastname=payment.student.lastname,
462                state=payment.student.state,
463                current_session=payment.student.current_session,
464                entry_session=payment.student.entry_session,
465                entry_mode=payment.student.entry_mode,
466                faccode=payment.student.faccode,
467                depcode=payment.student.depcode,
468                certcode=payment.student.certcode,
469                p_id=payment.p_id,
470                amount_auth=payment.amount_auth,
471                p_category=payment.p_category,
472                display_item=payment.display_item,
473                p_session=payment.p_session,
474                p_state=payment.p_state,
475                creation_date=str('%s#' % to_timezone(
476                    payment.creation_date, tz)),
477                payment_date=str('%s#' % to_timezone(
478                    payment.payment_date, tz)),
[17856]479                p_level=payment.p_level,
480                p_combi=payment.p_combi,
[16269]481                )
482              )
[15798]483        return hitlist
[16169]484
485    @grok.require('waeup.xmlrpc')
[16193]486    def get_payment(self, p_id='non_existent'):
487        """Returns payment and payer data of payment tickets with specific p_id.
488        """
489        cat = getUtility(ICatalog, name='payments_catalog')
490        result = list(cat.searchResults(p_id=(p_id, p_id)))
491        if not len(result):
492            return None
493        payment =  result[0]
494        return dict(
495                p_id=payment.p_id,
496                amount_auth=payment.amount_auth,
497                p_category=payment.p_category,
498                display_item=payment.display_item,
499                p_session=payment.p_session,
500                p_state=payment.p_state,
501                r_company=getattr(payment, 'r_company', None),
502                id = IPayer(payment).id,
503                matric_number = IPayer(payment).matric_number,
504                fullname = IPayer(payment).display_fullname,
505                )
506
507    @grok.require('waeup.xmlrpc')
[16169]508    def get_unpaid_payments(self, days=3, company=None):
[16193]509        """Returns the payment and payer data of unpaid payment
510        tickets which have been created during the past days.
[16169]511        """
512        days_in_seconds = 86400 * int(days)
513        timestamp_now = ("%d" % int(time()*10000))[1:]
514        timestamp_now_minus_days = ("%d" % int((time()-days_in_seconds)*10000))[1:]
515        p_id_now = ("p%s" % timestamp_now)
516        p_id_minus_days = ("p%s" % timestamp_now_minus_days)
517        cat = getUtility(ICatalog, name='payments_catalog')
518        payments = list(
519            cat.searchResults(p_id=(p_id_minus_days, p_id_now),
520                              p_state=('unpaid', 'unpaid')))
[16191]521        payments += list(
522            cat.searchResults(p_id=(p_id_minus_days, p_id_now),
523                              p_state=('failed', 'failed')))
[16169]524        hitlist = []
525        if company:
526            for payment in payments:
527                if company == getattr(payment, 'r_company', None):
528                    hitlist.append(dict(
529                        p_id=payment.p_id,
530                        amount_auth=payment.amount_auth,
531                        p_category=payment.p_category,
532                        display_item=payment.display_item,
533                        p_session=payment.p_session,
534                        p_state=payment.p_state,
[16192]535                        r_company=getattr(payment, 'r_company', None),
536                        id = IPayer(payment).id,
537                        matric_number = IPayer(payment).matric_number,
538                        fullname = IPayer(payment).display_fullname,
[16169]539                        )
540                      )
541            return hitlist
542        for payment in payments:
543            hitlist.append(dict(
544                p_id=payment.p_id,
545                amount_auth=payment.amount_auth,
546                p_category=payment.p_category,
547                display_item=payment.display_item,
548                p_session=payment.p_session,
549                p_state=payment.p_state,
[16192]550                r_company=getattr(payment, 'r_company', None),
551                id = IPayer(payment).id,
552                matric_number = IPayer(payment).matric_number,
553                fullname = IPayer(payment).display_fullname,
[16169]554                )
555              )
556        return hitlist
[17787]557
558    # Data requested by the WAeUP team
559
560    @grok.require('waeup.xmlrpc')
561    def get_student_base_data(self,
562            current_session=None, current_level=None, certcode=None,
[17793]563            current_mode=None, faccode=None, depcode=None, p_session=None):
[17794]564        """Returns base data of a subset of students.
[17787]565        """
566        hitlist = []
[17833]567        if not current_session:
568            current_session = None
569        else:
[17808]570            try:
571                current_session = int(current_session)
572            except (TypeError, ValueError):
573                hitlist.append(dict(error="Wrong parameters"))
574                return hitlist
[17833]575        if not current_level:
576            current_level = None
577        else:
[17808]578            try:
579                current_level = int(current_level)
580            except (TypeError, ValueError):
581                hitlist.append(dict(error="Wrong parameters"))
582                return hitlist
[17833]583        if not depcode:
584            depcode = None
585        if not certcode:
586            certcode = None
587        if not current_mode:
588            current_mode = None
[17787]589        cat = queryUtility(ICatalog, name='students_catalog')
590        results = cat.searchResults(
591                current_session=(current_session, current_session),
592                current_level=(current_level, current_level),
593                certcode=(certcode, certcode),
594                current_mode=(current_mode, current_mode),
[17793]595                faccode=(depcode, depcode),
[17787]596                depcode=(depcode, depcode),
597                )
[17805]598        if len(results) > 2000:
[17794]599            hitlist.append(dict(error="Too many objects (500 max)"))
600            return hitlist
[17787]601        for student in results:
602            hitlist.append(dict(
603                student_id=student.student_id,
604                matric_number=student.matric_number,
605                reg_number=student.reg_number,
606                firstname=student.firstname,
607                middlename=student.middlename,
608                lastname=student.lastname,
609                state=student.state,
610                current_session=student.current_session,
611                entry_session=student.entry_session,
612                entry_mode=student.entry_mode,
613                faccode=student.faccode,
614                depcode=student.depcode,
615                certcode=student.certcode,
[17805]616                sex=student.sex,
[17787]617                )
618            )
619        return hitlist
Note: See TracBrowser for help on using the repository browser.