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

Last change on this file since 16303 was 15416, checked in by Henrik Bettermann, 6 years ago

Backup deleted graduated student data somewhere else to ease graduated student data migration.

  • Property svn:keywords set to Id
File size: 18.3 KB
Line 
1## $Id: test_student.py 15416 2019-05-21 08:02:51Z henrik $
2##
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.
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##
18"""Tests for students and related.
19"""
20import os
21import re
22import unittest
23import grok
24from cStringIO import StringIO
25from datetime import tzinfo
26from zope.component import getUtility, queryUtility, createObject
27from zope.catalog.interfaces import ICatalog
28from zope.component.interfaces import IFactory
29from zope.event import notify
30from zope.interface import verify
31from zope.schema.interfaces import RequiredMissing
32from hurry.workflow.interfaces import IWorkflowState
33from waeup.kofa.interfaces import IExtFileStore, IFileStoreNameChooser
34from waeup.kofa.students.student import (
35    Student, StudentFactory, handle_student_removed, path_from_studid)
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 (
41    IStudent, IStudentStudyCourse, IStudentPaymentsContainer,
42    IStudentAccommodation, IStudentStudyLevel, ICourseTicket, IBedTicket,
43    IStudentNavigation, IStudentsUtils)
44from waeup.kofa.students.tests.test_batching import StudentImportExportSetup
45from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
46from waeup.kofa.university.department import Department
47
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(
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')
68        return
69
70class StudentTest(FunctionalTestCase):
71
72    layer = FunctionalLayer
73
74    def setUp(self):
75        super(StudentTest, self).setUp()
76        self.student = Student()
77        self.student.firstname = u'Anna'
78        self.student.lastname = u'Tester'
79        self.studycourse = StudentStudyCourse()
80        self.studylevel = StudentStudyLevel()
81        self.studylevel.level_session = 2015
82        self.courseticket = CourseTicket()
83        self.payments = StudentPaymentsContainer()
84        self.accommodation = StudentAccommodation()
85        self.bedticket = BedTicket()
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)
94        verify.verifyClass(IStudentNavigation, Student)
95        verify.verifyObject(IStudent, self.student)
96        verify.verifyObject(IStudentNavigation, self.student)
97
98        verify.verifyClass(IStudentStudyCourse, StudentStudyCourse)
99        verify.verifyClass(IStudentNavigation, StudentStudyCourse)
100        verify.verifyObject(IStudentStudyCourse, self.studycourse)
101        verify.verifyObject(IStudentNavigation, self.studycourse)
102
103        verify.verifyClass(IStudentStudyLevel, StudentStudyLevel)
104        verify.verifyClass(IStudentNavigation, StudentStudyLevel)
105        verify.verifyObject(IStudentStudyLevel, self.studylevel)
106        verify.verifyObject(IStudentNavigation, self.studylevel)
107
108        verify.verifyClass(ICourseTicket, CourseTicket)
109        verify.verifyClass(IStudentNavigation, CourseTicket)
110        verify.verifyObject(ICourseTicket, self.courseticket)
111        verify.verifyObject(IStudentNavigation, self.courseticket)
112
113        verify.verifyClass(IStudentPaymentsContainer, StudentPaymentsContainer)
114        verify.verifyClass(IStudentNavigation, StudentPaymentsContainer)
115        verify.verifyObject(IStudentPaymentsContainer, self.payments)
116        verify.verifyObject(IStudentNavigation, self.payments)
117
118        verify.verifyClass(IStudentAccommodation, StudentAccommodation)
119        verify.verifyClass(IStudentNavigation, StudentAccommodation)
120        verify.verifyObject(IStudentAccommodation, self.accommodation)
121        verify.verifyObject(IStudentNavigation, self.accommodation)
122
123        verify.verifyClass(IBedTicket, BedTicket)
124        verify.verifyClass(IStudentNavigation, BedTicket)
125        verify.verifyObject(IBedTicket, self.bedticket)
126        verify.verifyObject(IStudentNavigation, self.bedticket)
127        return
128
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(
136            TypeError, studylevel.addCourseTicket, department, department)
137
138    def test_booking_date(self):
139        isinstance(self.bedticket.booking_date.tzinfo, tzinfo)
140        self.assertEqual(self.bedticket.booking_date.tzinfo, None)
141        return
142
143    def test_maint_payment_made(self):
144        self.assertFalse(self.bedticket.maint_payment_made)
145        return
146
147
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()
162        return
163
164    def create_passport_img(self, student):
165        # create some passport file for `student`
166        storage = getUtility(IExtFileStore)
167        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
168        self.image_contents = open(image_path, 'rb').read()
169        file_id = IFileStoreNameChooser(student).chooseName(
170            attr='passport.jpg')
171        storage.createFile(file_id, StringIO(self.image_contents))
172
173    def test_backup_single_student_data(self):
174        # when a single student is removed, the data is backed up.
175        self.setup_student(self.student)
176        # Add a fake image
177        self.create_passport_img(self.student)
178        handle_student_removed(self.student, None)
179        del_dir = self.app['datacenter'].deleted_path
180        del_img_path = os.path.join(
181            del_dir, 'media', 'students', '00110', 'A111111',
182            'passport_A111111.jpg')
183        # The image was copied over
184        self.assertTrue(os.path.isfile(del_img_path))
185        self.assertEqual(
186            open(del_img_path, 'rb').read(),
187            self.image_contents)
188        # The student data were put into CSV files
189        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
190            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
191        for name in STUDENT_BACKUP_EXPORTER_NAMES:
192            csv_path = os.path.join(del_dir, '%s.csv' % name)
193            self.assertTrue(os.path.isfile(csv_path))
194            contents = open(csv_path, 'rb').read().split('\r\n')
195            # We expect 3 lines output including a linebreak at end of file.
196            self.assertEqual(len(contents), 3)
197        return
198
199    def test_backup_single_graduated_data(self):
200        # when a single graduated student is removed, the data is backed up
201        # somewhere else
202        self.setup_student(self.student)
203        IWorkflowState(self.student).setState('graduated')
204        # Add a fake image
205        self.create_passport_img(self.student)
206        handle_student_removed(self.student, None)
207        del_dir = self.app['datacenter'].graduated_path
208        del_img_path = os.path.join(
209            del_dir, 'media', 'students', '00110', 'A111111',
210            'passport_A111111.jpg')
211        # The image was copied over
212        self.assertTrue(os.path.isfile(del_img_path))
213        self.assertEqual(
214            open(del_img_path, 'rb').read(),
215            self.image_contents)
216        # The student data were put into CSV files
217        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
218            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
219        for name in STUDENT_BACKUP_EXPORTER_NAMES:
220            csv_path = os.path.join(del_dir, '%s.csv' % name)
221            self.assertTrue(os.path.isfile(csv_path))
222            contents = open(csv_path, 'rb').read().split('\r\n')
223            # We expect 3 lines output including a linebreak at end of file.
224            self.assertEqual(len(contents), 3)
225        return
226
227    def test_backup_append_csv(self):
228        # when several students are removed, existing CSVs are appended
229        self.setup_student(self.student)
230        # Add a fake image
231        self.create_passport_img(self.student)
232        del_dir = self.app['datacenter'].deleted_path
233        # put fake data into students.csv with trailing linebreak
234        students_csv = os.path.join(del_dir, 'students.csv')
235        open(students_csv, 'wb').write('line1\r\nline2\r\n')
236        handle_student_removed(self.student, None)
237        contents = open(students_csv, 'rb').read().split('\r\n')
238        # there should be 4 lines in result csv (including trailing linebreak)
239        self.assertEqual(len(contents), 4)
240        return
241
242    def test_old_files_removed(self):
243        # make sure old files are not accessible any more
244        self.setup_student(self.student)
245        # Add a fake image
246        self.create_passport_img(self.student)
247        # make sure we can access the image before removal
248        file_store = getUtility(IExtFileStore)
249        image = file_store.getFileByContext(self.student, attr='passport.jpg')
250        self.assertTrue(image is not None)
251
252        # remove image (hopefully)
253        handle_student_removed(self.student, None)
254
255        # the is not accessible anymore
256        image = file_store.getFileByContext(self.student, attr='passport.jpg')
257        self.assertEqual(image, None)
258        return
259
260    def test_csv_file_entries_have_timestamp(self):
261        # each row in written csv files has a ``del_date`` column to
262        # tell when the associated student was deleted
263        self.setup_student(self.student)
264        del_dir = self.app['datacenter'].deleted_path
265        students_csv = os.path.join(del_dir, 'students.csv')
266        handle_student_removed(self.student, None)
267        contents = open(students_csv, 'rb').read().split('\r\n')
268        # the CSV header ends with a ``del_date`` column
269        self.assertTrue(contents[0].endswith(',del_date'))
270        # each line ends with an UTC timestamp
271        timestamp = contents[1][-23:]
272        self.assertTrue(re.match(
273            '^\d\d-\d\d-\d\d \d\d:\d\d:\d\d\+00:00$', timestamp))
274        return
275
276class StudentTransferTests(StudentImportExportSetup):
277
278    layer = FunctionalLayer
279
280    def setUp(self):
281        super(StudentTransferTests, self).setUp()
282
283        # Add additional certificate
284        self.certificate2 = createObject('waeup.Certificate')
285        self.certificate2.code = 'CERT2'
286        self.certificate2.application_category = 'basic'
287        self.certificate2.start_level = 200
288        self.certificate2.end_level = 500
289        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
290            self.certificate2)
291
292        # Add student with subobjects
293        student = Student()
294        self.app['students'].addStudent(student)
295        student = self.setup_student(student)
296        notify(grok.ObjectModifiedEvent(student))
297        self.student = self.app['students'][student.student_id]
298        return
299
300    def test_transfer_student(self):
301        self.assertRaises(
302            RequiredMissing, self.student.transfer, self.certificate2)
303        error = self.student.transfer(self.certificate2, current_session=1000)
304        self.assertTrue(error == -1)
305        error = self.student.transfer(self.certificate2, current_session=2013)
306        self.assertTrue(error == None)
307        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
308        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
309        self.assertEqual(self.student['studycourse_1'].current_session, 2012)
310        self.assertEqual(self.student['studycourse'].current_session, 2013)
311        self.assertEqual(self.student['studycourse'].entry_session,
312            self.student['studycourse_1'].entry_session)
313        self.assertEqual(self.student['studycourse_1'].__name__, 'studycourse_1')
314        logfile = os.path.join(
315            self.app['datacenter'].storage, 'logs', 'students.log')
316        logcontent = open(logfile).read()
317        self.assertTrue('system - K1000000 - transferred from CERT1 to CERT2'
318            in logcontent)
319        messages = ' '.join(self.student.history.messages)
320        self.assertMatches(
321            '...<YYYY-MM-DD hh:mm:ss> UTC - '
322            'Transferred from CERT1 to CERT2 by system', messages)
323
324        # The students_catalog has been updated.
325        cat = queryUtility(ICatalog, name='students_catalog')
326        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
327        results = [x for x in results]
328        self.assertEqual(len(results), 0)
329        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
330        results = [x for x in results]
331        self.assertEqual(len(results), 1)
332        assert results[0] is self.app['students'][self.student.student_id]
333        results = cat.searchResults(current_session=(2013,2013))
334        results = [x for x in results]
335        self.assertEqual(len(results), 1)
336        assert results[0] is self.app['students'][self.student.student_id]
337
338        # studycourse_1 is the previous course.
339        self.assertFalse(self.student['studycourse'].is_previous)
340        self.assertTrue(self.student['studycourse_1'].is_previous)
341
342        # Students can be transferred (only) two times.
343        error = self.student.transfer(self.certificate,
344            current_session=2013)
345        self.assertTrue(error == None)
346        error = self.student.transfer(self.certificate2,
347            current_session=2013)
348        self.assertTrue(error == -3)
349        self.assertEqual([i for i in self.student.keys()],
350            [u'accommodation', u'payments', u'studycourse',
351             u'studycourse_1', u'studycourse_2'])
352
353        # The studycourse with highest order number is the previous
354        # course.
355        self.assertFalse(self.student['studycourse'].is_previous)
356        self.assertFalse(self.student['studycourse_1'].is_previous)
357        self.assertTrue(self.student['studycourse_2'].is_previous)
358
359        # The students_catalog has been updated again.
360        cat = queryUtility(ICatalog, name='students_catalog')
361        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
362        results = [x for x in results]
363        self.assertEqual(len(results), 1)
364        assert results[0] is self.app['students'][self.student.student_id]
365
366        # Previous transfer can be successively reverted.
367        self.assertEqual(self.student['studycourse_2'].certificate.code, 'CERT2')
368        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
369        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
370        error = self.student.revert_transfer()
371        self.assertTrue(error == None)
372        self.assertEqual([i for i in self.student.keys()],
373            [u'accommodation', u'payments', u'studycourse',
374             u'studycourse_1'])
375        self.assertEqual(self.student['studycourse_1'].certificate.code, 'CERT1')
376        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT2')
377        # The students_catalog has been updated again.
378        cat = queryUtility(ICatalog, name='students_catalog')
379        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
380        results = [x for x in results]
381        self.assertEqual(len(results), 1)
382        assert results[0] is self.app['students'][self.student.student_id]
383        error = self.student.revert_transfer()
384        self.assertTrue(error == None)
385        self.assertEqual([i for i in self.student.keys()],
386            [u'accommodation', u'payments', u'studycourse'])
387        self.assertEqual(self.student['studycourse'].certificate.code, 'CERT1')
388        error = self.student.revert_transfer()
389        self.assertTrue(error == -1)
390        # The students_catalog has been updated again.
391        cat = queryUtility(ICatalog, name='students_catalog')
392        results = cat.searchResults(certcode=('CERT1', 'CERT1'))
393        results = [x for x in results]
394        self.assertEqual(len(results), 1)
395        assert results[0] is self.app['students'][self.student.student_id]
396        results = cat.searchResults(certcode=('CERT2', 'CERT2'))
397        results = [x for x in results]
398        self.assertEqual(len(results), 0)
399        logcontent = open(logfile).read()
400        self.assertTrue('system - K1000000 - transfer reverted'
401            in logcontent)
402        messages = ' '.join(self.student.history.messages)
403        self.assertTrue('Transfer reverted by system' in messages)
404        return
405
406class StudentFactoryTest(FunctionalTestCase):
407
408    layer = FunctionalLayer
409
410    def setUp(self):
411        super(StudentFactoryTest, self).setUp()
412        self.factory = StudentFactory()
413
414    def tearDown(self):
415        super(StudentFactoryTest, self).tearDown()
416
417    def test_interfaces(self):
418        verify.verifyClass(IFactory, StudentFactory)
419        verify.verifyObject(IFactory, self.factory)
420
421    def test_factory(self):
422        obj = self.factory()
423        assert isinstance(obj, Student)
424
425    def test_getInterfaces(self):
426        implemented_by = self.factory.getInterfaces()
427        assert implemented_by.isOrExtends(IStudent)
Note: See TracBrowser for help on using the repository browser.