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

Last change on this file since 13225 was 13002, checked in by Henrik Bettermann, 9 years ago

More docs. Complete IStudentStudyCourse and IStudentStudyLevel interfaces and adjust unit tests.

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