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

Last change on this file since 17610 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
Line 
1## $Id: test_student.py 16827 2022-02-22 12:48:36Z 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        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)
169        return
170
171    def create_passport_img(self, student):
172        # create some passport file for `student`
173        storage = getUtility(IExtFileStore)
174        image_path = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
175        self.image_contents = open(image_path, 'rb').read()
176        file_id = IFileStoreNameChooser(student).chooseName(
177            attr='passport.jpg')
178        storage.createFile(file_id, StringIO(self.image_contents))
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(
188            del_dir, 'media', 'students', '00110', 'A111111',
189            'passport_A111111.jpg')
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:
199            if name[-2:-1] == '_':
200                continue
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
207
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')
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(),
224            self.image_contents)
225        # The student data were put into CSV files
226        STUDENT_BACKUP_EXPORTER_NAMES = getUtility(
227            IStudentsUtils).STUDENT_BACKUP_EXPORTER_NAMES
228        for name in STUDENT_BACKUP_EXPORTER_NAMES:
229            if name[-2:-1] == '_':
230                continue
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
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
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
323class StudentTransferTests(StudentImportExportSetup):
324
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)
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)
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)
358        self.assertEqual(self.student['studycourse'].entry_session,
359            self.student['studycourse_1'].entry_session)
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)
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)
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
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
389        # Students can be transferred (only) two times.
390        error = self.student.transfer(self.certificate,
391            current_session=2013)
392        self.assertTrue(error == None)
393        error = self.student.transfer(self.certificate2,
394            current_session=2013)
395        self.assertTrue(error == -3)
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
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
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]
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)
451        return
452
453class StudentFactoryTest(FunctionalTestCase):
454
455    layer = FunctionalLayer
456
457    def setUp(self):
458        super(StudentFactoryTest, self).setUp()
459        self.factory = StudentFactory()
460
461    def tearDown(self):
462        super(StudentFactoryTest, self).tearDown()
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.