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

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

Increase number of exportable students.

  • Property svn:keywords set to Id
File size: 22.1 KB
Line 
1## $Id: webservices.py 17805 2024-05-29 11:20:51Z 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, current_level=None, certcode=None,
409            current_mode=None, depcode=None, p_session=None):
410        """Returns bursary data of a subset of students.
411        """
412        if not current_session:
413            current_session = None
414        if not current_level:
415            current_level = None
416        if not depcode:
417            depcode = None
418        if not certcode:
419            certcode = None
420        if not current_mode:
421            current_mode = None
422        hitlist = []
423        cat = queryUtility(ICatalog, name='students_catalog')
424        results = list(
425            cat.searchResults(
426                current_session=(current_session, current_session),
427                current_level=(current_level, current_level),
428                certcode=(certcode, certcode),
429                current_mode=(current_mode, current_mode),
430                depcode=(depcode, depcode),
431                ))
432        payments = get_payments(results, paysession=p_session)
433        tz = getUtility(IKofaUtils).tzinfo
434        for payment in payments:
435            hitlist.append(dict(
436                student_id=payment.student.student_id,
437                matric_number=payment.student.matric_number,
438                reg_number=payment.student.reg_number,
439                firstname=payment.student.firstname,
440                middlename=payment.student.middlename,
441                lastname=payment.student.lastname,
442                state=payment.student.state,
443                current_session=payment.student.current_session,
444                entry_session=payment.student.entry_session,
445                entry_mode=payment.student.entry_mode,
446                faccode=payment.student.faccode,
447                depcode=payment.student.depcode,
448                certcode=payment.student.certcode,
449                p_id=payment.p_id,
450                amount_auth=payment.amount_auth,
451                p_category=payment.p_category,
452                display_item=payment.display_item,
453                p_session=payment.p_session,
454                p_state=payment.p_state,
455                creation_date=str('%s#' % to_timezone(
456                    payment.creation_date, tz)),
457                payment_date=str('%s#' % to_timezone(
458                    payment.payment_date, tz)),
459                )
460              )
461        return hitlist
462
463    @grok.require('waeup.xmlrpc')
464    def get_payment(self, p_id='non_existent'):
465        """Returns payment and payer data of payment tickets with specific p_id.
466        """
467        cat = getUtility(ICatalog, name='payments_catalog')
468        result = list(cat.searchResults(p_id=(p_id, p_id)))
469        if not len(result):
470            return None
471        payment =  result[0]
472        return dict(
473                p_id=payment.p_id,
474                amount_auth=payment.amount_auth,
475                p_category=payment.p_category,
476                display_item=payment.display_item,
477                p_session=payment.p_session,
478                p_state=payment.p_state,
479                r_company=getattr(payment, 'r_company', None),
480                id = IPayer(payment).id,
481                matric_number = IPayer(payment).matric_number,
482                fullname = IPayer(payment).display_fullname,
483                )
484
485    @grok.require('waeup.xmlrpc')
486    def get_unpaid_payments(self, days=3, company=None):
487        """Returns the payment and payer data of unpaid payment
488        tickets which have been created during the past days.
489        """
490        days_in_seconds = 86400 * int(days)
491        timestamp_now = ("%d" % int(time()*10000))[1:]
492        timestamp_now_minus_days = ("%d" % int((time()-days_in_seconds)*10000))[1:]
493        p_id_now = ("p%s" % timestamp_now)
494        p_id_minus_days = ("p%s" % timestamp_now_minus_days)
495        cat = getUtility(ICatalog, name='payments_catalog')
496        payments = list(
497            cat.searchResults(p_id=(p_id_minus_days, p_id_now),
498                              p_state=('unpaid', 'unpaid')))
499        payments += list(
500            cat.searchResults(p_id=(p_id_minus_days, p_id_now),
501                              p_state=('failed', 'failed')))
502        hitlist = []
503        if company:
504            for payment in payments:
505                if company == getattr(payment, 'r_company', None):
506                    hitlist.append(dict(
507                        p_id=payment.p_id,
508                        amount_auth=payment.amount_auth,
509                        p_category=payment.p_category,
510                        display_item=payment.display_item,
511                        p_session=payment.p_session,
512                        p_state=payment.p_state,
513                        r_company=getattr(payment, 'r_company', None),
514                        id = IPayer(payment).id,
515                        matric_number = IPayer(payment).matric_number,
516                        fullname = IPayer(payment).display_fullname,
517                        )
518                      )
519            return hitlist
520        for payment in payments:
521            hitlist.append(dict(
522                p_id=payment.p_id,
523                amount_auth=payment.amount_auth,
524                p_category=payment.p_category,
525                display_item=payment.display_item,
526                p_session=payment.p_session,
527                p_state=payment.p_state,
528                r_company=getattr(payment, 'r_company', None),
529                id = IPayer(payment).id,
530                matric_number = IPayer(payment).matric_number,
531                fullname = IPayer(payment).display_fullname,
532                )
533              )
534        return hitlist
535
536    # Data requested by the WAeUP team
537
538    @grok.require('waeup.xmlrpc')
539    def get_student_base_data(self,
540            current_session=None, current_level=None, certcode=None,
541            current_mode=None, faccode=None, depcode=None, p_session=None):
542        """Returns base data of a subset of students.
543        """
544        hitlist = []
545        try:
546            current_session = int(current_session)
547            current_level = int(current_level)
548        except (TypeError, ValueError):
549            hitlist.append(dict(error="Wrong parameters"))
550            return hitlist
551        cat = queryUtility(ICatalog, name='students_catalog')
552        results = cat.searchResults(
553                current_session=(current_session, current_session),
554                current_level=(current_level, current_level),
555                certcode=(certcode, certcode),
556                current_mode=(current_mode, current_mode),
557                faccode=(depcode, depcode),
558                depcode=(depcode, depcode),
559                )
560        if len(results) > 2000:
561            hitlist.append(dict(error="Too many objects (500 max)"))
562            return hitlist
563        for student in results:
564            hitlist.append(dict(
565                student_id=student.student_id,
566                matric_number=student.matric_number,
567                reg_number=student.reg_number,
568                firstname=student.firstname,
569                middlename=student.middlename,
570                lastname=student.lastname,
571                state=student.state,
572                current_session=student.current_session,
573                entry_session=student.entry_session,
574                entry_mode=student.entry_mode,
575                faccode=student.faccode,
576                depcode=student.depcode,
577                certcode=student.certcode,
578                sex=student.sex,
579                )
580            )
581        return hitlist
Note: See TracBrowser for help on using the repository browser.