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

Last change on this file since 18067 was 18058, checked in by Henrik Bettermann, 7 weeks ago
  • Replace get_unpaid_payments webservice method by get_payments.
  • format_float is rounding now.
  • Property svn:keywords set to Id
File size: 23.6 KB
Line 
1## $Id: webservices.py 18058 2025-04-10 10:13:10Z 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
19import os
20import xmlrpclib
21from time import time
22from cStringIO import StringIO
23from zope.component import getUtility, queryUtility
24from zope.catalog.interfaces import ICatalog
25from waeup.kofa.interfaces import (
26    IUniversity, IExtFileStore, IFileStoreNameChooser, IKofaUtils,
27    GRADUATED, TRANSREL)
28from waeup.kofa.payments.interfaces import IPayer
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
32
33
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
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
63
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
88
89    @grok.require('waeup.xmlrpc')
90    def get_courses_by_session(self, identifier=None, session=None):
91        """1. What COURSES are registered by student X in session Y?
92
93        """
94        students = self.context['students']
95        student = get_student(students, identifier)
96        if student is None:
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
116
117    @grok.require('waeup.xmlrpc')
118    def get_students_by_course(self, course=None, session=None):
119        """2. What STUDENTS registered (student id / matric no)
120        for course Z in session Y and did they pay school fee?
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 = []
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
148            hitlist.append((
149                c_ticket.student.student_id,
150                c_ticket.student.matric_number,
151                c_ticket.__parent__.validated_by,
152                amount
153                ))
154        return list(set(hitlist))
155
156    @grok.require('waeup.xmlrpc')
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
161        """
162        try:
163            session = int(session)
164            level = int(level)
165        except (TypeError, ValueError):
166            pass
167        if session in (None, '',0):
168            session= None
169        if level in (None, '',0):
170            level= None
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')
177        hitdict = {}
178        for course in courses:
179            coursetickets = cat.searchResults(
180                session=(session, session),
181                level=(level, level),
182                code=(course, course))
183            for c_ticket in coursetickets:
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,
188                        c_ticket.student.current_session,
189                        c_ticket.student.current_level,
190                        [c_ticket.code,])
191                else:
192                    hitdict[c_ticket.student.student_id][4].append(
193                        c_ticket.code,)
194        return hitdict
195
196    @grok.require('waeup.xmlrpc')
197    def get_student_info(self, identifier=None):
198        """3a. Who is the student with matriculation number / student id
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
208    @grok.require('waeup.Public')
209    def get_grad_student(self, identifier=None, email=None):
210        """Check if student record exist, check email address and
211        retrieve registration state.
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
219        transcript_released = False
220        if student.email == email:
221            correct_email = True
222        if student.state == GRADUATED:
223            has_graduated = True
224        if student.state == TRANSREL:
225            transcript_released = True
226        return [correct_email, has_graduated, transcript_released]
227
228    @grok.require('waeup.xmlrpc')
229    def get_student_passport(self, identifier=None):
230        """3b. Get passport picture of student with
231        matriculation number / student id.
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())
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
259
260    @grok.require('waeup.xmlrpc')
261    def check_student_credentials(self, username, password):
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)
284
285    @grok.require('waeup.xmlrpc')
286    def get_student_moodle_data(self, identifier=None):
287        """Returns student data to update user data and enroll user
288        in Moodle courses.
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                    )
299
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(
358                attr='finger%s.fpm' % num)
359            file_store.createFile(file_id, StringIO(val.data))
360            student.writeLogMessage(self, 'fingerprint stored')
361            result = True
362        return result
363
364    @grok.require('waeup.getBiometricData')
365    def get_student_fingerprints(self, identifier=None):
366        """Returns student fingerprint data if available.
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).
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,
388            fingerprints={},
389            img_name=None,
390            img=None,
391            )
392        file_store = getUtility(IExtFileStore)
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)]:
400            fp_file = file_store.getFileByContext(
401                student, attr='finger%s.fpm' % num)
402            if fp_file is not None:
403                result['fingerprints'][num] = xmlrpclib.Binary(fp_file.read())
404        return result
405
406    @grok.require('waeup.xmlrpc')
407    def get_bursary_data(self,
408            current_session=None, p_session=None, current_level=None, certcode=None,
409            current_mode=None, depcode=None):
410        """Returns bursary data of a subset of students.
411        """
412        hitlist = []
413        if not current_session:
414            current_session = None
415        else:
416            try:
417                current_session = int(current_session)
418            except (TypeError, ValueError):
419                hitlist.append(dict(error="Wrong parameters"))
420                return hitlist
421        if not current_level:
422            current_level = None
423        else:
424            try:
425                current_level = int(current_level)
426            except (TypeError, ValueError):
427                hitlist.append(dict(error="Wrong parameters"))
428                return hitlist
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
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                ))
452        payments = get_payments(results, paysession=p_session)
453        tz = getUtility(IKofaUtils).tzinfo
454        for payment in payments:
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)),
479                p_level=payment.p_level,
480                p_combi=payment.p_combi,
481                )
482              )
483        return hitlist
484
485    @grok.require('waeup.xmlrpc')
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')
508    def get_payments(self, days=3, paid=False, company=None):
509        """Returns the payment and payer data of paid or unpaid payment
510        tickets which have been created during the past days.
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        if paid:
519            payments = list(
520                cat.searchResults(p_id=(p_id_minus_days, p_id_now),
521                                  p_state=('paid', 'paid')))
522        else:
523            payments = list(
524                cat.searchResults(p_id=(p_id_minus_days, p_id_now),
525                                  p_state=('unpaid', 'unpaid')))
526            payments += list(
527                cat.searchResults(p_id=(p_id_minus_days, p_id_now),
528                                  p_state=('failed', 'failed')))
529        hitlist = []
530        if company:
531            for payment in payments:
532                if company == getattr(payment, 'r_company', None):
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,
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,
544                        )
545                      )
546            return hitlist
547        for payment in payments:
548            hitlist.append(dict(
549                p_id=payment.p_id,
550                amount_auth=payment.amount_auth,
551                p_category=payment.p_category,
552                display_item=payment.display_item,
553                p_session=payment.p_session,
554                p_state=payment.p_state,
555                r_company=getattr(payment, 'r_company', None),
556                id = IPayer(payment).id,
557                matric_number = IPayer(payment).matric_number,
558                fullname = IPayer(payment).display_fullname,
559                )
560              )
561        return hitlist
562
563    # Data requested by the WAeUP team
564
565    @grok.require('waeup.xmlrpc')
566    def get_student_base_data(self,
567            current_session=None, current_level=None, certcode=None,
568            current_mode=None, faccode=None, depcode=None, p_session=None):
569        """Returns base data of a subset of students.
570        """
571        hitlist = []
572        if not current_session:
573            current_session = None
574        else:
575            try:
576                current_session = int(current_session)
577            except (TypeError, ValueError):
578                hitlist.append(dict(error="Wrong parameters"))
579                return hitlist
580        if not current_level:
581            current_level = None
582        else:
583            try:
584                current_level = int(current_level)
585            except (TypeError, ValueError):
586                hitlist.append(dict(error="Wrong parameters"))
587                return hitlist
588        if not depcode:
589            depcode = None
590        if not certcode:
591            certcode = None
592        if not current_mode:
593            current_mode = None
594        cat = queryUtility(ICatalog, name='students_catalog')
595        results = cat.searchResults(
596                current_session=(current_session, current_session),
597                current_level=(current_level, current_level),
598                certcode=(certcode, certcode),
599                current_mode=(current_mode, current_mode),
600                faccode=(depcode, depcode),
601                depcode=(depcode, depcode),
602                )
603        if len(results) > 2000:
604            hitlist.append(dict(error="Too many objects (500 max)"))
605            return hitlist
606        for student in results:
607            hitlist.append(dict(
608                student_id=student.student_id,
609                matric_number=student.matric_number,
610                reg_number=student.reg_number,
611                firstname=student.firstname,
612                middlename=student.middlename,
613                lastname=student.lastname,
614                state=student.state,
615                current_session=student.current_session,
616                entry_session=student.entry_session,
617                entry_mode=student.entry_mode,
618                faccode=student.faccode,
619                depcode=student.depcode,
620                certcode=student.certcode,
621                sex=student.sex,
622                )
623            )
624        return hitlist
Note: See TracBrowser for help on using the repository browser.