source: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_student.py

Last change on this file was 16827, checked in by Henrik Bettermann, 3 years ago

Add exporters for previous study course data.

  • Property svn:keywords set to Id
File size: 20.6 KB
RevLine 
[7193]1## $Id: test_student.py 16827 2022-02-22 12:48:36Z henrik $
2##
[6621]3## Copyright (C) 2011 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.
[7193]8##
[6621]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.
[7193]13##
[6621]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##
18"""Tests for students and related.
19"""
[8403]20import os
21import re
[8448]22import unittest
[9131]23import grok
[8403]24from cStringIO import StringIO
[8181]25from datetime import tzinfo
[9131]26from zope.component import getUtility, queryUtility, createObject
27from zope.catalog.interfaces import ICatalog
[6621]28from zope.component.interfaces import IFactory
[9131]29from zope.event import notify
[6752]30from zope.interface import verify
[9131]31from zope.schema.interfaces import RequiredMissing
[15416]32from hurry.workflow.interfaces import IWorkflowState
[8403]33from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
34from waeup.kofa.students.student import (
[8448]35    Student, StudentFactory, handle_student_removed, path_from_studid)
[7811]36from waeup.kofa.students.studycourse import StudentStudyCourse
37from waeup.kofa.students.studylevel import StudentStudyLevel, CourseTicket
38from waeup.kofa.students.payments import StudentPaymentsContainer
39from waeup.kofa.students.accommodation import StudentAccommodation, BedTicket
40from waeup.kofa.students.interfaces import (
[8403]41    IStudent, IStudentStudyCourse, IStudentPaymentsContainer,
[8735]42    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, IBedTicket,
[12104]43    IStudentNavigation, IStudentsUtils)
[8403]44from waeup.kofa.students.tests.test_batching import StudentImportExportSetup
[7811]45from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
46from waeup.kofa.university.department import Department
[6621]47
[8448]48class HelperTests(unittest.TestCase):
49    # Tests for helper functions in student module.
50
51    def test_path_from_studid(self):
52        # make sure we get predictable paths from student ids.
53        self.assertEqual(
54            path_from_studid('K1000000'), u'01000/K1000000')
55        self.assertEqual(
[8452]56            path_from_studid('K1234567'), u'01234/K1234567')
57        self.assertEqual(
58            path_from_studid('K12345678'), u'12345/K12345678')
59        # The algorithm works also for overlong numbers, just to be
60        # sure.
61        self.assertEqual(
62            path_from_studid('K123456789'), u'123456/K123456789')
63        # low numbers (< 10**6) are treated special: they get max. of
64        # 10,000 entries. That's mainly because of old students
65        # migrated into our portal.
66        self.assertEqual(
67            path_from_studid('KM123456'), u'00120/KM123456')
[8448]68        return
69
[6621]70class StudentTest(FunctionalTestCase):
71
72    layer = FunctionalLayer
73
74    def setUp(self):
75        super(StudentTest, self).setUp()
76        self.student = Student()
[7364]77        self.student.firstname = u'Anna'
78        self.student.lastname = u'Tester'
[6633]79        self.studycourse = StudentStudyCourse()
[6781]80        self.studylevel = StudentStudyLevel()
[13002]81        self.studylevel.level_session = 2015
[6795]82        self.courseticket = CourseTicket()
[6859]83        self.payments = StudentPaymentsContainer()
[6635]84        self.accommodation = StudentAccommodation()
[6989]85        self.bedticket = BedTicket()
[6621]86        return
87
88    def tearDown(self):
89        super(StudentTest, self).tearDown()
90        return
91
92    def test_interfaces(self):
93        verify.verifyClass(IStudent, Student)
[8735]94        verify.verifyClass(IStudentNavigation, Student)
[6621]95        verify.verifyObject(IStudent, self.student)
[8735]96        verify.verifyObject(IStudentNavigation, self.student)
97
[6633]98        verify.verifyClass(IStudentStudyCourse, StudentStudyCourse)
[8735]99        verify.verifyClass(IStudentNavigation, StudentStudyCourse)
[6633]100        verify.verifyObject(IStudentStudyCourse, self.studycourse)
[8735]101        verify.verifyObject(IStudentNavigation, self.studycourse)
102
103        verify.verifyClass(IStudentStudyLevel, StudentStudyLevel)
104        verify.verifyClass(IStudentNavigation, StudentStudyLevel)
[6781]105        verify.verifyObject(IStudentStudyLevel, self.studylevel)
[8735]106        verify.verifyObject(IStudentNavigation, self.studylevel)
107
108        verify.verifyClass(ICourseTicket, CourseTicket)
109        verify.verifyClass(IStudentNavigation, CourseTicket)
[6781]110        verify.verifyObject(ICourseTicket, self.courseticket)
[8735]111        verify.verifyObject(IStudentNavigation, self.courseticket)
112
[6859]113        verify.verifyClass(IStudentPaymentsContainer, StudentPaymentsContainer)
[8735]114        verify.verifyClass(IStudentNavigation, StudentPaymentsContainer)
[6859]115        verify.verifyObject(IStudentPaymentsContainer, self.payments)
[8735]116        verify.verifyObject(IStudentNavigation, self.payments)
117
[6635]118        verify.verifyClass(IStudentAccommodation, StudentAccommodation)
[8735]119        verify.verifyClass(IStudentNavigation, StudentAccommodation)
[6635]120        verify.verifyObject(IStudentAccommodation, self.accommodation)
[8735]121        verify.verifyObject(IStudentNavigation, self.accommodation)
122
[6989]123        verify.verifyClass(IBedTicket, BedTicket)
[8735]124        verify.verifyClass(IStudentNavigation, BedTicket)
[6989]125        verify.verifyObject(IBedTicket, self.bedticket)
[8735]126        verify.verifyObject(IStudentNavigation, self.bedticket)
[6621]127        return
128
[7077]129    def test_base(self):
130        department = Department()
131        studycourse = StudentStudyCourse()
132        self.assertRaises(
133            TypeError, studycourse.addStudentStudyLevel, department)
134        studylevel = StudentStudyLevel()
135        self.assertRaises(
[8920]136            TypeError, studylevel.addCourseTicket, department, department)
[7077]137
[8181]138    def test_booking_date(self):
139        isinstance(self.bedticket.booking_date.tzinfo, tzinfo)
[8194]140        self.assertEqual(self.bedticket.booking_date.tzinfo, None)
[8181]141        return
142
[13314]143    def test_maint_payment_made(self):
144        self.assertFalse(self.bedticket.maint_payment_made)
145        return
146
147
[8403]148class StudentRemovalTests(StudentImportExportSetup):
149    # Test handle_student_removed
150    #
151    # This is a complex action updating several CSV files and moving
152    # stored files to a backup location.
153    #
154    # These tests make no assumptions about the CSV files except that
155    # they contain a deletion timestamp at end of each data row
156
157    layer = FunctionalLayer
158
159    def setUp(self):
160        super(StudentRemovalTests, self).setUp()
161        self.setup_for_export()
[16827]162        self.certificate2 = createObject('waeup.Certificate')
163        self.certificate2.code = 'CERT2'
164        self.certificate2.application_category = 'basic'
165        self.certificate2.start_level = 200
166        self.certificate2.end_level = 500
167        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
168            self.certificate2)
[8403]169        return
170
171    def create_passport_img(self, student):
[8467]172        # create some passport file for `student`
[8403]173        storage = getUtility(IExtFileStore)
[8467]174        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
175        self.image_contents = open(image_path, 'rb').read()
[8403]176        file_id = IFileStoreNameChooser(student).chooseName(
177            attr='passport.jpg')
[8467]178        storage.createFile(file_id, StringIO(self.image_contents))
[8403]179
180    def test_backup_single_student_data(self):
181        # when a single student is removed, the data is backed up.
182        self.setup_student(self.student)
183        # Add a fake image
184        self.create_passport_img(self.student)
185        handle_student_removed(self.student, None)
186        del_dir = self.app['datacenter'].deleted_path
187        del_img_path = os.path.join(
[8467]188            del_dir, 'media', 'students', '00110', 'A111111',
[8403]189            'passport_A111111.jpg')
[15416]190        # The image was copied over
191        self.assertTrue(os.path.isfile(del_img_path))
192        self.assertEqual(
193            open(del_img_path, 'rb').read(),
194            self.image_contents)
195        # The student data were put into CSV files
196        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
197            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
198        for name in STUDENT_BACKUP_EXPORTER_NAMES:
[16827]199            if name[-2:-1] == '_':
200                continue
[15416]201            csv_path = os.path.join(del_dir, '%s.csv' % name)
202            self.assertTrue(os.path.isfile(csv_path))
203            contents = open(csv_path, 'rb').read().split('\r\n')
204            # We expect 3 lines output including a linebreak at end of file.
205            self.assertEqual(len(contents), 3)
206        return
[8403]207
[15416]208    def test_backup_single_graduated_data(self):
209        # when a single graduated student is removed, the data is backed up
210        # somewhere else
211        self.setup_student(self.student)
212        IWorkflowState(self.student).setState('graduated')
213        # Add a fake image
214        self.create_passport_img(self.student)
215        handle_student_removed(self.student, None)
216        del_dir = self.app['datacenter'].graduated_path
217        del_img_path = os.path.join(
218            del_dir, 'media', 'students', '00110', 'A111111',
219            'passport_A111111.jpg')
[8403]220        # The image was copied over
221        self.assertTrue(os.path.isfile(del_img_path))
222        self.assertEqual(
223            open(del_img_path, 'rb').read(),
[8467]224            self.image_contents)
[8403]225        # The student data were put into CSV files
[12971]226        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
227            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
228        for name in STUDENT_BACKUP_EXPORTER_NAMES:
[16827]229            if name[-2:-1] == '_':
230                continue
[8403]231            csv_path = os.path.join(del_dir, '%s.csv' % name)
232            self.assertTrue(os.path.isfile(csv_path))
233            contents = open(csv_path, 'rb').read().split('\r\n')
234            # We expect 3 lines output including a linebreak at end of file.
235            self.assertEqual(len(contents), 3)
236        return
237
[16827]238    def test_backup_single_graduated_transferred_data(self):
239        del_dir = self.app['datacenter'].graduated_path
240        # when a single graduated student is removed, the data is backed up
241        # somewhere else including previous study courses
242        self.setup_student(self.student)
243        error = self.student.transfer(self.certificate2, current_session=2013)
244        self.assertTrue(error == None)
245        IWorkflowState(self.student).setState('graduated')
246        handle_student_removed(self.student, None)
247        # The previous study course data were put into CSV files
248        name = 'studentstudycourses_1'
249        csv_path = os.path.join(del_dir, '%s.csv' % name)
250        self.assertTrue(os.path.isfile(csv_path))
251        contents = open(csv_path, 'rb').read().split('\r\n')
252        # We expect 3 lines output including a linebreak at end of file.
253        self.assertEqual(len(contents), 3)
254        name = 'studentstudycourses_2'
255        csv_path = os.path.join(del_dir, '%s.csv' % name)
256        self.assertTrue(os.path.isfile(csv_path))
257        contents = open(csv_path, 'rb').read().split('\r\n')
258        # We expect only 2 lines output including a linebreak at end of file.
259        self.assertEqual(len(contents), 2)
260        name = 'studentstudylevels_1'
261        csv_path = os.path.join(del_dir, '%s.csv' % name)
262        self.assertTrue(os.path.isfile(csv_path))
263        contents = open(csv_path, 'rb').read().split('\r\n')
264        # We expect 3 lines output including a linebreak at end of file.
265        self.assertEqual(len(contents), 3)
266        name = 'coursetickets_1'
267        csv_path = os.path.join(del_dir, '%s.csv' % name)
268        self.assertTrue(os.path.isfile(csv_path))
269        contents = open(csv_path, 'rb').read().split('\r\n')
270        # We expect 3 lines output including a linebreak at end of file.
271        self.assertEqual(len(contents), 3)
272        return
273
[8403]274    def test_backup_append_csv(self):
275        # when several students are removed, existing CSVs are appended
276        self.setup_student(self.student)
277        # Add a fake image
278        self.create_passport_img(self.student)
279        del_dir = self.app['datacenter'].deleted_path
280        # put fake data into students.csv with trailing linebreak
281        students_csv = os.path.join(del_dir, 'students.csv')
282        open(students_csv, 'wb').write('line1\r\nline2\r\n')
283        handle_student_removed(self.student, None)
284        contents = open(students_csv, 'rb').read().split('\r\n')
285        # there should be 4 lines in result csv (including trailing linebreak)
286        self.assertEqual(len(contents), 4)
287        return
288
289    def test_old_files_removed(self):
290        # make sure old files are not accessible any more
291        self.setup_student(self.student)
292        # Add a fake image
293        self.create_passport_img(self.student)
294        # make sure we can access the image before removal
295        file_store = getUtility(IExtFileStore)
296        image = file_store.getFileByContext(self.student, attr='passport.jpg')
297        self.assertTrue(image is not None)
298
299        # remove image (hopefully)
300        handle_student_removed(self.student, None)
301
302        # the is not accessible anymore
303        image = file_store.getFileByContext(self.student, attr='passport.jpg')
304        self.assertEqual(image, None)
305        return
306
307    def test_csv_file_entries_have_timestamp(self):
308        # each row in written csv files has a ``del_date`` column to
309        # tell when the associated student was deleted
310        self.setup_student(self.student)
311        del_dir = self.app['datacenter'].deleted_path
312        students_csv = os.path.join(del_dir, 'students.csv')
313        handle_student_removed(self.student, None)
314        contents = open(students_csv, 'rb').read().split('\r\n')
315        # the CSV header ends with a ``del_date`` column
316        self.assertTrue(contents[0].endswith(',del_date'))
317        # each line ends with an UTC timestamp
318        timestamp = contents[1][-23:]
319        self.assertTrue(re.match(
320            '^\d\d-\d\d-\d\d \d\d:\d\d:\d\d\+00:00$', timestamp))
321        return
322
[9131]323class StudentTransferTests(StudentImportExportSetup):
[8403]324
[9131]325    layer = FunctionalLayer
326
327    def setUp(self):
328        super(StudentTransferTests, self).setUp()
329
330        # Add additional certificate
331        self.certificate2 = createObject('waeup.Certificate')
332        self.certificate2.code = 'CERT2'
333        self.certificate2.application_category = 'basic'
334        self.certificate2.start_level = 200
335        self.certificate2.end_level = 500
336        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
337            self.certificate2)
338
339        # Add student with subobjects
340        student = Student()
341        self.app['students'].addStudent(student)
342        student = self.setup_student(student)
343        notify(grok.ObjectModifiedEvent(student))
344        self.student = self.app['students'][student.student_id]
345        return
346
347    def test_transfer_student(self):
348        self.assertRaises(
349            RequiredMissing, self.student.transfer, self.certificate2)
[9137]350        error = self.student.transfer(self.certificate2, current_session=1000)
351        self.assertTrue(error == -1)
352        error = self.student.transfer(self.certificate2, current_session=2013)
353        self.assertTrue(error == None)
[9131]354        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
355        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
356        self.assertEqual(self.student['studycourse_1'].current_session, 2012)
357        self.assertEqual(self.student['studycourse'].current_session, 2013)
[9136]358        self.assertEqual(self.student['studycourse'].entry_session,
359            self.student['studycourse_1'].entry_session)
[9131]360        self.assertEqual(self.student['studycourse_1'].__name__, 'studycourse_1')
361        logfile = os.path.join(
362            self.app['datacenter'].storage, 'logs', 'students.log')
363        logcontent = open(logfile).read()
364        self.assertTrue('system - K1000000 - transferred from CERT1 to CERT2'
365            in logcontent)
[9137]366        messages = ' '.join(self.student.history.messages)
367        self.assertMatches(
368            '...<YYYY-MM-DD hh:mm:ss> UTC - '
369            'Transferred from CERT1 to CERT2 by system', messages)
[9131]370
371        # The students_catalog has been updated.
372        cat = queryUtility(ICatalog, name='students_catalog')
373        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
374        results = [x for x in results]
375        self.assertEqual(len(results), 0)
376        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
377        results = [x for x in results]
378        self.assertEqual(len(results), 1)
379        assert results[0] is self.app['students'][self.student.student_id]
380        results = cat.searchResults(current_session=(2013,2013))
381        results = [x for x in results]
382        self.assertEqual(len(results), 1)
383        assert results[0] is self.app['students'][self.student.student_id]
384
[10059]385        # studycourse_1 is the previous course.
386        self.assertFalse(self.student['studycourse'].is_previous)
387        self.assertTrue(self.student['studycourse_1'].is_previous)
388
[9131]389        # Students can be transferred (only) two times.
[9137]390        error = self.student.transfer(self.certificate,
[9136]391            current_session=2013)
[9137]392        self.assertTrue(error == None)
393        error = self.student.transfer(self.certificate2,
[9136]394            current_session=2013)
[9138]395        self.assertTrue(error == -3)
[9131]396        self.assertEqual([i for i in self.student.keys()],
397            [u'accommodation', u'payments', u'studycourse',
398             u'studycourse_1', u'studycourse_2'])
399
[10059]400        # The studycourse with highest order number is the previous
401        # course.
402        self.assertFalse(self.student['studycourse'].is_previous)
403        self.assertFalse(self.student['studycourse_1'].is_previous)
404        self.assertTrue(self.student['studycourse_2'].is_previous)
405
[9131]406        # The students_catalog has been updated again.
407        cat = queryUtility(ICatalog, name='students_catalog')
408        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
409        results = [x for x in results]
410        self.assertEqual(len(results), 1)
411        assert results[0] is self.app['students'][self.student.student_id]
[10054]412
413        # Previous transfer can be successively reverted.
414        self.assertEqual(self.student['studycourse_2'].certificate.code, 'CERT2')
415        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
416        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
417        error = self.student.revert_transfer()
418        self.assertTrue(error == None)
419        self.assertEqual([i for i in self.student.keys()],
420            [u'accommodation', u'payments', u'studycourse',
421             u'studycourse_1'])
422        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
423        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
424        # The students_catalog has been updated again.
425        cat = queryUtility(ICatalog, name='students_catalog')
426        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
427        results = [x for x in results]
428        self.assertEqual(len(results), 1)
429        assert results[0] is self.app['students'][self.student.student_id]
430        error = self.student.revert_transfer()
431        self.assertTrue(error == None)
432        self.assertEqual([i for i in self.student.keys()],
433            [u'accommodation', u'payments', u'studycourse'])
434        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
435        error = self.student.revert_transfer()
436        self.assertTrue(error == -1)
437        # The students_catalog has been updated again.
438        cat = queryUtility(ICatalog, name='students_catalog')
439        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
440        results = [x for x in results]
441        self.assertEqual(len(results), 1)
442        assert results[0] is self.app['students'][self.student.student_id]
443        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
444        results = [x for x in results]
445        self.assertEqual(len(results), 0)
446        logcontent = open(logfile).read()
447        self.assertTrue('system - K1000000 - transfer reverted'
448            in logcontent)
449        messages = ' '.join(self.student.history.messages)
450        self.assertTrue('Transfer reverted by system' in messages)
[9131]451        return
452
[6621]453class StudentFactoryTest(FunctionalTestCase):
454
455    layer = FunctionalLayer
456
457    def setUp(self):
[6751]458        super(StudentFactoryTest, self).setUp()
[6621]459        self.factory = StudentFactory()
460
461    def tearDown(self):
[6751]462        super(StudentFactoryTest, self).tearDown()
[6621]463
464    def test_interfaces(self):
465        verify.verifyClass(IFactory, StudentFactory)
466        verify.verifyObject(IFactory, self.factory)
467
468    def test_factory(self):
469        obj = self.factory()
470        assert isinstance(obj, Student)
471
472    def test_getInterfaces(self):
473        implemented_by = self.factory.getInterfaces()
474        assert implemented_by.isOrExtends(IStudent)
Note: See TracBrowser for help on using the repository browser.