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

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

Add StudentUnpaidPaymentExporter? to export only unpaid tickets. This exporter is designed for finding and finally purging outdated payment ticket.

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