source: main/waeup.kofa/trunk/src/waeup/kofa/students/tests/test_browser.py @ 15607

Last change on this file since 15607 was 15606, checked in by Henrik Bettermann, 5 years ago

Parents access implementation (part 1)

  • Property svn:keywords set to Id
File size: 240.7 KB
Line 
1# -*- coding: utf-8 -*-
2## $Id: test_browser.py 15606 2019-09-24 17:21:28Z henrik $
3##
4## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
5## This program is free software; you can redistribute it and/or modify
6## it under the terms of the GNU General Public License as published by
7## the Free Software Foundation; either version 2 of the License, or
8## (at your option) any later version.
9##
10## This program is distributed in the hope that it will be useful,
11## but WITHOUT ANY WARRANTY; without even the implied warranty of
12## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13## GNU General Public License for more details.
14##
15## You should have received a copy of the GNU General Public License
16## along with this program; if not, write to the Free Software
17## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18##
19"""
20Test the student-related UI components.
21"""
22import shutil
23import tempfile
24import pytz
25import base64
26from datetime import datetime, timedelta, date
27from StringIO import StringIO
28import os
29import grok
30from zc.async.testing import wait_for_result
31from zope.event import notify
32from zope.component import createObject, queryUtility, getUtility
33from zope.component.hooks import setSite, clearSite
34from zope.catalog.interfaces import ICatalog
35from zope.security.interfaces import Unauthorized
36from zope.securitypolicy.interfaces import IPrincipalRoleManager
37from zope.testbrowser.testing import Browser
38from zope.interface import implementedBy
39from zope.schema.fieldproperty import FieldProperty
40from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
41from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
42from waeup.kofa.app import University
43from waeup.kofa.interfaces import IFileStoreNameChooser, IExtFileStore
44from waeup.kofa.payments.interfaces import IPayer
45from waeup.kofa.students.payments import StudentOnlinePayment
46from waeup.kofa.students.student import Student
47from waeup.kofa.students.studylevel import StudentStudyLevel
48from waeup.kofa.university.faculty import Faculty
49from waeup.kofa.university.department import Department
50from waeup.kofa.interfaces import IUserAccount, IJobManager, VALIDATED, CREATED
51from waeup.kofa.authentication import LocalRoleSetEvent
52from waeup.kofa.hostels.hostel import Hostel, Bed, NOT_OCCUPIED
53from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
54from waeup.kofa.browser.tests.test_pdf import samples_dir
55from waeup.kofa.tests.test_authentication import SECRET
56
57PH_LEN = 15911  # Length of placeholder file
58
59SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
60SAMPLE_IMAGE_BMP = os.path.join(os.path.dirname(__file__), 'test_image.bmp')
61URL_LECTURER_LANDING = 'http://localhost/app/my_courses'
62
63curr_year = datetime.now().year
64
65def lookup_submit_value(name, value, browser):
66    """Find a button with a certain value."""
67    for num in range(0, 100):
68        try:
69            button = browser.getControl(name=name, index=num)
70            if button.value.endswith(value):
71                return button
72        except IndexError:
73            break
74    return None
75
76
77class StudentsFullSetup(FunctionalTestCase):
78    # A test case that only contains a setup and teardown
79    #
80    # Complete setup for students handlings is rather complex and
81    # requires lots of things created before we can start. This is a
82    # setup that does all this, creates a university, creates PINs,
83    # etc.  so that we do not have to bother with that in different
84    # test cases.
85
86    layer = FunctionalLayer
87
88    def setUp(self):
89        super(StudentsFullSetup, self).setUp()
90
91        # Setup a sample site for each test
92        app = University()
93        self.dc_root = tempfile.mkdtemp()
94        app['datacenter'].setStoragePath(self.dc_root)
95
96        # Prepopulate the ZODB...
97        self.getRootFolder()['app'] = app
98        # we add the site immediately after creation to the
99        # ZODB. Catalogs and other local utilities are not setup
100        # before that step.
101        self.app = self.getRootFolder()['app']
102        # Set site here. Some of the following setup code might need
103        # to access grok.getSite() and should get our new app then
104        setSite(app)
105
106        # Add student with subobjects
107        student = createObject('waeup.Student')
108        student.firstname = u'Anna'
109        student.lastname = u'Tester'
110        student.reg_number = u'123'
111        student.matric_number = u'234'
112        student.sex = u'm'
113        student.email = 'aa@aa.ng'
114        student.phone = u'1234'
115        student.date_of_birth = date(1981, 2, 4)
116        self.app['students'].addStudent(student)
117        self.student_id = student.student_id
118        self.student = self.app['students'][self.student_id]
119
120        # Set password
121        IUserAccount(
122            self.app['students'][self.student_id]).setPassword('spwd')
123
124        self.login_path = 'http://localhost/app/login'
125        self.container_path = 'http://localhost/app/students'
126        self.manage_container_path = self.container_path + '/@@manage'
127        self.add_student_path = self.container_path + '/addstudent'
128        self.student_path = self.container_path + '/' + self.student_id
129        self.manage_student_path = self.student_path + '/manage_base'
130        self.trigtrans_path = self.student_path + '/trigtrans'
131        self.clearance_path = self.student_path + '/view_clearance'
132        self.personal_path = self.student_path + '/view_personal'
133        self.edit_clearance_path = self.student_path + '/cedit'
134        self.manage_clearance_path = self.student_path + '/manage_clearance'
135        self.edit_personal_path = self.student_path + '/edit_personal'
136        self.manage_personal_path = self.student_path + '/manage_personal'
137        self.studycourse_path = self.student_path + '/studycourse'
138        self.payments_path = self.student_path + '/payments'
139        self.acco_path = self.student_path + '/accommodation'
140        self.history_path = self.student_path + '/history'
141
142        # Create 5 access codes with prefix'PWD'
143        pin_container = self.app['accesscodes']
144        pin_container.createBatch(
145            datetime.utcnow(), 'some_userid', 'PWD', 9.99, 5)
146        pins = pin_container['PWD-1'].values()
147        self.pwdpins = [x.representation for x in pins]
148        self.existing_pwdpin = self.pwdpins[0]
149        parts = self.existing_pwdpin.split('-')[1:]
150        self.existing_pwdseries, self.existing_pwdnumber = parts
151        # Create 5 access codes with prefix 'CLR'
152        pin_container.createBatch(
153            datetime.now(), 'some_userid', 'CLR', 9.99, 5)
154        pins = pin_container['CLR-1'].values()
155        pins[0].owner = u'Hans Wurst'
156        self.existing_clrac = pins[0]
157        self.existing_clrpin = pins[0].representation
158        parts = self.existing_clrpin.split('-')[1:]
159        self.existing_clrseries, self.existing_clrnumber = parts
160        # Create 2 access codes with prefix 'HOS'
161        pin_container.createBatch(
162            datetime.now(), 'some_userid', 'HOS', 9.99, 2)
163        pins = pin_container['HOS-1'].values()
164        self.existing_hosac = pins[0]
165        self.existing_hospin = pins[0].representation
166        parts = self.existing_hospin.split('-')[1:]
167        self.existing_hosseries, self.existing_hosnumber = parts
168
169        # Populate university
170        self.certificate = createObject('waeup.Certificate')
171        self.certificate.code = u'CERT1'
172        self.certificate.application_category = 'basic'
173        self.certificate.study_mode = 'ug_ft'
174        self.certificate.start_level = 100
175        self.certificate.end_level = 500
176        self.certificate.school_fee_1 = 40000.0
177        self.certificate.school_fee_2 = 20000.0
178        self.app['faculties']['fac1'] = Faculty(code=u'fac1')
179        self.app['faculties']['fac1']['dep1'] = Department(code=u'dep1')
180        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
181            self.certificate)
182        self.course = createObject('waeup.Course')
183        self.course.code = 'COURSE1'
184        self.course.semester = 1
185        self.course.credits = 10
186        self.course.passmark = 40
187        self.app['faculties']['fac1']['dep1'].courses.addCourse(
188            self.course)
189        self.app['faculties']['fac1']['dep1'].certificates[
190            'CERT1'].addCertCourse(self.course, level=100)
191
192        # Configure university and hostels
193        self.app['hostels'].accommodation_states = ['admitted']
194        self.app['hostels'].accommodation_session = 2004
195        delta = timedelta(days=10)
196        self.app['hostels'].startdate = datetime.now(pytz.utc) - delta
197        self.app['hostels'].enddate = datetime.now(pytz.utc) + delta
198        self.app['configuration'].carry_over = True
199        configuration = createObject('waeup.SessionConfiguration')
200        configuration.academic_session = 2004
201        configuration.clearance_fee = 3456.0
202        configuration.transcript_fee = 4567.0
203        configuration.booking_fee = 123.4
204        configuration.maint_fee = 987.0
205        configuration.transfer_fee = 456.0
206        configuration.late_registration_fee = 345.0
207        self.app['configuration'].addSessionConfiguration(configuration)
208
209        # Create a hostel with two beds
210        hostel = Hostel()
211        hostel.hostel_id = u'hall-1'
212        hostel.hostel_name = u'Hall 1'
213        hostel.maint_fee = 876.0
214        self.app['hostels'].addHostel(hostel)
215        bed = Bed()
216        bed.bed_id = u'hall-1_A_101_A'
217        bed.bed_number = 1
218        bed.owner = NOT_OCCUPIED
219        bed.bed_type = u'regular_male_fr'
220        self.app['hostels'][hostel.hostel_id].addBed(bed)
221        bed = Bed()
222        bed.bed_id = u'hall-1_A_101_B'
223        bed.bed_number = 2
224        bed.owner = NOT_OCCUPIED
225        bed.bed_type = u'regular_female_fr'
226        self.app['hostels'][hostel.hostel_id].addBed(bed)
227
228        # Set study course attributes of test student
229        self.student['studycourse'].certificate = self.certificate
230        self.student['studycourse'].current_session = 2004
231        self.student['studycourse'].entry_session = 2004
232        self.student['studycourse'].current_verdict = 'A'
233        self.student['studycourse'].current_level = 100
234        # Update the catalog
235        notify(grok.ObjectModifiedEvent(self.student))
236
237        # Put the prepopulated site into test ZODB and prepare test
238        # browser
239        self.browser = Browser()
240        self.browser.handleErrors = False
241
242    def tearDown(self):
243        super(StudentsFullSetup, self).tearDown()
244        clearSite()
245        shutil.rmtree(self.dc_root)
246
247
248class StudentsContainerUITests(StudentsFullSetup):
249    # Tests for StudentsContainer class views and pages
250
251    layer = FunctionalLayer
252
253    def test_anonymous_access(self):
254        # Anonymous users can't access students containers
255        self.assertRaises(
256            Unauthorized, self.browser.open, self.container_path)
257        self.assertRaises(
258            Unauthorized, self.browser.open, self.manage_container_path)
259        return
260
261    def test_manage_access(self):
262        # Managers can access the view page of students
263        # containers and can perform actions
264        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
265        self.browser.open(self.container_path)
266        self.assertEqual(self.browser.headers['Status'], '200 Ok')
267        self.assertEqual(self.browser.url, self.container_path)
268        self.browser.getLink("Manage students section").click()
269        self.assertEqual(self.browser.headers['Status'], '200 Ok')
270        self.assertEqual(self.browser.url, self.manage_container_path)
271        return
272
273    def test_add_search_delete_students(self):
274        # Managers can add search and remove students
275        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
276        self.browser.open(self.manage_container_path)
277        self.browser.getLink("Add student").click()
278        self.assertEqual(self.browser.headers['Status'], '200 Ok')
279        self.assertEqual(self.browser.url, self.add_student_path)
280        self.browser.getControl(name="form.firstname").value = 'Bob'
281        self.browser.getControl(name="form.lastname").value = 'Tester'
282        self.browser.getControl(name="form.reg_number").value = '123'
283        self.browser.getControl("Create student").click()
284        self.assertTrue(
285            'Registration number exists already' in self.browser.contents)
286        self.browser.getControl(name="form.reg_number").value = '1234'
287        self.browser.getControl("Create student").click()
288        self.assertTrue('Student record created' in self.browser.contents)
289
290        # Registration and matric numbers must be unique
291        self.browser.getLink("Manage").click()
292        self.browser.getControl(name="form.reg_number").value = '123'
293        self.browser.getControl("Save").click()
294        self.assertMatches('...Registration number exists...',
295                           self.browser.contents)
296        self.browser.getControl(name="form.reg_number").value = '789'
297        self.browser.getControl(name="form.matric_number").value = '234'
298        self.browser.getControl("Save").click()
299        self.assertMatches('...Matriculation number exists...',
300                           self.browser.contents)
301
302        # We can find a student with a certain student_id
303        self.browser.open(self.container_path)
304        self.browser.getControl("Find student(s)").click()
305        self.assertTrue('Empty search string' in self.browser.contents)
306        self.browser.getControl(name="searchtype").value = ['student_id']
307        self.browser.getControl(name="searchterm").value = self.student_id
308        self.browser.getControl("Find student(s)").click()
309        self.assertTrue('Anna Tester' in self.browser.contents)
310
311        # We can find a student in a certain session
312        self.browser.open(self.container_path)
313        self.browser.getControl(name="searchtype").value = ['current_session']
314        self.browser.getControl(name="searchterm").value = '2004'
315        self.browser.getControl("Find student(s)").click()
316        self.assertTrue('Anna Tester' in self.browser.contents)
317        # Session fileds require integer values
318        self.browser.open(self.container_path)
319        self.browser.getControl(name="searchtype").value = ['current_session']
320        self.browser.getControl(name="searchterm").value = '2004/2005'
321        self.browser.getControl("Find student(s)").click()
322        self.assertTrue('Only year dates allowed' in self.browser.contents)
323        self.browser.open(self.manage_container_path)
324        self.browser.getControl(name="searchtype").value = ['current_session']
325        self.browser.getControl(name="searchterm").value = '2004/2005'
326        self.browser.getControl("Find student(s)").click()
327        self.assertTrue('Only year dates allowed' in self.browser.contents)
328
329        # We can find a student in a certain study_mode
330        self.browser.open(self.container_path)
331        self.browser.getControl(name="searchtype").value = ['current_mode']
332        self.browser.getControl(name="searchterm").value = 'ug_ft'
333        self.browser.getControl("Find student(s)").click()
334        self.assertTrue('Anna Tester' in self.browser.contents)
335
336        # We can find a student in a certain department
337        self.browser.open(self.container_path)
338        self.browser.getControl(name="searchtype").value = ['depcode']
339        self.browser.getControl(name="searchterm").value = 'dep1'
340        self.browser.getControl("Find student(s)").click()
341        self.assertTrue('Anna Tester' in self.browser.contents)
342
343        # We can find a student by searching for all kind of name parts
344        self.browser.open(self.manage_container_path)
345        self.browser.getControl("Find student(s)").click()
346        self.assertTrue('Empty search string' in self.browser.contents)
347        self.browser.getControl(name="searchtype").value = ['fullname']
348        self.browser.getControl(name="searchterm").value = 'Anna Tester'
349        self.browser.getControl("Find student(s)").click()
350        self.assertTrue('Anna Tester' in self.browser.contents)
351        self.browser.open(self.manage_container_path)
352        self.browser.getControl(name="searchtype").value = ['fullname']
353        self.browser.getControl(name="searchterm").value = 'Anna'
354        self.browser.getControl("Find student(s)").click()
355        self.assertTrue('Anna Tester' in self.browser.contents)
356        self.browser.open(self.manage_container_path)
357        self.browser.getControl(name="searchtype").value = ['fullname']
358        self.browser.getControl(name="searchterm").value = 'Tester'
359        self.browser.getControl("Find student(s)").click()
360        self.assertTrue('Anna Tester' in self.browser.contents)
361        self.browser.open(self.manage_container_path)
362        self.browser.getControl(name="searchtype").value = ['fullname']
363        self.browser.getControl(name="searchterm").value = 'An'
364        self.browser.getControl("Find student(s)").click()
365        self.assertFalse('Anna Tester' in self.browser.contents)
366        self.browser.open(self.manage_container_path)
367        self.browser.getControl(name="searchtype").value = ['fullname']
368        self.browser.getControl(name="searchterm").value = 'An*'
369        self.browser.getControl("Find student(s)").click()
370        self.assertTrue('Anna Tester' in self.browser.contents)
371        self.browser.open(self.manage_container_path)
372        self.browser.getControl(name="searchtype").value = ['fullname']
373        self.browser.getControl(name="searchterm").value = 'tester'
374        self.browser.getControl("Find student(s)").click()
375        self.assertTrue('Anna Tester' in self.browser.contents)
376        self.browser.open(self.manage_container_path)
377        self.browser.getControl(name="searchtype").value = ['fullname']
378        self.browser.getControl(name="searchterm").value = 'Tester Ana'
379        self.browser.getControl("Find student(s)").click()
380        self.assertFalse('Anna Tester' in self.browser.contents)
381        self.browser.open(self.manage_container_path)
382        self.browser.getControl(name="searchtype").value = ['fullname']
383        self.browser.getControl(name="searchterm").value = 'Tester Anna'
384        self.browser.getControl("Find student(s)").click()
385        self.assertTrue('Anna Tester' in self.browser.contents)
386        # The old searchterm will be used again
387        self.browser.getControl("Find student(s)").click()
388        self.assertTrue('Anna Tester' in self.browser.contents)
389
390        # We can find suspended students
391        self.student.suspended = True
392        notify(grok.ObjectModifiedEvent(self.student))
393        self.browser.open(self.manage_container_path)
394        self.browser.getControl(name="searchtype").value = ['suspended']
395        self.browser.getControl("Find student(s)").click()
396        self.assertTrue('Anna Tester' in self.browser.contents)
397        self.browser.open(self.container_path)
398        self.browser.getControl(name="searchtype").value = ['suspended']
399        self.browser.getControl("Find student(s)").click()
400        self.assertTrue('Anna Tester' in self.browser.contents)
401
402        # The catalog is informed when studycourse objects have been
403        # edited
404        self.browser.open(self.studycourse_path + '/manage')
405        self.browser.getControl(name="form.current_session").value = ['2010']
406        self.browser.getControl(name="form.entry_session").value = ['2010']
407        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
408        self.browser.getControl("Save").click()
409
410        # We can find the student in the new session
411        self.browser.open(self.manage_container_path)
412        self.browser.getControl(name="searchtype").value = ['current_session']
413        self.browser.getControl(name="searchterm").value = '2010'
414        self.browser.getControl("Find student(s)").click()
415        self.assertTrue('Anna Tester' in self.browser.contents)
416
417        ctrl = self.browser.getControl(name='entries')
418        ctrl.getControl(value=self.student_id).selected = True
419        self.browser.getControl("Remove selected", index=0).click()
420        self.assertTrue('Successfully removed' in self.browser.contents)
421        self.browser.getControl(name="searchtype").value = ['student_id']
422        self.browser.getControl(name="searchterm").value = self.student_id
423        self.browser.getControl("Find student(s)").click()
424        self.assertTrue('No student found' in self.browser.contents)
425
426        self.browser.open(self.container_path)
427        self.browser.getControl(name="searchtype").value = ['student_id']
428        self.browser.getControl(name="searchterm").value = self.student_id
429        self.browser.getControl("Find student(s)").click()
430        self.assertTrue('No student found' in self.browser.contents)
431        return
432
433    def test_add_graduated_students(self):
434        # Managers can add search and remove students
435        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
436        self.browser.open(self.manage_container_path)
437        self.browser.getLink("Add student").click()
438        self.assertEqual(self.browser.headers['Status'], '200 Ok')
439        self.assertEqual(self.browser.url, self.add_student_path)
440        self.browser.getControl(name="form.firstname").value = 'Bob'
441        self.browser.getControl(name="form.lastname").value = 'Tester'
442        self.browser.getControl(name="form.reg_number").value = '1234'
443        self.browser.getControl("Create graduated student").click()
444        self.assertTrue('Graduated student record created' in self.browser.contents)
445        self.assertEqual(self.app['students']['K1000001'].state, 'graduated')
446        self.browser.open("http://localhost/app/students/K1000001/history")
447        self.assertTrue("State 'graduated' set by Manager" in self.browser.contents)
448        return
449
450
451class OfficerUITests(StudentsFullSetup):
452    # Tests for Student class views and pages
453
454    def test_student_properties(self):
455        self.student['studycourse'].current_level = 100
456        self.assertEqual(self.student.current_level, 100)
457        self.student['studycourse'].current_session = 2011
458        self.assertEqual(self.student.current_session, 2011)
459        self.student['studycourse'].current_verdict = 'A'
460        self.assertEqual(self.student.current_verdict, 'A')
461        return
462
463    def test_studylevelmanagepage(self):
464        studylevel = StudentStudyLevel()
465        studylevel.level = 100
466        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
467        self.student['studycourse'].addStudentStudyLevel(cert, studylevel)
468        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
469        self.browser.open(self.studycourse_path + '/100/manage')
470        self.assertEqual(
471            self.browser.url, self.studycourse_path + '/100/manage')
472        self.assertEqual(self.browser.headers['Status'], '200 Ok')
473
474    def test_basic_auth(self):
475        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
476        self.browser.open('http://localhost/app')
477        self.browser.getLink("Logout").click()
478        self.assertTrue('You have been logged out' in self.browser.contents)
479        # But we are still logged in since we've used basic
480        # authentication here.  Wikipedia says: Existing browsers
481        # retain authentication information until the tab or browser
482        # is closed or the user clears the history.  HTTP does not
483        # provide a method for a server to direct clients to discard
484        # these cached credentials. This means that there is no
485        # effective way for a server to "log out" the user without
486        # closing the browser. This is a significant defect that
487        # requires browser manufacturers to support a "logout" user
488        # interface element ...
489        self.assertTrue('Manager' in self.browser.contents)
490
491    def test_basic_auth_base64(self):
492        auth_token = base64.b64encode('mgr:mgrpw')
493        self.browser.addHeader('Authorization', 'Basic %s' % auth_token)
494        self.browser.open(self.manage_container_path)
495        self.assertEqual(self.browser.headers['Status'], '200 Ok')
496
497    def test_manage_access(self):
498        # Managers can access the pages of students
499        # and can perform actions
500        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
501        self.browser.open(self.student_path)
502        self.assertEqual(self.browser.headers['Status'], '200 Ok')
503        self.assertEqual(self.browser.url, self.student_path)
504        self.browser.getLink("Trigger").click()
505        self.assertEqual(self.browser.headers['Status'], '200 Ok')
506        # Managers can trigger transitions
507        self.browser.getControl(name="transition").value = ['admit']
508        self.browser.getControl("Save").click()
509        # Managers can edit base
510        self.browser.open(self.student_path)
511        self.browser.getLink("Manage").click()
512        self.assertEqual(self.browser.url, self.manage_student_path)
513        self.assertEqual(self.browser.headers['Status'], '200 Ok')
514        self.browser.getControl(name="form.firstname").value = 'John'
515        self.browser.getControl(name="form.lastname").value = 'Tester'
516        self.browser.getControl(name="form.reg_number").value = '345'
517        self.browser.getControl(name="password").value = 'secret'
518        self.browser.getControl(name="control_password").value = 'secret'
519        self.browser.getControl("Save").click()
520        self.assertMatches('...Form has been saved...',
521                           self.browser.contents)
522        self.browser.open(self.student_path)
523        self.browser.getLink("Clearance Data").click()
524        self.assertEqual(self.browser.headers['Status'], '200 Ok')
525        self.assertEqual(self.browser.url, self.clearance_path)
526        self.browser.getLink("Manage").click()
527        self.assertEqual(self.browser.headers['Status'], '200 Ok')
528        self.assertEqual(self.browser.url, self.manage_clearance_path)
529        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
530        self.browser.getControl("Save").click()
531        self.assertMatches('...Form has been saved...',
532                           self.browser.contents)
533
534        self.browser.open(self.student_path)
535        self.browser.getLink("Personal Data").click()
536        self.assertEqual(self.browser.headers['Status'], '200 Ok')
537        self.assertEqual(self.browser.url, self.personal_path)
538        self.browser.getLink("Manage").click()
539        self.assertEqual(self.browser.headers['Status'], '200 Ok')
540        self.assertEqual(self.browser.url, self.manage_personal_path)
541        self.browser.open(self.personal_path)
542        self.assertTrue('Updated' in self.browser.contents)
543        self.browser.getLink("Edit").click()
544        self.assertEqual(self.browser.headers['Status'], '200 Ok')
545        self.assertEqual(self.browser.url, self.edit_personal_path)
546        self.browser.getControl("Save").click()
547        # perm_address is required in IStudentPersonalEdit
548        self.assertMatches('...Required input is missing...',
549                           self.browser.contents)
550        self.browser.getControl(name="form.perm_address").value = 'My address!'
551        self.browser.getControl("Save").click()
552        self.assertMatches('...Form has been saved...',
553                           self.browser.contents)
554
555        # Managers can browse all subobjects
556        self.browser.open(self.student_path)
557        self.browser.getLink("Payments").click()
558        self.assertEqual(self.browser.headers['Status'], '200 Ok')
559        self.assertEqual(self.browser.url, self.payments_path)
560        self.browser.open(self.student_path)
561        self.browser.getLink("Accommodation").click()
562        self.assertEqual(self.browser.headers['Status'], '200 Ok')
563        self.assertEqual(self.browser.url, self.acco_path)
564        self.browser.open(self.student_path)
565        self.browser.getLink("History").click()
566        self.assertEqual(self.browser.headers['Status'], '200 Ok')
567        self.assertEqual(self.browser.url, self.history_path)
568        self.assertMatches('...Admitted by Manager...',
569                           self.browser.contents)
570        # Only the Application Slip does not exist
571        self.assertFalse('Application Slip' in self.browser.contents)
572        return
573
574    def test_flash_notice(self):
575        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
576        self.browser.open(self.student_path)
577        self.assertFalse('alert alert-warning' in self.browser.contents)
578        self.student.flash_notice = u'Happy Birthday!'
579        self.browser.open(self.student_path)
580        self.assertTrue(
581            '<div><div class="alert alert-warning">Happy Birthday!</div>'
582            in self.browser.contents)
583        return
584
585    def test_manage_contact_student(self):
586        # Managers can contact student
587        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
588        # Remove required FieldProperty attribute first ...
589        delattr(Student, 'email')
590        # ... and replace by None
591        self.student.email = None
592        # Now we have to add  the FieldProperty attribute again. Otherwise
593        # many other tests below will fail.
594        iface = list(implementedBy(Student))[0]
595        field_property = FieldProperty(iface['email'])
596        setattr(Student, 'email', field_property)
597        self.browser.open(self.student_path)
598        self.browser.getLink("Send email").click()
599        self.browser.getControl(
600            name="form.subject").value = 'Important subject'
601        self.browser.getControl(name="form.body").value = 'Hello!'
602        self.browser.getControl("Send message now").click()
603        self.assertTrue(
604            'An smtp server error occurred' in self.browser.contents)
605        self.student.email = 'xx@yy.zz'
606        self.browser.getControl("Send message now").click()
607        self.assertTrue('Your message has been sent' in self.browser.contents)
608        return
609
610    def test_manage_remove_department(self):
611        # Lazy student is studying CERT1
612        lazystudent = Student()
613        lazystudent.firstname = u'Lazy'
614        lazystudent.lastname = u'Student'
615        self.app['students'].addStudent(lazystudent)
616        student_id = lazystudent.student_id
617        student_path = self.container_path + '/' + student_id
618        lazystudent['studycourse'].certificate = self.certificate
619        notify(grok.ObjectModifiedEvent(lazystudent))
620        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
621        self.browser.open(student_path + '/studycourse')
622        self.assertTrue('CERT1' in self.browser.contents)
623        # After some years the department is removed
624        del self.app['faculties']['fac1']['dep1']
625        # So CERT1 does no longer exist and lazy student's
626        # certificate reference is removed too
627        self.browser.open(student_path + '/studycourse')
628        self.assertEqual(self.browser.headers['Status'], '200 Ok')
629        self.assertEqual(self.browser.url, student_path + '/studycourse')
630        self.assertFalse('CERT1' in self.browser.contents)
631        self.assertMatches('...<div>--</div>...',
632                           self.browser.contents)
633
634    def test_manage_upload_file(self):
635        # Managers can upload a file via the StudentClearanceManageFormPage
636        # The image is stored even if form has errors
637        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
638        self.browser.open(self.manage_clearance_path)
639        # No birth certificate has been uploaded yet
640        # Browsing the link shows a placerholder image
641        self.browser.open('birth_certificate')
642        self.assertEqual(
643            self.browser.headers['content-type'], 'image/jpeg')
644        self.assertEqual(len(self.browser.contents), PH_LEN)
645        # Create a pseudo image file and select it to be uploaded in form
646        # as birth certificate
647        self.browser.open(self.manage_clearance_path)
648        image = open(SAMPLE_IMAGE, 'rb')
649        ctrl = self.browser.getControl(name='birthcertificateupload')
650        file_ctrl = ctrl.mech_control
651        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
652        # The Save action does not upload files
653        self.browser.getControl("Save").click()  # submit form
654        self.assertFalse(
655            '<a target="image" href="birth_certificate">'
656            in self.browser.contents)
657        # ... but the correct upload submit button does
658        image = open(SAMPLE_IMAGE)
659        ctrl = self.browser.getControl(name='birthcertificateupload')
660        file_ctrl = ctrl.mech_control
661        file_ctrl.add_file(image, filename='my_birth_certificate.jpg')
662        self.browser.getControl(
663            name='upload_birthcertificateupload').click()
664        # There is a correct <img> link included
665        self.assertTrue(
666            'href="http://localhost/app/students/K1000000/birth_certificate"'
667            in self.browser.contents)
668        # Browsing the link shows a real image
669        self.browser.open('birth_certificate')
670        self.assertEqual(
671            self.browser.headers['content-type'], 'image/jpeg')
672        self.assertEqual(len(self.browser.contents), 2787)
673        # We can't reupload a file. The existing file must be deleted first.
674        self.browser.open(self.manage_clearance_path)
675        self.assertFalse(
676            'upload_birthcertificateupload' in self.browser.contents)
677        # File must be deleted first
678        self.browser.getControl(name='delete_birthcertificateupload').click()
679        self.assertTrue(
680            'birth_certificate deleted' in self.browser.contents)
681        # Uploading a file which is bigger than 250k will raise an error
682        big_image = StringIO(open(SAMPLE_IMAGE, 'rb').read() * 100)
683        ctrl = self.browser.getControl(name='birthcertificateupload')
684        file_ctrl = ctrl.mech_control
685        file_ctrl.add_file(big_image, filename='my_birth_certificate.jpg')
686        self.browser.getControl(
687            name='upload_birthcertificateupload').click()
688        self.assertTrue(
689            'Uploaded file is too big' in self.browser.contents)
690        # we do not rely on filename extensions given by uploaders
691        image = open(SAMPLE_IMAGE, 'rb')  # a jpg-file
692        ctrl = self.browser.getControl(name='birthcertificateupload')
693        file_ctrl = ctrl.mech_control
694        # tell uploaded file is bmp
695        file_ctrl.add_file(image, filename='my_birth_certificate.bmp')
696        self.browser.getControl(
697            name='upload_birthcertificateupload').click()
698        self.assertTrue(
699            # jpg file was recognized
700            'File birth_certificate.jpg uploaded.' in self.browser.contents)
701        # Delete file again
702        self.browser.getControl(name='delete_birthcertificateupload').click()
703        self.assertTrue(
704            'birth_certificate deleted' in self.browser.contents)
705        # File names must meet several conditions
706        bmp_image = open(SAMPLE_IMAGE_BMP, 'rb')
707        ctrl = self.browser.getControl(name='birthcertificateupload')
708        file_ctrl = ctrl.mech_control
709        file_ctrl.add_file(bmp_image, filename='my_birth_certificate.bmp')
710        self.browser.getControl(
711            name='upload_birthcertificateupload').click()
712        self.assertTrue(
713            'Only the following extensions are allowed'
714            in self.browser.contents)
715
716        # Managers can upload a file via the StudentBaseManageFormPage
717        self.browser.open(self.manage_student_path)
718        image = open(SAMPLE_IMAGE_BMP, 'rb')
719        ctrl = self.browser.getControl(name='passportuploadmanage')
720        file_ctrl = ctrl.mech_control
721        file_ctrl.add_file(image, filename='my_photo.bmp')
722        self.browser.getControl(
723            name='upload_passportuploadmanage').click()
724        self.assertTrue(
725            'jpg file format expected' in self.browser.contents)
726        ctrl = self.browser.getControl(name='passportuploadmanage')
727        file_ctrl = ctrl.mech_control
728        image = open(SAMPLE_IMAGE, 'rb')
729        file_ctrl.add_file(image, filename='my_photo.jpg')
730        self.browser.getControl(
731            name='upload_passportuploadmanage').click()
732        self.assertTrue(
733            'src="http://localhost/app/students/K1000000/passport.jpg"'
734            in self.browser.contents)
735        # We remove the passport file again
736        self.browser.open(self.manage_student_path)
737        self.browser.getControl('Delete').click()
738        self.browser.open(self.student_path + '/clearance_slip.pdf')
739        self.assertEqual(self.browser.headers['Status'], '200 Ok')
740        self.assertEqual(self.browser.headers['Content-Type'],
741                         'application/pdf')
742        # We want to see the signature fields.
743        IWorkflowState(self.student).setState('cleared')
744        self.browser.open(self.student_path + '/clearance_slip.pdf')
745        self.assertEqual(self.browser.headers['Status'], '200 Ok')
746        self.assertEqual(self.browser.headers['Content-Type'],
747                         'application/pdf')
748        path = os.path.join(samples_dir(), 'clearance_slip.pdf')
749        open(path, 'wb').write(self.browser.contents)
750        print "Sample PDF clearance_slip.pdf written to %s" % path
751
752    def test_manage_course_lists(self):
753        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
754        self.browser.open(self.student_path)
755        self.browser.getLink("Study Course").click()
756        self.assertEqual(self.browser.headers['Status'], '200 Ok')
757        self.assertEqual(self.browser.url, self.studycourse_path)
758        self.assertTrue('Undergraduate Full-Time' in self.browser.contents)
759        self.browser.getLink("Manage").click()
760        self.assertTrue('Manage study course' in self.browser.contents)
761        # Before we can select a level, the certificate must
762        # be selected and saved
763        self.browser.getControl(name="form.certificate").value = ['CERT1']
764        self.browser.getControl(name="form.current_session").value = ['2004']
765        self.browser.getControl(name="form.current_verdict").value = ['A']
766        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
767        self.browser.getControl("Save").click()
768        # Now we can save also the current level which depends on start and end
769        # level of the certificate
770        self.browser.getControl(name="form.current_level").value = ['100']
771        self.browser.getControl("Save").click()
772        # Managers can add and remove any study level (course list)
773        self.browser.getControl(name="addlevel").value = ['100']
774        self.browser.getControl("Add study level").click()
775        self.assertMatches(
776            '...You must select a session...', self.browser.contents)
777        self.browser.getControl(name="addlevel").value = ['100']
778        self.browser.getControl(name="level_session").value = ['2004']
779        self.browser.getControl("Add study level").click()
780        self.assertMatches('...<span>100</span>...', self.browser.contents)
781        self.assertEqual(self.student['studycourse']['100'].level, 100)
782        self.assertEqual(
783            self.student['studycourse']['100'].level_session, 2004)
784        self.browser.getControl(name="addlevel").value = ['100']
785        self.browser.getControl(name="level_session").value = ['2004']
786        self.browser.getControl("Add study level").click()
787        self.assertMatches('...This level exists...', self.browser.contents)
788        self.browser.getControl("Remove selected").click()
789        self.assertMatches(
790            '...No study level selected...', self.browser.contents)
791        self.browser.getControl(name="val_id").value = ['100']
792        self.browser.getControl(name="level_session").value = ['2004']
793        self.browser.getControl("Remove selected").click()
794        self.assertMatches('...Successfully removed...', self.browser.contents)
795        # Removing levels is properly logged
796        logfile = os.path.join(
797            self.app['datacenter'].storage, 'logs', 'students.log')
798        logcontent = open(logfile).read()
799        self.assertTrue(
800            'zope.mgr - students.browser.StudyCourseManageFormPage '
801            '- K1000000 - removed: 100' in logcontent)
802        # Add level again
803        self.browser.getControl(name="addlevel").value = ['100']
804        self.browser.getControl(name="level_session").value = ['2004']
805        self.browser.getControl("Add study level").click()
806
807        # Managers can view and manage course lists
808        self.browser.getLink("100").click()
809        self.assertMatches(
810            '...: 100 (Year 1)...', self.browser.contents)
811        self.browser.getLink("Manage").click()
812        self.browser.getControl(name="form.level_session").value = ['2002']
813        self.browser.getControl("Save").click()
814        self.browser.getControl("Remove selected").click()
815        self.assertMatches('...No ticket selected...', self.browser.contents)
816        ctrl = self.browser.getControl(name='val_id')
817        ctrl.getControl(value='COURSE1').selected = True
818        self.browser.getControl("Remove selected", index=0).click()
819        self.assertTrue('Successfully removed' in self.browser.contents)
820        # Removing course tickets is properly logged
821        logfile = os.path.join(
822            self.app['datacenter'].storage, 'logs', 'students.log')
823        logcontent = open(logfile).read()
824        self.assertTrue(
825            'zope.mgr - students.browser.StudyLevelManageFormPage '
826            '- K1000000 - removed: COURSE1 at 100' in logcontent)
827        self.browser.getLink("here").click()
828        self.browser.getControl(name="form.course").value = ['COURSE1']
829        self.course.credits = 100
830        self.browser.getControl("Add course ticket").click()
831        self.assertMatches(
832            '...Maximum credits exceeded...', self.browser.contents)
833        self.course.credits = 10
834        self.browser.getControl("Add course ticket").click()
835        self.assertTrue('Successfully added' in self.browser.contents)
836        # We can do the same by adding the course on the manage page directly
837        del self.student['studycourse']['100']['COURSE1']
838        self.browser.getControl(name="course").value = 'COURSE1'
839        self.browser.getControl("Add course ticket").click()
840        self.assertTrue('Successfully added' in self.browser.contents)
841        self.browser.getLink("here").click()
842        self.browser.getControl(name="form.course").value = ['COURSE1']
843        self.browser.getControl("Add course ticket").click()
844        self.assertTrue('The ticket exists' in self.browser.contents)
845        self.browser.getControl("Cancel").click()
846        self.browser.getLink("COURSE1").click()
847        self.browser.getLink("Manage").click()
848        self.browser.getControl("Save").click()
849        self.assertTrue('Form has been saved' in self.browser.contents)
850        # Grade and weight have been determined
851        self.browser.open(self.studycourse_path + '/100/COURSE1')
852        self.assertFalse('Grade' in self.browser.contents)
853        self.assertFalse('Weight' in self.browser.contents)
854        self.student['studycourse']['100']['COURSE1'].score = 55
855        self.browser.open(self.studycourse_path + '/100/COURSE1')
856        self.assertTrue('Grade' in self.browser.contents)
857        self.assertTrue('Weight' in self.browser.contents)
858        self.assertEqual(
859            self.student['studycourse']['100']['COURSE1'].grade, 'C')
860        self.assertEqual(
861            self.student['studycourse']['100']['COURSE1'].weight, 3)
862        # We add another ticket to check if GPA will be correctly
863        # calculated (and rounded)
864        courseticket = createObject('waeup.CourseTicket')
865        courseticket.code = 'ANYCODE'
866        courseticket.title = u'Any TITLE'
867        courseticket.credits = 13
868        courseticket.score = 66
869        courseticket.semester = 1
870        courseticket.dcode = u'ANYDCODE'
871        courseticket.fcode = u'ANYFCODE'
872        self.student['studycourse']['100']['COURSE2'] = courseticket
873        self.browser.open(self.student_path + '/studycourse/100')
874        # total credits
875        self.assertEqual(
876            self.student['studycourse']['100'].gpa_params_rectified[1], 23)
877        # weigheted credits = 3 * 10 + 4 * 13
878        self.assertEqual(
879            self.student['studycourse']['100'].gpa_params_rectified[2], 82.0)
880        # sgpa = 82 / 23
881        self.assertEqual(
882            self.student['studycourse']['100'].gpa_params_rectified[0],
883            3.5652173913043477)
884        # Carry-over courses will be collected when next level is created
885        self.browser.open(self.student_path + '/studycourse/manage')
886        # Add next level
887        self.student['studycourse']['100']['COURSE1'].score = 10
888        self.browser.getControl(name="addlevel").value = ['200']
889        self.browser.getControl(name="level_session").value = ['2005']
890        self.browser.getControl("Add study level").click()
891        self.browser.getLink("200").click()
892        self.assertMatches(
893            '...: 200 (Year 2)...', self.browser.contents)
894        # Since COURSE1 has score 10 it becomes a carry-over course
895        # in level 200
896        self.assertEqual(
897            sorted(self.student['studycourse']['200'].keys()), [u'COURSE1'])
898        self.assertTrue(
899            self.student['studycourse']['200']['COURSE1'].carry_over)
900        # Passed and failed courses have been counted
901        self.assertEqual(
902            self.student['studycourse']['100'].passed_params,
903            (1, 1, 13, 10, 'COURSE1 ', ''))
904        self.assertEqual(
905            self.student['studycourse']['200'].passed_params,
906            (0, 0, 0, 0, '', 'COURSE1 '))
907        # And also cumulative params can be calculated. Meanwhile we have the
908        # following courses: COURSE1 and COURSE2 in level 100 and
909        # COURSE1 as carry-over course in level 200.
910        self.assertEqual(
911            self.student['studycourse']['100'].cumulative_params,
912            (2.260869565217391, 23, 52.0, 23, 13))
913        # COURSE1 in level 200 is not taken into consideration
914        # when calculating the gpa.
915        self.assertEqual(
916            self.student['studycourse']['200'].cumulative_params,
917            (2.260869565217391, 23, 52.0, 33, 13))
918        return
919
920    def test_gpa_calculation_with_carryover(self):
921        studylevel = createObject(u'waeup.StudentStudyLevel')
922        studylevel.level = 100
923        studylevel.level_session = 2005
924        self.student['studycourse'].entry_mode = 'ug_ft'
925        self.student['studycourse'].addStudentStudyLevel(
926            self.certificate, studylevel)
927        # First course has been added automatically.
928        # Set score above passmark.
929        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark + 1
930        # GPA is 1.
931        self.assertEqual(
932            self.student['studycourse']['100'].gpa_params_rectified[0], 1.0)
933        # Set score below passmark.
934        studylevel['COURSE1'].score = studylevel['COURSE1'].passmark - 1
935        # GPA is still 0.
936        self.assertEqual(
937            self.student['studycourse']['100'].gpa_params_rectified[0], 0.0)
938        studylevel2 = createObject(u'waeup.StudentStudyLevel')
939        studylevel2.level = 200
940        studylevel2.level_session = 2006
941        self.student['studycourse'].addStudentStudyLevel(
942            self.certificate, studylevel2)
943        # Carry-over course has been autonatically added.
944        studylevel2['COURSE1'].score = 66
945        # The score of the carry-over course is now used for calculation of the
946        # GPA at level 100 ...
947        self.assertEqual(
948            self.student['studycourse']['100'].gpa_params_rectified[0], 4.0)
949        # ... but not at level 200
950        self.assertEqual(
951            self.student['studycourse']['200'].gpa_params_rectified[0], 0.0)
952        return
953
954    def test_manage_payments(self):
955        # Managers can add online school fee payment tickets
956        # if certain requirements are met
957        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
958        self.browser.open(self.payments_path)
959        IWorkflowState(self.student).setState('cleared')
960        self.browser.getLink("Add current session payment ticket").click()
961        self.browser.getControl(name="form.p_category").value = ['schoolfee']
962        self.browser.getControl("Create ticket").click()
963        self.assertMatches('...ticket created...',
964                           self.browser.contents)
965        ctrl = self.browser.getControl(name='val_id')
966        value = ctrl.options[0]
967        self.browser.getLink(value).click()
968        self.assertMatches('...Amount Authorized...',
969                           self.browser.contents)
970        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
971        payment_url = self.browser.url
972        logfile = os.path.join(
973            self.app['datacenter'].storage, 'logs', 'students.log')
974        logcontent = open(logfile).read()
975        self.assertTrue(
976            ' zope.mgr - students.browser.OnlinePaymentAddFormPage - '
977            'K1000000 - added: %s' % value
978            in logcontent)
979        # The pdf payment slip can't yet be opened
980        #self.browser.open(payment_url + '/payment_slip.pdf')
981        #self.assertMatches('...Ticket not yet paid...',
982        #                   self.browser.contents)
983
984        # The same payment (with same p_item, p_session and
985        # p_category) can be initialized a second time if the former
986        # ticket is not yet paid.
987        self.browser.open(self.payments_path)
988        self.browser.getLink("Add current session payment ticket").click()
989        self.browser.getControl(name="form.p_category").value = ['schoolfee']
990        self.browser.getControl("Create ticket").click()
991        self.assertMatches('...Payment ticket created...',
992                           self.browser.contents)
993
994        # The ticket can be found in the payments_catalog
995        cat = queryUtility(ICatalog, name='payments_catalog')
996        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
997        self.assertEqual(len(results), 2)
998        self.assertTrue(results[0] is self.student['payments'][value])
999        # Managers can approve the payment
1000        # If, by some reason, the state has already changed,
1001        # an access code is created after approval.
1002        IWorkflowState(self.student).setState('school fee paid')
1003        self.assertEqual(len(self.app['accesscodes']['SFE-0']), 0)
1004        self.browser.open(payment_url)
1005        self.browser.getLink("Approve payment").click()
1006        self.assertMatches(
1007            '...Payment approved...', self.browser.contents)
1008        # Approval is logged in students.log ...
1009        logcontent = open(logfile).read()
1010        self.assertTrue(
1011            'zope.mgr - students.browser.OnlinePaymentApproveView '
1012            '- K1000000 - schoolfee payment approved'
1013            in logcontent)
1014        # ... and in payments.log
1015        logfile = os.path.join(
1016            self.app['datacenter'].storage, 'logs', 'payments.log')
1017        logcontent = open(logfile).read()
1018        self.assertTrue(
1019            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
1020            in logcontent)
1021        # The authorized amount has been stored in the new access code
1022        self.assertEqual(
1023            self.app['accesscodes']['SFE-0'].values()[0].cost, 40000.0)
1024
1025        # The catalog has been updated
1026        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
1027        self.assertTrue(len(results), 0)
1028        results = list(cat.searchResults(p_state=('paid', 'paid')))
1029        self.assertTrue(len(results), 1)
1030        self.assertTrue(results[0] is self.student['payments'][value])
1031
1032        # Payments can't be approved twice
1033        self.browser.open(payment_url + '/approve')
1034        self.assertMatches('...This ticket has already been paid...',
1035                          self.browser.contents)
1036
1037        # Now the first ticket is paid and no more ticket of same type
1038        # (with same p_item, p_session and p_category) can be added.
1039        # First we have to reset the workflow state.
1040        IWorkflowState(self.student).setState('cleared')
1041        self.browser.open(self.payments_path)
1042        self.browser.getLink("Add current session payment ticket").click()
1043        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1044        self.browser.getControl("Create ticket").click()
1045        self.assertMatches(
1046            '...This type of payment has already been made...',
1047            self.browser.contents)
1048
1049        # Managers can open the pdf payment slip
1050        self.browser.open(payment_url)
1051        self.browser.getLink("Download payment slip").click()
1052        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1053        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
1054
1055        # Managers can remove online school fee payment tickets
1056        self.browser.open(self.payments_path)
1057        self.browser.getControl("Remove selected").click()
1058        self.assertMatches('...No payment selected...', self.browser.contents)
1059        ctrl = self.browser.getControl(name='val_id')
1060        value = ctrl.options[0]
1061        ctrl.getControl(value=value).selected = True
1062        self.browser.getControl("Remove selected", index=0).click()
1063        self.assertTrue('Successfully removed' in self.browser.contents)
1064
1065        # Managers can add online clearance payment tickets
1066        self.browser.open(self.payments_path + '/addop')
1067        self.browser.getControl(name="form.p_category").value = ['clearance']
1068        self.browser.getControl("Create ticket").click()
1069        self.assertMatches('...ticket created...',
1070                           self.browser.contents)
1071
1072        # Managers can approve the payment
1073        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
1074        ctrl = self.browser.getControl(name='val_id')
1075        value = ctrl.options[1] # The clearance payment is the second in the table
1076        self.browser.getLink(value).click()
1077        self.browser.open(self.browser.url + '/approve')
1078        self.assertMatches('...Payment approved...',
1079                          self.browser.contents)
1080        expected = '''...
1081        <td>
1082          <span>Paid</span>
1083        </td>...'''
1084        self.assertMatches(expected,self.browser.contents)
1085        # The new CLR-0 pin has been created
1086        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
1087        pin = self.app['accesscodes']['CLR-0'].keys()[0]
1088        ac = self.app['accesscodes']['CLR-0'][pin]
1089        self.assertEqual(ac.owner, self.student_id)
1090        self.assertEqual(ac.cost, 3456.0)
1091
1092        # Managers can add online transcript payment tickets
1093        self.browser.open(self.payments_path + '/addop')
1094        self.browser.getControl(name="form.p_category").value = ['transcript']
1095        self.browser.getControl("Create ticket").click()
1096        self.assertMatches('...ticket created...',
1097                           self.browser.contents)
1098
1099        # Managers can approve the payment
1100        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
1101        ctrl = self.browser.getControl(name='val_id')
1102        value = ctrl.options[2] # The clearance payment is the third in the table
1103        self.browser.getLink(value).click()
1104        self.browser.open(self.browser.url + '/approve')
1105        self.assertMatches('...Payment approved...',
1106                          self.browser.contents)
1107        expected = '''...
1108        <td>
1109          <span>Paid</span>
1110        </td>...'''
1111        self.assertMatches(expected,self.browser.contents)
1112        # The new CLR-0 pin has been created
1113        self.assertEqual(len(self.app['accesscodes']['TSC-0']),1)
1114        pin = self.app['accesscodes']['TSC-0'].keys()[0]
1115        ac = self.app['accesscodes']['TSC-0'][pin]
1116        self.assertEqual(ac.owner, self.student_id)
1117        self.assertEqual(ac.cost, 4567.0)
1118        return
1119
1120    def test_add_transfer_payment(self):
1121        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1122        self.browser.open(self.payments_path)
1123        self.browser.getLink("Add current session payment ticket").click()
1124        self.browser.getControl(name="form.p_category").value = ['transfer']
1125        self.browser.getControl(name="new_programme").value = 'my new study course'
1126        self.browser.getControl("Create ticket").click()
1127        self.assertMatches('...ticket created...',
1128                           self.browser.contents)
1129        ctrl = self.browser.getControl(name='val_id')
1130        value = ctrl.options[0]
1131        self.browser.getLink(value).click()
1132        self.assertMatches('...my new study course...',
1133                           self.browser.contents)
1134        self.assertEqual(self.student['payments'][value].p_item, u'my new study course')
1135
1136    def test_manage_payments_bypass_ac_creation(self):
1137        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1138        self.browser.open(self.payments_path)
1139        IWorkflowState(self.student).setState('cleared')
1140        self.browser.getLink("Add current session payment ticket").click()
1141        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1142        self.browser.getControl("Create ticket").click()
1143        ctrl = self.browser.getControl(name='val_id')
1144        value = ctrl.options[0]
1145        self.browser.getLink(value).click()
1146        payment_url = self.browser.url
1147        logfile = os.path.join(
1148            self.app['datacenter'].storage, 'logs', 'students.log')
1149        # The ticket can be found in the payments_catalog
1150        cat = queryUtility(ICatalog, name='payments_catalog')
1151        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
1152        self.assertTrue(len(results), 1)
1153        self.assertTrue(results[0] is self.student['payments'][value])
1154        # Managers can approve the payment
1155        self.browser.open(payment_url)
1156        self.browser.getLink("Approve payment").click()
1157        self.assertMatches('...Payment approved...',
1158                          self.browser.contents)
1159        # Approval is logged in students.log ...
1160        logcontent = open(logfile).read()
1161        self.assertTrue(
1162            'zope.mgr - students.browser.OnlinePaymentApproveView '
1163            '- K1000000 - schoolfee payment approved'
1164            in logcontent)
1165        # ... and in payments.log
1166        logfile = os.path.join(
1167            self.app['datacenter'].storage, 'logs', 'payments.log')
1168        logcontent = open(logfile).read()
1169        self.assertTrue(
1170            '"zope.mgr",K1000000,%s,schoolfee,40000.0,AP,,,,,,\n' % value
1171            in logcontent)
1172        # Student is in state school fee paid, no activation
1173        # code was necessary.
1174        self.assertEqual(self.student.state, 'school fee paid')
1175        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
1176        return
1177
1178    def test_payment_disabled(self):
1179        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1180        self.browser.open(self.payments_path)
1181        IWorkflowState(self.student).setState('cleared')
1182        self.browser.getLink("Add current session payment ticket").click()
1183        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1184        self.browser.getControl("Create ticket").click()
1185        self.assertMatches('...ticket created...',
1186                           self.browser.contents)
1187        self.app['configuration']['2004'].payment_disabled = ['sf_all']
1188        self.browser.getLink("Add current session payment ticket").click()
1189        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1190        self.browser.getControl("Create ticket").click()
1191        self.assertMatches('...This category of payments has been disabled...',
1192                           self.browser.contents)
1193        return
1194
1195    def test_manage_balance_payments(self):
1196
1197        # Login
1198        #self.browser.open(self.login_path)
1199        #self.browser.getControl(name="form.login").value = self.student_id
1200        #self.browser.getControl(name="form.password").value = 'spwd'
1201        #self.browser.getControl("Login").click()
1202
1203        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1204        self.browser.open(self.payments_path)
1205
1206        # Managers can add balance school fee payment tickets in any state.
1207        IWorkflowState(self.student).setState('courses registered')
1208        self.browser.open(self.payments_path)
1209        self.browser.getLink("Add balance payment ticket").click()
1210
1211        # Balance payment form is provided
1212        self.assertEqual(self.student.current_session, 2004)
1213        self.browser.getControl(name="form.p_category").value = ['schoolfee']
1214        self.browser.getControl(name="form.balance_session").value = ['2004']
1215        self.browser.getControl(name="form.balance_level").value = ['300']
1216        self.browser.getControl(name="form.balance_amount").value = '-567.8'
1217        self.browser.getControl("Create ticket").click()
1218        self.assertMatches('...Amount must be greater than 0...',
1219                           self.browser.contents)
1220        self.browser.getControl(name="form.balance_amount").value = '0'
1221        self.browser.getControl("Create ticket").click()
1222        self.assertMatches('...Amount must be greater than 0...',
1223                           self.browser.contents)
1224        self.browser.getControl(name="form.balance_amount").value = '567.8'
1225        self.browser.getControl("Create ticket").click()
1226        self.assertMatches('...ticket created...',
1227                           self.browser.contents)
1228        ctrl = self.browser.getControl(name='val_id')
1229        value = ctrl.options[0]
1230        self.browser.getLink(value).click()
1231        self.assertMatches('...Amount Authorized...',
1232                           self.browser.contents)
1233        self.assertEqual(self.student['payments'][value].amount_auth, 567.8)
1234        # Payment attributes are properly set
1235        self.assertEqual(self.student['payments'][value].p_session, 2004)
1236        self.assertEqual(self.student['payments'][value].p_level, 300)
1237        self.assertEqual(self.student['payments'][value].p_item, u'Balance')
1238        self.assertEqual(self.student['payments'][value].p_category, 'schoolfee')
1239        # Adding payment tickets is logged.
1240        logfile = os.path.join(
1241            self.app['datacenter'].storage, 'logs', 'students.log')
1242        logcontent = open(logfile).read()
1243        self.assertTrue('zope.mgr - students.browser.BalancePaymentAddFormPage '
1244                        '- K1000000 - added: %s' % value in logcontent)
1245
1246    def test_manage_accommodation(self):
1247        logfile = os.path.join(
1248            self.app['datacenter'].storage, 'logs', 'students.log')
1249        # Managers can add online booking fee payment tickets and open the
1250        # callback view (see test_manage_payments)
1251        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1252        self.browser.open(self.payments_path)
1253        self.browser.getLink("Add current session payment ticket").click()
1254        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
1255        # If student is not in accommodation session, payment cannot be processed
1256        self.app['hostels'].accommodation_session = 2011
1257        self.browser.getControl("Create ticket").click()
1258        self.assertMatches('...Your current session does not match...',
1259                           self.browser.contents)
1260        self.app['hostels'].accommodation_session = 2004
1261        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
1262        self.browser.getControl("Create ticket").click()
1263        ctrl = self.browser.getControl(name='val_id')
1264        value = ctrl.options[0]
1265        self.browser.getLink(value).click()
1266        self.browser.open(self.browser.url + '/approve')
1267        # The new HOS-0 pin has been created
1268        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
1269        pin = self.app['accesscodes']['HOS-0'].keys()[0]
1270        ac = self.app['accesscodes']['HOS-0'][pin]
1271        self.assertEqual(ac.owner, self.student_id)
1272        parts = pin.split('-')[1:]
1273        sfeseries, sfenumber = parts
1274        # Managers can use HOS code and book a bed space with it
1275        self.browser.open(self.acco_path)
1276        self.browser.getControl("Book accommodation").click()
1277        self.assertMatches('...You are in the wrong...',
1278                           self.browser.contents)
1279        IWorkflowInfo(self.student).fireTransition('admit')
1280        # An existing HOS code can only be used if students
1281        # are in accommodation session
1282        self.student['studycourse'].current_session = 2003
1283        self.browser.getControl("Book accommodation").click()
1284        self.assertMatches('...Your current session does not match...',
1285                           self.browser.contents)
1286        self.student['studycourse'].current_session = 2004
1287        # All requirements are met and ticket can be created
1288        self.browser.getControl("Book accommodation").click()
1289        self.assertMatches('...Activation Code:...',
1290                           self.browser.contents)
1291        self.browser.getControl(name="ac_series").value = sfeseries
1292        self.browser.getControl(name="ac_number").value = sfenumber
1293        self.browser.getControl("Create bed ticket").click()
1294        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
1295                           self.browser.contents)
1296        # Bed has been allocated
1297        bed1 = self.app['hostels']['hall-1']['hall-1_A_101_A']
1298        self.assertTrue(bed1.owner == self.student_id)
1299        # BedTicketAddPage is now blocked
1300        self.browser.getControl("Book accommodation").click()
1301        self.assertMatches('...You already booked a bed space...',
1302            self.browser.contents)
1303        # The bed ticket displays the data correctly
1304        self.browser.open(self.acco_path + '/2004')
1305        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
1306                           self.browser.contents)
1307        self.assertMatches('...2004/2005...', self.browser.contents)
1308        self.assertMatches('...regular_male_fr...', self.browser.contents)
1309        self.assertMatches('...%s...' % pin, self.browser.contents)
1310        # Booking is properly logged
1311        logcontent = open(logfile).read()
1312        self.assertTrue('zope.mgr - students.browser.BedTicketAddPage '
1313            '- K1000000 - booked: hall-1_A_101_A' in logcontent)
1314        # Managers can relocate students if the student's bed_type has changed
1315        self.browser.getLink("Relocate student").click()
1316        self.assertMatches(
1317            "...Student can't be relocated...", self.browser.contents)
1318        self.student.sex = u'f'
1319        self.browser.getLink("Relocate student").click()
1320        self.assertMatches(
1321            "...Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
1322        self.assertTrue(bed1.owner == NOT_OCCUPIED)
1323        bed2 = self.app['hostels']['hall-1']['hall-1_A_101_B']
1324        self.assertTrue(bed2.owner == self.student_id)
1325        self.assertTrue(self.student['accommodation'][
1326            '2004'].bed_type == u'regular_female_fr')
1327        # Relocation is properly logged
1328        logcontent = open(logfile).read()
1329        self.assertTrue('zope.mgr - students.accommodation.BedTicket '
1330            '- K1000000 - relocated: hall-1_A_101_B' in logcontent)
1331        # The payment object still shows the original payment item
1332        payment_id = self.student['payments'].keys()[0]
1333        payment = self.student['payments'][payment_id]
1334        self.assertTrue(payment.p_item == u'regular_male_fr')
1335        # Managers can relocate students if the bed's bed_type has changed
1336        bed1.bed_type = u'regular_female_fr'
1337        bed2.bed_type = u'regular_male_fr'
1338        notify(grok.ObjectModifiedEvent(bed1))
1339        notify(grok.ObjectModifiedEvent(bed2))
1340        self.browser.getLink("Relocate student").click()
1341        self.assertMatches(
1342            "...Student relocated...", self.browser.contents)
1343        self.assertMatches(
1344            "... Hall 1, Block A, Room 101, Bed A...", self.browser.contents)
1345        self.assertMatches(bed1.owner, self.student_id)
1346        self.assertMatches(bed2.owner, NOT_OCCUPIED)
1347        # Managers can't relocate students if bed is reserved
1348        self.student.sex = u'm'
1349        bed1.bed_type = u'regular_female_reserved'
1350        notify(grok.ObjectModifiedEvent(bed1))
1351        self.browser.getLink("Relocate student").click()
1352        self.assertMatches(
1353            "...Students in reserved beds can't be relocated...",
1354            self.browser.contents)
1355        # Managers can relocate students if booking has been cancelled but
1356        # other bed space has been manually allocated after cancellation
1357        old_owner = bed1.releaseBed()
1358        self.assertMatches(old_owner, self.student_id)
1359        bed2.owner = self.student_id
1360        self.browser.open(self.acco_path + '/2004')
1361        self.assertMatches(
1362            "...booking cancelled...", self.browser.contents)
1363        self.browser.getLink("Relocate student").click()
1364        # We didn't informed the catalog therefore the new owner is not found
1365        self.assertMatches(
1366            "...There is no free bed in your category regular_male_fr...",
1367            self.browser.contents)
1368        # Now we fire the event properly
1369        notify(grok.ObjectModifiedEvent(bed2))
1370        self.browser.getLink("Relocate student").click()
1371        self.assertMatches(
1372            "...Student relocated...", self.browser.contents)
1373        self.assertMatches(
1374            "... Hall 1, Block A, Room 101, Bed B...", self.browser.contents)
1375          # Managers can delete bed tickets
1376        self.browser.open(self.acco_path)
1377        ctrl = self.browser.getControl(name='val_id')
1378        value = ctrl.options[0]
1379        ctrl.getControl(value=value).selected = True
1380        self.browser.getControl("Remove selected", index=0).click()
1381        self.assertMatches('...Successfully removed...', self.browser.contents)
1382        # The bed has been properly released by the event handler
1383        self.assertMatches(bed1.owner, NOT_OCCUPIED)
1384        self.assertMatches(bed2.owner, NOT_OCCUPIED)
1385        return
1386
1387    def test_manage_workflow(self):
1388        # Managers can pass through the whole workflow
1389        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1390        student = self.app['students'][self.student_id]
1391        self.browser.open(self.trigtrans_path)
1392        self.assertTrue(student.clearance_locked)
1393        self.browser.getControl(name="transition").value = ['admit']
1394        self.browser.getControl("Save").click()
1395        self.assertTrue(student.clearance_locked)
1396        self.browser.getControl(name="transition").value = ['start_clearance']
1397        self.browser.getControl("Save").click()
1398        self.assertFalse(student.clearance_locked)
1399        self.browser.getControl(name="transition").value = ['request_clearance']
1400        self.browser.getControl("Save").click()
1401        self.assertTrue(student.clearance_locked)
1402        self.browser.getControl(name="transition").value = ['clear']
1403        self.browser.getControl("Save").click()
1404        # Managers approve payment, they do not pay
1405        self.assertFalse('pay_first_school_fee' in self.browser.contents)
1406        self.browser.getControl(
1407            name="transition").value = ['approve_first_school_fee']
1408        self.browser.getControl("Save").click()
1409        self.browser.getControl(name="transition").value = ['reset6']
1410        self.browser.getControl("Save").click()
1411        # In state returning the pay_school_fee transition triggers some
1412        # changes of attributes
1413        self.browser.getControl(name="transition").value = ['approve_school_fee']
1414        self.browser.getControl("Save").click()
1415        self.assertEqual(student['studycourse'].current_session, 2005) # +1
1416        self.assertEqual(student['studycourse'].current_level, 200) # +100
1417        self.assertEqual(student['studycourse'].current_verdict, '0') # 0 = Zero = not set
1418        self.assertEqual(student['studycourse'].previous_verdict, 'A')
1419        self.browser.getControl(name="transition").value = ['register_courses']
1420        self.browser.getControl("Save").click()
1421        self.browser.getControl(name="transition").value = ['validate_courses']
1422        self.browser.getControl("Save").click()
1423        self.browser.getControl(name="transition").value = ['return']
1424        self.browser.getControl("Save").click()
1425        return
1426
1427    def test_manage_pg_workflow(self):
1428        # Managers can pass through the whole workflow
1429        IWorkflowState(self.student).setState('school fee paid')
1430        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1431        self.browser.open(self.trigtrans_path)
1432        self.assertTrue('<option value="reset6">' in self.browser.contents)
1433        self.assertTrue('<option value="register_courses">' in self.browser.contents)
1434        self.assertTrue('<option value="reset5">' in self.browser.contents)
1435        self.certificate.study_mode = 'pg_ft'
1436        self.browser.open(self.trigtrans_path)
1437        self.assertFalse('<option value="reset6">' in self.browser.contents)
1438        self.assertFalse('<option value="register_courses">' in self.browser.contents)
1439        self.assertTrue('<option value="reset5">' in self.browser.contents)
1440        return
1441
1442    def test_manage_import(self):
1443        # Managers can import student data files
1444        datacenter_path = 'http://localhost/app/datacenter'
1445        # Prepare a csv file for students
1446        open('students.csv', 'wb').write(
1447"""firstname,lastname,reg_number,date_of_birth,matric_number,email,phone,sex,password
1448Aaren,Pieri,1,1990-01-02,100000,aa@aa.ng,1234,m,mypwd1
1449Claus,Finau,2,1990-01-03,100001,aa@aa.ng,1234,m,mypwd1
1450Brit,Berson,3,1990-01-04,100001,aa@aa.ng,1234,m,mypwd1
1451""")
1452        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1453        self.browser.open(datacenter_path)
1454        self.browser.getLink('Upload data').click()
1455        filecontents = StringIO(open('students.csv', 'rb').read())
1456        filewidget = self.browser.getControl(name='uploadfile:file')
1457        filewidget.add_file(filecontents, 'text/plain', 'students.csv')
1458        self.browser.getControl(name='SUBMIT').click()
1459        self.browser.getLink('Process data').click()
1460        self.browser.getLink("Switch maintenance mode").click()
1461        button = lookup_submit_value(
1462            'select', 'students_zope.mgr.csv', self.browser)
1463        button.click()
1464        importerselect = self.browser.getControl(name='importer')
1465        modeselect = self.browser.getControl(name='mode')
1466        importerselect.getControl('Student Processor').selected = True
1467        modeselect.getControl(value='create').selected = True
1468        self.browser.getControl('Proceed to step 3').click()
1469        self.assertTrue('Header fields OK' in self.browser.contents)
1470        self.browser.getControl('Perform import').click()
1471        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
1472        self.assertTrue('Successfully processed 2 rows' in self.browser.contents)
1473        self.assertTrue('Batch processing finished' in self.browser.contents)
1474        open('studycourses.csv', 'wb').write(
1475"""reg_number,matric_number,certificate,current_session,current_level
14761,,CERT1,2008,100
1477,100001,CERT1,2008,100
1478,100002,CERT1,2008,100
1479""")
1480        self.browser.open(datacenter_path)
1481        self.browser.getLink('Upload data').click()
1482        filecontents = StringIO(open('studycourses.csv', 'rb').read())
1483        filewidget = self.browser.getControl(name='uploadfile:file')
1484        filewidget.add_file(filecontents, 'text/plain', 'studycourses.csv')
1485        self.browser.getControl(name='SUBMIT').click()
1486        self.browser.getLink('Process data').click()
1487        # Meanwhile maintenance mode is disabled again.
1488        self.browser.getLink("Switch maintenance mode").click()
1489        button = lookup_submit_value(
1490            'select', 'studycourses_zope.mgr.csv', self.browser)
1491        button.click()
1492        importerselect = self.browser.getControl(name='importer')
1493        modeselect = self.browser.getControl(name='mode')
1494        importerselect.getControl(
1495            'StudentStudyCourse Processor (update only)').selected = True
1496        modeselect.getControl(value='create').selected = True
1497        self.browser.getControl('Proceed to step 3').click()
1498        self.assertTrue('Update mode only' in self.browser.contents)
1499        self.browser.getControl('Proceed to step 3').click()
1500        self.assertTrue('Header fields OK' in self.browser.contents)
1501        self.browser.getControl('Perform import').click()
1502        self.assertTrue('Processing of 1 rows failed' in self.browser.contents)
1503        self.assertTrue('Successfully processed 2 rows'
1504                        in self.browser.contents)
1505        # The students are properly indexed and we can
1506        # thus find a student in  the department
1507        self.browser.open(self.manage_container_path)
1508        self.browser.getControl(name="searchtype").value = ['depcode']
1509        self.browser.getControl(name="searchterm").value = 'dep1'
1510        self.browser.getControl("Find student(s)").click()
1511        self.assertTrue('Aaren Pieri' in self.browser.contents)
1512        # We can search for a new student by name ...
1513        self.browser.getControl(name="searchtype").value = ['fullname']
1514        self.browser.getControl(name="searchterm").value = 'Claus'
1515        self.browser.getControl("Find student(s)").click()
1516        self.assertTrue('Claus Finau' in self.browser.contents)
1517        # ... and check if the imported password has been properly set
1518        ctrl = self.browser.getControl(name='entries')
1519        value = ctrl.options[0]
1520        claus = self.app['students'][value]
1521        self.assertTrue(IUserAccount(claus).checkPassword('mypwd1'))
1522        return
1523
1524    def init_clearance_officer(self):
1525        # Create clearance officer
1526        self.app['users'].addUser('mrclear', SECRET)
1527        self.app['users']['mrclear'].email = 'mrclear@foo.ng'
1528        self.app['users']['mrclear'].title = 'Carlo Pitter'
1529        # Clearance officers need not necessarily to get
1530        # the StudentsOfficer site role
1531        #prmglobal = IPrincipalRoleManager(self.app)
1532        #prmglobal.assignRoleToPrincipal('waeup.StudentsOfficer', 'mrclear')
1533        # Assign local ClearanceOfficer role
1534        self.department = self.app['faculties']['fac1']['dep1']
1535        prmlocal = IPrincipalRoleManager(self.department)
1536        prmlocal.assignRoleToPrincipal('waeup.local.ClearanceOfficer', 'mrclear')
1537        IWorkflowState(self.student).setState('clearance started')
1538        # Add another student for testing
1539        other_student = Student()
1540        other_student.firstname = u'Dep2'
1541        other_student.lastname = u'Student'
1542        self.app['students'].addStudent(other_student)
1543        self.other_student_path = (
1544            'http://localhost/app/students/%s' % other_student.student_id)
1545        # Login as clearance officer
1546        self.browser.open(self.login_path)
1547        self.browser.getControl(name="form.login").value = 'mrclear'
1548        self.browser.getControl(name="form.password").value = SECRET
1549        self.browser.getControl("Login").click()
1550
1551    def test_handle_clearance_by_co(self):
1552        self.init_clearance_officer()
1553        self.assertMatches('...You logged in...', self.browser.contents)
1554        # CO is landing on index page.
1555        self.assertEqual(self.browser.url, 'http://localhost/app/index')
1556        # CO can see his roles
1557        self.browser.getLink("My Roles").click()
1558        self.assertMatches(
1559            '...<div>Academics Officer (view only)</div>...',
1560            self.browser.contents)
1561        # But not his local role ...
1562        self.assertFalse('Clearance Officer' in self.browser.contents)
1563        # ... because we forgot to notify the department that the local role
1564        # has changed.
1565        notify(LocalRoleSetEvent(
1566            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
1567            granted=True))
1568        self.browser.open('http://localhost/app/users/mrclear/my_roles')
1569        self.assertTrue('Clearance Officer' in self.browser.contents)
1570        self.assertMatches(
1571            '...<a href="http://localhost/app/faculties/fac1/dep1">...',
1572            self.browser.contents)
1573        # CO can view the student ...
1574        self.browser.open(self.clearance_path)
1575        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1576        self.assertEqual(self.browser.url, self.clearance_path)
1577        # ... but not other students.
1578        self.assertRaises(
1579            Unauthorized, self.browser.open, self.other_student_path)
1580        # Clearance is disabled for this session.
1581        self.browser.open(self.clearance_path)
1582        self.assertFalse('Clear student' in self.browser.contents)
1583        self.browser.open(self.student_path + '/clear')
1584        self.assertTrue('Clearance is disabled for this session'
1585            in self.browser.contents)
1586        self.app['configuration']['2004'].clearance_enabled = True
1587        # Only in state clearance requested the CO does see the 'Clear' button.
1588        self.browser.open(self.clearance_path)
1589        self.assertFalse('Clear student' in self.browser.contents)
1590        self.browser.open(self.student_path + '/clear')
1591        self.assertTrue('Student is in wrong state.'
1592            in self.browser.contents)
1593        IWorkflowInfo(self.student).fireTransition('request_clearance')
1594        self.browser.open(self.clearance_path)
1595        self.assertTrue('Clear student' in self.browser.contents)
1596        self.browser.getLink("Clear student").click()
1597        self.assertTrue('Student has been cleared' in self.browser.contents)
1598        self.assertTrue('cleared' in self.browser.contents)
1599        self.browser.open(self.history_path)
1600        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
1601        # Hide real name.
1602        self.app['users']['mrclear'].public_name = 'My Public Name'
1603        self.browser.open(self.clearance_path)
1604        self.browser.getLink("Reject clearance").click()
1605        self.assertEqual(
1606            self.browser.url, self.student_path + '/reject_clearance')
1607        # Type comment why.
1608        self.browser.getControl(name="form.officer_comment").value = (
1609            'Dear Student,\n'
1610            'You did not fill properly.')
1611        self.browser.getControl("Save comment").click()
1612        self.assertTrue('Clearance has been annulled' in self.browser.contents)
1613        url = ('http://localhost/app/students/K1000000/'
1614              'contactstudent?body=Dear+Student%2C%0AYou+did+not+fill+properly.'
1615              '&subject=Clearance+has+been+annulled.')
1616        # CO does now see the prefilled contact form and can send a message.
1617        self.assertEqual(self.browser.url, url)
1618        self.assertTrue('clearance started' in self.browser.contents)
1619        self.assertTrue('name="form.subject" size="20" type="text" '
1620            'value="Clearance has been annulled."'
1621            in self.browser.contents)
1622        self.assertTrue('name="form.body" rows="10" >Dear Student,'
1623            in self.browser.contents)
1624        self.browser.getControl("Send message now").click()
1625        self.assertTrue('Your message has been sent' in self.browser.contents)
1626        # The comment has been stored ...
1627        self.assertEqual(self.student.officer_comment,
1628            u'Dear Student,\nYou did not fill properly.')
1629        # ... and logged.
1630        logfile = os.path.join(
1631            self.app['datacenter'].storage, 'logs', 'students.log')
1632        logcontent = open(logfile).read()
1633        self.assertTrue(
1634            'INFO - mrclear - students.browser.StudentRejectClearancePage - '
1635            'K1000000 - comment: Dear Student,<br>You did not fill '
1636            'properly.\n' in logcontent)
1637        self.browser.open(self.history_path)
1638        self.assertTrue("Reset to 'clearance started' by My Public Name" in
1639            self.browser.contents)
1640        IWorkflowInfo(self.student).fireTransition('request_clearance')
1641        self.browser.open(self.clearance_path)
1642        self.browser.getLink("Reject clearance").click()
1643        self.browser.getControl("Save comment").click()
1644        self.assertTrue('Clearance request has been rejected'
1645            in self.browser.contents)
1646        self.assertTrue('clearance started' in self.browser.contents)
1647        # The CO can't clear students if not in state
1648        # clearance requested.
1649        self.browser.open(self.student_path + '/clear')
1650        self.assertTrue('Student is in wrong state'
1651            in self.browser.contents)
1652        # The CO can go to his department throug the my_roles page ...
1653        self.browser.open('http://localhost/app/users/mrclear/my_roles')
1654        self.browser.getLink("http://localhost/app/faculties/fac1/dep1").click()
1655        # ... and view the list of students.
1656        self.browser.getLink("Show students").click()
1657        self.browser.getControl(name="session").value = ['2004']
1658        self.browser.getControl(name="level").value = ['200']
1659        self.browser.getControl("Show").click()
1660        self.assertFalse(self.student_id in self.browser.contents)
1661        self.browser.getControl(name="session").value = ['2004']
1662        self.browser.getControl(name="level").value = ['100']
1663        self.browser.getControl("Show").click()
1664        self.assertTrue(self.student_id in self.browser.contents)
1665        # The comment is indicated by 'yes'.
1666        self.assertTrue('<td><span>yes</span></td>' in self.browser.contents)
1667        # Check if the enquiries form is not pre-filled with officer_comment
1668        # (regression test).
1669        self.browser.getLink("Logout").click()
1670        self.browser.open('http://localhost/app/enquiries')
1671        self.assertFalse(
1672            'You did not fill properly'
1673            in self.browser.contents)
1674        # When a student is cleared the comment is automatically deleted
1675        IWorkflowInfo(self.student).fireTransition('request_clearance')
1676        IWorkflowInfo(self.student).fireTransition('clear')
1677        self.assertEqual(self.student.officer_comment, None)
1678        return
1679
1680    def test_handle_mass_clearance_by_co(self):
1681        self.init_clearance_officer()
1682        # Additional setups according to test above
1683        notify(LocalRoleSetEvent(
1684            self.department, 'waeup.local.ClearanceOfficer', 'mrclear',
1685            granted=True))
1686        self.app['configuration']['2004'].clearance_enabled = True
1687        IWorkflowState(self.student).setState('clearance requested')
1688        # Update the catalog
1689        notify(grok.ObjectModifiedEvent(self.student))
1690        # The CO can go to the department and clear all students in department
1691        self.browser.open('http://localhost/app/faculties/fac1/dep1')
1692        self.browser.getLink("Clear all students").click()
1693        self.assertTrue('1 students have been cleared' in self.browser.contents)
1694        self.browser.open(self.history_path)
1695        self.assertTrue('Cleared by Carlo Pitter' in self.browser.contents)
1696        logfile = os.path.join(
1697            self.app['datacenter'].storage, 'logs', 'students.log')
1698        logcontent = open(logfile).read()
1699        self.assertTrue(
1700            'INFO - mrclear - K1000000 - Cleared' in logcontent)
1701        self.browser.open('http://localhost/app/faculties/fac1/dep1')
1702        self.browser.getLink("Clear all students").click()
1703        self.assertTrue('0 students have been cleared' in self.browser.contents)
1704        return
1705
1706    def test_handle_courses_by_ca(self):
1707        self.app['users'].addUser('mrsadvise', SECRET)
1708        self.app['users']['mrsadvise'].email = 'mradvise@foo.ng'
1709        self.app['users']['mrsadvise'].title = u'Helen Procter'
1710        # Assign local CourseAdviser100 role for a certificate
1711        cert = self.app['faculties']['fac1']['dep1'].certificates['CERT1']
1712        prmlocal = IPrincipalRoleManager(cert)
1713        prmlocal.assignRoleToPrincipal('waeup.local.CourseAdviser100', 'mrsadvise')
1714        IWorkflowState(self.student).setState('school fee paid')
1715        # Login as course adviser.
1716        self.browser.open(self.login_path)
1717        self.browser.getControl(name="form.login").value = 'mrsadvise'
1718        self.browser.getControl(name="form.password").value = SECRET
1719        self.browser.getControl("Login").click()
1720        self.assertMatches('...You logged in...', self.browser.contents)
1721        # CO can see his roles.
1722        self.browser.getLink("My Roles").click()
1723        self.assertMatches(
1724            '...<div>Academics Officer (view only)</div>...',
1725            self.browser.contents)
1726        # But not his local role ...
1727        self.assertFalse('Course Adviser' in self.browser.contents)
1728        # ... because we forgot to notify the certificate that the local role
1729        # has changed.
1730        notify(LocalRoleSetEvent(
1731            cert, 'waeup.local.CourseAdviser100', 'mrsadvise', granted=True))
1732        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
1733        self.assertTrue('Course Adviser 100L' in self.browser.contents)
1734        self.assertMatches(
1735            '...<a href="http://localhost/app/faculties/fac1/dep1/certificates/CERT1">...',
1736            self.browser.contents)
1737        # CA can view the student ...
1738        self.browser.open(self.student_path)
1739        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1740        self.assertEqual(self.browser.url, self.student_path)
1741        # ... but not other students.
1742        other_student = Student()
1743        other_student.firstname = u'Dep2'
1744        other_student.lastname = u'Student'
1745        self.app['students'].addStudent(other_student)
1746        other_student_path = (
1747            'http://localhost/app/students/%s' % other_student.student_id)
1748        self.assertRaises(
1749            Unauthorized, self.browser.open, other_student_path)
1750        # We add study level 110 to the student's studycourse.
1751        studylevel = StudentStudyLevel()
1752        studylevel.level = 110
1753        self.student['studycourse'].addStudentStudyLevel(
1754            cert,studylevel)
1755        L110_student_path = self.studycourse_path + '/110'
1756        # The CA can neither see the Validate nor the Edit button.
1757        self.browser.open(L110_student_path)
1758        self.assertFalse('Validate courses' in self.browser.contents)
1759        self.assertFalse('Edit' in self.browser.contents)
1760        IWorkflowInfo(self.student).fireTransition('register_courses')
1761        self.browser.open(L110_student_path)
1762        self.assertFalse('Validate courses' in self.browser.contents)
1763        self.assertFalse('Edit' in self.browser.contents)
1764        # Only in state courses registered and only if the current level
1765        # corresponds with the name of the study level object
1766        # the 100L CA does see the 'Validate' button but not
1767        # the edit button.
1768        self.student['studycourse'].current_level = 110
1769        self.browser.open(L110_student_path)
1770        self.assertFalse('Edit' in self.browser.contents)
1771        self.assertTrue('Validate courses' in self.browser.contents)
1772        # But a 100L CA does not see the button at other levels.
1773        studylevel2 = StudentStudyLevel()
1774        studylevel2.level = 200
1775        self.student['studycourse'].addStudentStudyLevel(
1776            cert,studylevel2)
1777        L200_student_path = self.studycourse_path + '/200'
1778        self.browser.open(L200_student_path)
1779        self.assertFalse('Edit' in self.browser.contents)
1780        self.assertFalse('Validate courses' in self.browser.contents)
1781        self.browser.open(L110_student_path)
1782        self.browser.getLink("Validate courses").click()
1783        self.assertTrue('Course list has been validated' in self.browser.contents)
1784        self.assertTrue('courses validated' in self.browser.contents)
1785        self.assertEqual(self.student['studycourse']['110'].validated_by,
1786            'Helen Procter')
1787        self.assertMatches(
1788            '<YYYY-MM-DD hh:mm:ss>',
1789            self.student['studycourse']['110'].validation_date.strftime(
1790                "%Y-%m-%d %H:%M:%S"))
1791        self.browser.getLink("Reject courses").click()
1792        self.assertTrue('Course list request has been annulled.'
1793            in self.browser.contents)
1794        urlmessage = 'Course+list+request+has+been+annulled.'
1795        self.assertEqual(self.browser.url, self.student_path +
1796            '/contactstudent?subject=%s' % urlmessage)
1797        self.assertTrue('school fee paid' in self.browser.contents)
1798        self.assertTrue(self.student['studycourse']['110'].validated_by is None)
1799        self.assertTrue(self.student['studycourse']['110'].validation_date is None)
1800        IWorkflowInfo(self.student).fireTransition('register_courses')
1801        self.browser.open(L110_student_path)
1802        self.browser.getLink("Reject courses").click()
1803        self.assertTrue('Course list has been unregistered'
1804            in self.browser.contents)
1805        self.assertTrue('school fee paid' in self.browser.contents)
1806        # CA does now see the contact form and can send a message.
1807        self.browser.getControl(name="form.subject").value = 'Important subject'
1808        self.browser.getControl(name="form.body").value = 'Course list rejected'
1809        self.browser.getControl("Send message now").click()
1810        self.assertTrue('Your message has been sent' in self.browser.contents)
1811        # The CA does now see the Edit button and can edit
1812        # current study level.
1813        self.browser.open(L110_student_path)
1814        self.browser.getLink("Edit").click()
1815        self.assertTrue('Edit course list of 100 (Year 1) on 1st probation'
1816            in self.browser.contents)
1817        # The CA can't validate courses if not in state
1818        # courses registered.
1819        self.browser.open(L110_student_path + '/validate_courses')
1820        self.assertTrue('Student is in the wrong state'
1821            in self.browser.contents)
1822        # The CA can go to his certificate through the my_roles page ...
1823        self.browser.open('http://localhost/app/users/mrsadvise/my_roles')
1824        self.browser.getLink(
1825            "http://localhost/app/faculties/fac1/dep1/certificates/CERT1").click()
1826        # ... and view the list of students.
1827        self.browser.getLink("Show students").click()
1828        self.browser.getControl(name="session").value = ['2004']
1829        self.browser.getControl(name="level").value = ['100']
1830        self.browser.getControl("Show").click()
1831        self.assertTrue(self.student_id in self.browser.contents)
1832
1833    def test_change_current_mode(self):
1834        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1835        self.browser.open(self.clearance_path)
1836        self.assertFalse('Employer' in self.browser.contents)
1837        self.browser.open(self.manage_clearance_path)
1838        self.assertFalse('Employer' in self.browser.contents)
1839        self.browser.open(self.edit_clearance_path)
1840        self.assertFalse('Employer' in self.browser.contents)
1841        # Now we change the study mode of the certificate and a different
1842        # interface is used by clearance views.
1843        self.certificate.study_mode = 'pg_ft'
1844        # Invariants are not being checked here?!
1845        self.certificate.end_level = 100
1846        self.browser.open(self.clearance_path)
1847        self.assertTrue('Employer' in self.browser.contents)
1848        self.browser.open(self.manage_clearance_path)
1849        self.assertTrue('Employer' in self.browser.contents)
1850        IWorkflowState(self.student).setState('clearance started')
1851        self.browser.open(self.edit_clearance_path)
1852        self.assertTrue('Employer' in self.browser.contents)
1853
1854    def test_find_students_in_faculties(self):
1855        # Create local students manager in faculty
1856        self.app['users'].addUser('mrmanager', SECRET)
1857        self.app['users']['mrmanager'].email = 'mrmanager@foo.ng'
1858        self.app['users']['mrmanager'].title = u'Volk Wagen'
1859        # Assign LocalStudentsManager role for faculty
1860        fac = self.app['faculties']['fac1']
1861        prmlocal = IPrincipalRoleManager(fac)
1862        prmlocal.assignRoleToPrincipal(
1863            'waeup.local.LocalStudentsManager', 'mrmanager')
1864        notify(LocalRoleSetEvent(
1865            fac, 'waeup.local.LocalStudentsManager', 'mrmanager',
1866            granted=True))
1867        # Login as manager
1868        self.browser.open(self.login_path)
1869        self.browser.getControl(name="form.login").value = 'mrmanager'
1870        self.browser.getControl(name="form.password").value = SECRET
1871        self.browser.getControl("Login").click()
1872        self.assertMatches('...You logged in...', self.browser.contents)
1873        # Manager can see his roles
1874        self.browser.getLink("My Roles").click()
1875        self.assertMatches(
1876            '...<span>Students Manager</span>...',
1877            self.browser.contents)
1878        # The manager can go to his faculty
1879        self.browser.getLink(
1880            "http://localhost/app/faculties/fac1").click()
1881        # and find students
1882        self.browser.getLink("Find students").click()
1883        self.browser.getControl("Find student").click()
1884        self.assertTrue('Empty search string' in self.browser.contents)
1885        self.browser.getControl(name="searchtype").value = ['student_id']
1886        self.browser.getControl(name="searchterm").value = self.student_id
1887        self.browser.getControl("Find student").click()
1888        self.assertTrue('Anna Tester' in self.browser.contents)
1889
1890    def test_activate_deactivate_buttons(self):
1891        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1892        self.browser.open(self.student_path)
1893        self.browser.getLink("Deactivate").click()
1894        self.assertTrue(
1895            'Student account has been deactivated.' in self.browser.contents)
1896        self.assertTrue(
1897            'Base Data (account deactivated)' in self.browser.contents)
1898        self.assertTrue(self.student.suspended)
1899        self.browser.getLink("Activate").click()
1900        self.assertTrue(
1901            'Student account has been activated.' in self.browser.contents)
1902        self.assertFalse(
1903            'Base Data (account deactivated)' in self.browser.contents)
1904        self.assertFalse(self.student.suspended)
1905        # History messages have been added ...
1906        self.browser.getLink("History").click()
1907        self.assertTrue(
1908            'Student account deactivated by Manager<br />' in self.browser.contents)
1909        self.assertTrue(
1910            'Student account activated by Manager<br />' in self.browser.contents)
1911        # ... and actions have been logged.
1912        logfile = os.path.join(
1913            self.app['datacenter'].storage, 'logs', 'students.log')
1914        logcontent = open(logfile).read()
1915        self.assertTrue('zope.mgr - students.browser.StudentDeactivateView - '
1916                        'K1000000 - account deactivated' in logcontent)
1917        self.assertTrue('zope.mgr - students.browser.StudentActivateView - '
1918                        'K1000000 - account activated' in logcontent)
1919
1920    def test_manage_student_transfer(self):
1921        # Add second certificate
1922        self.certificate2 = createObject('waeup.Certificate')
1923        self.certificate2.code = u'CERT2'
1924        self.certificate2.study_mode = 'ug_ft'
1925        self.certificate2.start_level = 999
1926        self.certificate2.end_level = 999
1927        self.app['faculties']['fac1']['dep1'].certificates.addCertificate(
1928            self.certificate2)
1929
1930        # Add study level to old study course
1931        studylevel = createObject(u'waeup.StudentStudyLevel')
1932        studylevel.level = 200
1933        self.student['studycourse'].addStudentStudyLevel(
1934            self.certificate, studylevel)
1935        studylevel = createObject(u'waeup.StudentStudyLevel')
1936        studylevel.level = 999
1937        self.student['studycourse'].addStudentStudyLevel(
1938            self.certificate, studylevel)
1939
1940        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
1941        self.browser.open(self.student_path)
1942        self.browser.getLink("Transfer").click()
1943        self.browser.getControl(name="form.certificate").value = ['CERT2']
1944        self.browser.getControl(name="form.current_session").value = ['2011']
1945        self.browser.getControl(name="form.current_level").value = ['200']
1946        self.browser.getControl("Transfer").click()
1947        self.assertTrue(
1948            'Current level does not match certificate levels'
1949            in self.browser.contents)
1950        self.browser.getControl(name="form.current_level").value = ['999']
1951        self.browser.getControl("Transfer").click()
1952        self.assertTrue('Successfully transferred' in self.browser.contents)
1953        # The catalog has been updated
1954        cat = queryUtility(ICatalog, name='students_catalog')
1955        results = list(
1956            cat.searchResults(
1957            certcode=('CERT2', 'CERT2')))
1958        self.assertTrue(results[0] is self.student)
1959        results = list(
1960            cat.searchResults(
1961            current_session=(2011, 2011)))
1962        self.assertTrue(results[0] is self.student)
1963        # Add study level to new study course
1964        studylevel = createObject(u'waeup.StudentStudyLevel')
1965        studylevel.level = 999
1966        self.student['studycourse'].addStudentStudyLevel(
1967            self.certificate, studylevel)
1968
1969        # Edit and add pages are locked for old study courses
1970        self.browser.open(self.student_path + '/studycourse/manage')
1971        self.assertFalse('The requested form is locked' in self.browser.contents)
1972        self.browser.open(self.student_path + '/studycourse_1/manage')
1973        self.assertTrue('The requested form is locked' in self.browser.contents)
1974
1975        self.browser.open(self.student_path + '/studycourse/start_session')
1976        self.assertFalse('The requested form is locked' in self.browser.contents)
1977        self.browser.open(self.student_path + '/studycourse_1/start_session')
1978        self.assertTrue('The requested form is locked' in self.browser.contents)
1979
1980        IWorkflowState(self.student).setState('school fee paid')
1981        self.browser.open(self.student_path + '/studycourse/add')
1982        self.assertFalse('The requested form is locked' in self.browser.contents)
1983        self.browser.open(self.student_path + '/studycourse_1/add')
1984        self.assertTrue('The requested form is locked' in self.browser.contents)
1985
1986        self.browser.open(self.student_path + '/studycourse/999/manage')
1987        self.assertFalse('The requested form is locked' in self.browser.contents)
1988        self.browser.open(self.student_path + '/studycourse_1/999/manage')
1989        self.assertTrue('The requested form is locked' in self.browser.contents)
1990
1991        self.browser.open(self.student_path + '/studycourse/999/validate_courses')
1992        self.assertFalse('The requested form is locked' in self.browser.contents)
1993        self.browser.open(self.student_path + '/studycourse_1/999/validate_courses')
1994        self.assertTrue('The requested form is locked' in self.browser.contents)
1995
1996        self.browser.open(self.student_path + '/studycourse/999/reject_courses')
1997        self.assertFalse('The requested form is locked' in self.browser.contents)
1998        self.browser.open(self.student_path + '/studycourse_1/999/reject_courses')
1999        self.assertTrue('The requested form is locked' in self.browser.contents)
2000
2001        self.browser.open(self.student_path + '/studycourse/999/add')
2002        self.assertFalse('The requested form is locked' in self.browser.contents)
2003        self.browser.open(self.student_path + '/studycourse_1/999/add')
2004        self.assertTrue('The requested form is locked' in self.browser.contents)
2005
2006        self.browser.open(self.student_path + '/studycourse/999/edit')
2007        self.assertFalse('The requested form is locked' in self.browser.contents)
2008        self.browser.open(self.student_path + '/studycourse_1/999/edit')
2009        self.assertTrue('The requested form is locked' in self.browser.contents)
2010
2011        # Revert transfer
2012        self.browser.open(self.student_path + '/studycourse_1')
2013        self.browser.getLink("Reactivate").click()
2014        self.browser.getControl("Revert now").click()
2015        self.assertTrue('Previous transfer reverted' in self.browser.contents)
2016        results = list(
2017            cat.searchResults(
2018            certcode=('CERT1', 'CERT1')))
2019        self.assertTrue(results[0] is self.student)
2020        self.assertEqual([i for i in self.student.keys()],
2021            [u'accommodation', u'payments', u'studycourse'])
2022
2023    def test_login_as_student(self):
2024        # StudentImpersonators can login as student
2025        # Create clearance officer
2026        self.app['users'].addUser('mrofficer', SECRET)
2027        self.app['users']['mrofficer'].email = 'mrofficer@foo.ng'
2028        self.app['users']['mrofficer'].title = 'Harry Actor'
2029        prmglobal = IPrincipalRoleManager(self.app)
2030        prmglobal.assignRoleToPrincipal('waeup.StudentImpersonator', 'mrofficer')
2031        prmglobal.assignRoleToPrincipal('waeup.StudentsManager', 'mrofficer')
2032        # Login as student impersonator
2033        self.browser.open(self.login_path)
2034        self.browser.getControl(name="form.login").value = 'mrofficer'
2035        self.browser.getControl(name="form.password").value = SECRET
2036        self.browser.getControl("Login").click()
2037        self.assertMatches('...You logged in...', self.browser.contents)
2038        self.browser.open(self.student_path)
2039        self.browser.getLink("Login as").click()
2040        self.browser.getControl("Set password now").click()
2041        temp_password = self.browser.getControl(name='form.password').value
2042        self.browser.getControl("Login now").click()
2043        self.assertMatches(
2044            '...You successfully logged in as...', self.browser.contents)
2045        # We are logged in as student and can see the 'My Data' tab
2046        self.assertMatches(
2047            '...<a href="#" class="dropdown-toggle" data-toggle="dropdown">...',
2048            self.browser.contents)
2049        self.assertMatches(
2050            '...My Data...',
2051            self.browser.contents)
2052        self.browser.getLink("Logout").click()
2053        # The student can't login with the original password ...
2054        self.browser.open(self.login_path)
2055        self.browser.getControl(name="form.login").value = self.student_id
2056        self.browser.getControl(name="form.password").value = 'spwd'
2057        self.browser.getControl("Login").click()
2058        self.assertMatches(
2059            '...Your account has been temporarily deactivated...',
2060            self.browser.contents)
2061        # ... but with the temporary password
2062        self.browser.open(self.login_path)
2063        self.browser.getControl(name="form.login").value = self.student_id
2064        self.browser.getControl(name="form.password").value = temp_password
2065        self.browser.getControl("Login").click()
2066        self.assertMatches('...You logged in...', self.browser.contents)
2067        # Creation of temp_password is properly logged
2068        logfile = os.path.join(
2069            self.app['datacenter'].storage, 'logs', 'students.log')
2070        logcontent = open(logfile).read()
2071        self.assertTrue(
2072            'mrofficer - students.browser.LoginAsStudentStep1 - K1000000 - '
2073            'temp_password generated: %s' % temp_password in logcontent)
2074
2075    def test_transcripts(self):
2076        studylevel = createObject(u'waeup.StudentStudyLevel')
2077        studylevel.level = 100
2078        studylevel.level_session = 2005
2079        self.student['studycourse'].entry_mode = 'ug_ft'
2080        self.student['studycourse'].addStudentStudyLevel(
2081            self.certificate, studylevel)
2082        studylevel2 = createObject(u'waeup.StudentStudyLevel')
2083        studylevel2.level = 110
2084        studylevel2.level_session = 2006
2085        self.student['studycourse'].addStudentStudyLevel(
2086            self.certificate, studylevel2)
2087        # Add second course (COURSE has been added automatically)
2088        courseticket = createObject('waeup.CourseTicket')
2089        courseticket.code = 'ANYCODE'
2090        courseticket.title = u'Any TITLE'
2091        courseticket.credits = 13
2092        courseticket.score = 66
2093        courseticket.semester = 1
2094        courseticket.dcode = u'ANYDCODE'
2095        courseticket.fcode = u'ANYFCODE'
2096        self.student['studycourse']['110']['COURSE2'] = courseticket
2097        self.student['studycourse']['100']['COURSE1'].score = 55
2098        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 3.0)
2099        self.assertEqual(self.student['studycourse']['110'].gpa_params_rectified[0], 4.0)
2100        # Get transcript data
2101        td = self.student['studycourse'].getTranscriptData()
2102        self.assertEqual(td[0][0]['level_key'], '100')
2103        self.assertEqual(td[0][0]['sgpa'], 3.0)
2104        self.assertEqual(td[0][0]['level'].level, 100)
2105        self.assertEqual(td[0][0]['level'].level_session, 2005)
2106        self.assertEqual(td[0][0]['tickets_1'][0].code, 'COURSE1')
2107        self.assertEqual(td[0][1]['level_key'], '110')
2108        self.assertEqual(td[0][1]['sgpa'], 4.0)
2109        self.assertEqual(td[0][1]['level'].level, 110)
2110        self.assertEqual(td[0][1]['level'].level_session, 2006)
2111        self.assertEqual(td[0][1]['tickets_1'][0].code, 'ANYCODE')
2112        self.assertEqual(td[1], 3.5652173913043477)
2113        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
2114        self.browser.open(self.student_path + '/studycourse/transcript')
2115        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2116        self.assertTrue('Transcript' in self.browser.contents)
2117        # Officers can open the pdf transcript
2118        self.browser.open(self.student_path + '/studycourse/transcript.pdf')
2119        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2120        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2121        path = os.path.join(samples_dir(), 'transcript.pdf')
2122        open(path, 'wb').write(self.browser.contents)
2123        print "Sample PDF transcript.pdf written to %s" % path
2124
2125    def test_process_transcript(self):
2126        IWorkflowState(self.student).setState('transcript requested')
2127        notify(grok.ObjectModifiedEvent(self.student))
2128        self.student['studycourse'].transcript_comment = (
2129            u'On 07/08/2013 08:59:54 UTC K1000000 wrote:\n\nComment line 1 \n'
2130            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
2131            'Address line2\n\n')
2132        # Create officer with both roles
2133        self.app['users'].addUser('mrtranscript', SECRET)
2134        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
2135        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
2136        prmglobal = IPrincipalRoleManager(self.app)
2137        prmglobal.assignRoleToPrincipal('waeup.TranscriptOfficer', 'mrtranscript')
2138        prmglobal.assignRoleToPrincipal('waeup.StudentsManager', 'mrtranscript')
2139        prmglobal.assignRoleToPrincipal('waeup.TranscriptSignee', 'mrtranscript')
2140        # Login
2141        self.browser.open(self.login_path)
2142        self.browser.getControl(name="form.login").value = 'mrtranscript'
2143        self.browser.getControl(name="form.password").value = SECRET
2144        self.browser.getControl("Login").click()
2145        self.assertMatches('...You logged in...', self.browser.contents)
2146        # Officer can see his roles
2147        self.browser.getLink("My Roles").click()
2148        self.assertMatches(
2149            '...<div>Transcript Officer</div>...',
2150            self.browser.contents)
2151        # Officer can search for students in state 'transcript requested'
2152        self.browser.open(self.container_path)
2153        self.browser.getControl(name="searchtype").value = [
2154            'transcript requested']
2155        self.browser.getControl("Find student(s)").click()
2156        self.assertTrue('Anna Tester' in self.browser.contents)
2157        self.browser.getLink("K1000000").click()
2158        self.assertFalse('Release transcript request' in self.browser.contents)
2159        # Officers can still edit studycourse, studylevel and course tickets.
2160        self.browser.open(self.studycourse_path + '/manage')
2161        self.assertTrue('Undergraduate Full-Time</option>'
2162            in self.browser.contents)
2163        self.browser.getControl(name="form.certificate").value = ['CERT1']
2164        self.browser.getControl(name="form.current_session").value = ['2004']
2165        self.browser.getControl(name="form.current_verdict").value = ['A']
2166        self.browser.getControl(name="form.entry_mode").value = ['ug_ft']
2167        self.browser.getControl("Save").click()
2168        self.browser.getControl(name="form.current_level").value = ['100']
2169        self.browser.getControl("Save").click()
2170        self.browser.getControl(name="addlevel").value = ['100']
2171        self.browser.getControl(name="level_session").value = ['2004']
2172        self.browser.getControl("Add study level").click()
2173        self.browser.getLink("100").click()
2174        self.browser.getLink("Manage").click()
2175        self.browser.getControl(name="form.level_session").value = ['2002']
2176        self.browser.getControl("Save").click()
2177        self.browser.getLink("COURSE1").click()
2178        self.browser.getLink("Manage").click()
2179        self.browser.getControl("Save").click()
2180        self.assertTrue('Form has been saved' in self.browser.contents)
2181        # Officer can edit transcript remarks and validate the transcript
2182        self.browser.open(self.studycourse_path + '/transcript')
2183        self.browser.getLink("Validate transcript").click()
2184        self.browser.getLink("Edit").click()
2185        self.assertEqual(
2186            self.browser.url, self.studycourse_path + '/100/remark')
2187        self.browser.getControl(
2188            name="form.transcript_remark").value = 'Oh, the student failed'
2189        self.browser.getControl(
2190            "Save remark and go and back to transcript validation page").click()
2191        self.assertEqual(
2192            self.browser.url,
2193            self.studycourse_path + '/validate_transcript#tab4')
2194        self.assertEqual(self.student['studycourse']['100'].transcript_remark,
2195            'Oh, the student failed')
2196        self.browser.getControl("Save comment and validate transcript").click()
2197        # After validation all manage forms are locked.
2198        self.browser.open(self.studycourse_path + '/manage')
2199        self.assertTrue('The requested form is locked' in self.browser.contents)
2200        self.assertFalse('Undergraduate Full-Time</option>'
2201            in self.browser.contents)
2202        self.browser.open(self.studycourse_path + '/100/manage')
2203        self.assertTrue('The requested form is locked' in self.browser.contents)
2204        self.browser.open(self.studycourse_path + '/100/COURSE1/manage')
2205        self.assertTrue('The requested form is locked' in self.browser.contents)
2206        self.browser.open(self.studycourse_path + '/100/remark')
2207        self.assertTrue('The requested form is locked' in self.browser.contents)
2208
2209        # Transcript can be signed if officer has the permission to sign
2210        #self.browser.open(self.studycourse_path + '/transcript')
2211        #self.assertFalse('Sign transcript' in self.browser.contents)
2212        #prmglobal = IPrincipalRoleManager(self.app)
2213        #prmglobal.assignRoleToPrincipal('waeup.TranscriptSignee', 'mrtranscript')
2214
2215        self.browser.open(self.studycourse_path + '/transcript')
2216        self.browser.getLink("Sign transcript electronically").click()
2217        # Transcript signing has been logged ...
2218        logfile = os.path.join(
2219            self.app['datacenter'].storage, 'logs', 'students.log')
2220        logcontent = open(logfile).read()
2221        self.assertTrue(
2222            'mrtranscript - students.browser.StudentTranscriptSignView - '
2223            'K1000000 - Transcript signed' in logcontent)
2224        # ... appears in the student's history ...
2225        self.browser.open(self.history_path)
2226        self.assertTrue('Transcript signed by Ruth Gordon'
2227            in self.browser.contents)
2228        # ... and is also stored in the transcript_signee attribute.
2229        self.assertTrue(
2230            u'Electronically signed by Ruth Gordon (mrtranscript) on '
2231            in self.student['studycourse'].transcript_signees)
2232        # Officer can release the transcript
2233        self.browser.open(self.studycourse_path + '/transcript')
2234        self.browser.getLink("Release transcript").click()
2235        self.assertTrue(' UTC K1000000 wrote:<br><br>Comment line 1 <br>'
2236        'Comment line2<br><br>Dispatch Address:<br>Address line 1 <br>'
2237        'Address line2<br><br></p>' in self.browser.contents)
2238        self.browser.getControl(name="comment").value = (
2239            'Hello,\nYour transcript has been sent to the address provided.')
2240        self.browser.getControl("Save comment and release transcript").click()
2241        self.assertTrue(
2242            'UTC mrtranscript wrote:\n\nHello,\nYour transcript has '
2243            'been sent to the address provided.\n\n'
2244            in self.student['studycourse'].transcript_comment)
2245        # The comment has been logged
2246        logfile = os.path.join(
2247            self.app['datacenter'].storage, 'logs', 'students.log')
2248        logcontent = open(logfile).read()
2249        self.assertTrue(
2250            'mrtranscript - students.browser.StudentTranscriptReleaseFormPage - '
2251            'K1000000 - comment: Hello,<br>'
2252            'Your transcript has been sent to the address provided'
2253            in logcontent)
2254        # File has been stored in the file system
2255        # Check if transcript exists in the file system and is a PDF file
2256        storage = getUtility(IExtFileStore)
2257        file_id = IFileStoreNameChooser(
2258            self.student).chooseName(attr='final_transcript.pdf')
2259        pdf = storage.getFile(file_id).read()
2260        self.assertTrue(len(pdf) > 0)
2261        self.assertEqual(pdf[:8], '%PDF-1.4')
2262        # Copy the file to samples_dir
2263        path = os.path.join(samples_dir(), 'final_transcript.pdf')
2264        open(path, 'wb').write(pdf)
2265        print "Sample PDF final_transcript.pdf written to %s" % path
2266        # Check if there is an transcript pdf link in UI
2267        self.browser.open(self.student_path)
2268        self.assertTrue('Final Transcript' in self.browser.contents)
2269        self.browser.getLink("Final Transcript").click()
2270        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2271        self.assertEqual(self.browser.headers['Content-Type'],
2272                         'application/pdf')
2273        # Transcript views are no longer accessible
2274        self.browser.open(self.studycourse_path)
2275        self.assertFalse('studycourse/transcript' in self.browser.contents)
2276        self.browser.open(self.studycourse_path + '/transcript')
2277        self.assertTrue('Forbidden!' in self.browser.contents)
2278        self.browser.open(self.studycourse_path + '/transcript.pdf')
2279        self.assertTrue('Forbidden!' in self.browser.contents)
2280        # If we reset the transcript process
2281        # (can't be done by transcript officer), the file will be deleted
2282        IWorkflowInfo(self.student).fireTransition('reset11')
2283        self.browser.open(self.student_path)
2284        self.assertFalse('Final Transcript' in self.browser.contents)
2285        # ... and transcript process information has been removed
2286        self.assertEqual(self.student['studycourse'].transcript_comment, None)
2287        self.assertEqual(self.student['studycourse'].transcript_signees, None)
2288
2289    def test_landingpage_transcript_officer(self):
2290        IWorkflowState(self.student).setState('transcript requested')
2291        notify(grok.ObjectModifiedEvent(self.student))
2292        # Create transcript officer
2293        self.app['users'].addUser('mrtranscript', SECRET)
2294        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
2295        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
2296        # We assign transcript officer role at faculty level
2297        fac = self.app['faculties']['fac1']
2298        prmlocal = IPrincipalRoleManager(fac)
2299        prmlocal.assignRoleToPrincipal(
2300            'waeup.local.TranscriptOfficer', 'mrtranscript')
2301        notify(LocalRoleSetEvent(
2302            fac, 'waeup.local.TranscriptOfficer', 'mrtranscript', granted=True))
2303        # Login as transcript officer
2304        self.browser.open(self.login_path)
2305        self.browser.getControl(name="form.login").value = 'mrtranscript'
2306        self.browser.getControl(name="form.password").value = SECRET
2307        self.browser.getControl("Login").click()
2308        self.assertMatches('...You logged in...', self.browser.contents)
2309        # Officer is on landing page and does see the transcript link
2310        self.assertTrue(
2311            'http://localhost/app/students/K1000000/studycourse/transcript'
2312            in self.browser.contents)
2313        self.browser.getLink("K1000000").click()
2314        self.assertTrue(
2315            'Anna Tester: Transcript Data' in self.browser.contents)
2316        # Officer is on transcript page and can validate the transcript
2317        self.browser.getLink("Validate transcript").click()
2318        self.browser.getControl("Save comment and validate transcript").click()
2319        self.assertTrue(
2320            '<div class="alert alert-success">Transcript validated.</div>'
2321            in self.browser.contents)
2322        # Officer is still on transcript page and can release the transcript
2323        self.browser.getLink("Release transcript").click()
2324        self.browser.getControl("Save comment and release transcript").click()
2325        self.assertTrue(
2326            '<div class="alert alert-success">'
2327            'Transcript released and final transcript file saved.</div>'
2328            in self.browser.contents)
2329
2330    def test_landingpage_transcript_signee(self):
2331        IWorkflowState(self.student).setState('transcript validated')
2332        notify(grok.ObjectModifiedEvent(self.student))
2333        # Create transcript signee
2334        self.app['users'].addUser('mrtranscript', SECRET)
2335        self.app['users']['mrtranscript'].email = 'mrtranscript@foo.ng'
2336        self.app['users']['mrtranscript'].title = 'Ruth Gordon'
2337        # We assign transcript officer role at faculty level
2338        fac = self.app['faculties']['fac1']
2339        prmlocal = IPrincipalRoleManager(fac)
2340        prmlocal.assignRoleToPrincipal(
2341            'waeup.local.TranscriptSignee', 'mrtranscript')
2342        notify(LocalRoleSetEvent(
2343            fac, 'waeup.local.TranscriptSignee', 'mrtranscript', granted=True))
2344        # Login as transcript officer
2345        self.browser.open(self.login_path)
2346        self.browser.getControl(name="form.login").value = 'mrtranscript'
2347        self.browser.getControl(name="form.password").value = SECRET
2348        self.browser.getControl("Login").click()
2349        self.assertMatches('...You logged in...', self.browser.contents)
2350        # Officer is on landing page and does see the transcript link
2351        self.assertTrue(
2352            'http://localhost/app/students/K1000000/studycourse/transcript'
2353            in self.browser.contents)
2354        self.browser.getLink("K1000000").click()
2355        self.assertTrue(
2356            'Anna Tester: Transcript Data' in self.browser.contents)
2357        # Officer is on transcript page and can sign the transcript
2358        self.browser.getLink("Sign transcript").click()
2359        self.assertTrue(
2360            '<div class="alert alert-success">Transcript signed.</div>'
2361            in self.browser.contents)
2362        # Officer is still on transcript page
2363        self.assertTrue(
2364            'Anna Tester: Transcript Data' in self.browser.contents)
2365        # Officer can sign the transcript only once
2366        self.browser.getLink("Sign transcript").click()
2367        self.assertTrue(
2368            '<div class="alert alert-warning">'
2369            'You have already signed this transcript.</div>'
2370            in self.browser.contents)
2371        # Signature can be seen on transcript page
2372        self.assertTrue(
2373            'Electronically signed by Ruth Gordon (mrtranscript) on'
2374            in self.browser.contents)
2375
2376
2377class StudentUITests(StudentsFullSetup):
2378    # Tests for Student class views and pages
2379
2380    def test_student_change_password(self):
2381        # Students can change the password
2382        self.student.personal_updated = datetime.utcnow()
2383        self.browser.open(self.login_path)
2384        self.browser.getControl(name="form.login").value = self.student_id
2385        self.browser.getControl(name="form.password").value = 'spwd'
2386        self.browser.getControl("Login").click()
2387        self.assertEqual(self.browser.url, self.student_path)
2388        self.assertTrue('You logged in' in self.browser.contents)
2389        # Change password
2390        self.browser.getLink("Change password").click()
2391        self.browser.getControl(name="change_password").value = 'pw'
2392        self.browser.getControl(
2393            name="change_password_repeat").value = 'pw'
2394        self.browser.getControl("Save").click()
2395        self.assertTrue('Password must have at least' in self.browser.contents)
2396        self.browser.getControl(name="change_password").value = 'new_password'
2397        self.browser.getControl(
2398            name="change_password_repeat").value = 'new_passssword'
2399        self.browser.getControl("Save").click()
2400        self.assertTrue('Passwords do not match' in self.browser.contents)
2401        self.browser.getControl(name="change_password").value = 'new_password'
2402        self.browser.getControl(
2403            name="change_password_repeat").value = 'new_password'
2404        self.browser.getControl("Save").click()
2405        self.assertTrue('Password changed' in self.browser.contents)
2406        # We are still logged in. Changing the password hasn't thrown us out.
2407        self.browser.getLink("Base Data").click()
2408        self.assertEqual(self.browser.url, self.student_path)
2409        # We can logout
2410        self.browser.getLink("Logout").click()
2411        self.assertTrue('You have been logged out' in self.browser.contents)
2412        self.assertEqual(self.browser.url, 'http://localhost/app/index')
2413        # We can login again with the new password
2414        self.browser.getLink("Login").click()
2415        self.browser.open(self.login_path)
2416        self.browser.getControl(name="form.login").value = self.student_id
2417        self.browser.getControl(name="form.password").value = 'new_password'
2418        self.browser.getControl("Login").click()
2419        self.assertEqual(self.browser.url, self.student_path)
2420        self.assertTrue('You logged in' in self.browser.contents)
2421        return
2422
2423    def test_forbidden_name(self):
2424        self.student.lastname = u'<TAG>Tester</TAG>'
2425        self.browser.open(self.login_path)
2426        self.browser.getControl(name="form.login").value = self.student_id
2427        self.browser.getControl(name="form.password").value = 'spwd'
2428        self.browser.getControl("Login").click()
2429        self.assertTrue('XXX: Base Data' in self.browser.contents)
2430        self.assertTrue('&lt;TAG&gt;Tester&lt;/TAG&gt;' in self.browser.contents)
2431        self.assertFalse('<TAG>Tester</TAG>' in self.browser.contents)
2432        return
2433
2434    def test_setpassword(self):
2435        # Set password for first-time access
2436        student = Student()
2437        student.reg_number = u'123456'
2438        student.firstname = u'Klaus'
2439        student.lastname = u'Tester'
2440        self.app['students'].addStudent(student)
2441        setpassword_path = 'http://localhost/app/setpassword'
2442        student_path = 'http://localhost/app/students/%s' % student.student_id
2443        self.browser.open(setpassword_path)
2444        self.browser.getControl(name="ac_series").value = self.existing_pwdseries
2445        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
2446        self.browser.getControl(name="reg_number").value = '223456'
2447        self.browser.getControl("Set").click()
2448        self.assertMatches('...No student found...',
2449                           self.browser.contents)
2450        self.browser.getControl(name="reg_number").value = '123456'
2451        self.browser.getControl(name="ac_number").value = '999999'
2452        self.browser.getControl("Set").click()
2453        self.assertMatches('...Access code is invalid...',
2454                           self.browser.contents)
2455        self.browser.getControl(name="ac_number").value = self.existing_pwdnumber
2456        self.browser.getControl("Set").click()
2457        self.assertMatches('...Password has been set. Your Student Id is...',
2458                           self.browser.contents)
2459        self.browser.getControl("Set").click()
2460        self.assertMatches(
2461            '...Password has already been set. Your Student Id is...',
2462            self.browser.contents)
2463        existing_pwdpin = self.pwdpins[1]
2464        parts = existing_pwdpin.split('-')[1:]
2465        existing_pwdseries, existing_pwdnumber = parts
2466        self.browser.getControl(name="ac_series").value = existing_pwdseries
2467        self.browser.getControl(name="ac_number").value = existing_pwdnumber
2468        self.browser.getControl(name="reg_number").value = '123456'
2469        self.browser.getControl("Set").click()
2470        self.assertMatches(
2471            '...You are using the wrong Access Code...',
2472            self.browser.contents)
2473        # The student can login with the new credentials
2474        self.browser.open(self.login_path)
2475        self.browser.getControl(name="form.login").value = student.student_id
2476        self.browser.getControl(
2477            name="form.password").value = self.existing_pwdnumber
2478        self.browser.getControl("Login").click()
2479        self.assertEqual(self.browser.url, student_path)
2480        self.assertTrue('You logged in' in self.browser.contents)
2481        return
2482
2483    def test_student_login(self):
2484        # Student cant login if their password is not set
2485        self.student.password = None
2486        self.browser.open(self.login_path)
2487        self.browser.getControl(name="form.login").value = self.student_id
2488        self.browser.getControl(name="form.password").value = 'spwd'
2489        self.browser.getControl("Login").click()
2490        self.assertTrue(
2491            'You entered invalid credentials.' in self.browser.contents)
2492        # We set the password again
2493        IUserAccount(
2494            self.app['students'][self.student_id]).setPassword('spwd')
2495        # Students can't login if their account is suspended/deactivated
2496        self.student.suspended = True
2497        self.browser.open(self.login_path)
2498        self.browser.getControl(name="form.login").value = self.student_id
2499        self.browser.getControl(name="form.password").value = 'spwd'
2500        self.browser.getControl("Login").click()
2501        self.assertMatches(
2502            '...<div class="alert alert-warning">'
2503            'Your account has been deactivated.</div>...', self.browser.contents)
2504        # If suspended_comment is set this message will be flashed instead
2505        self.student.suspended_comment = u'Aetsch baetsch!'
2506        self.browser.getControl(name="form.login").value = self.student_id
2507        self.browser.getControl(name="form.password").value = 'spwd'
2508        self.browser.getControl("Login").click()
2509        self.assertMatches(
2510            '...<div class="alert alert-warning">Aetsch baetsch!</div>...',
2511            self.browser.contents)
2512        self.student.suspended = False
2513        # Students can't login if a temporary password has been set and
2514        # is not expired
2515        self.app['students'][self.student_id].setTempPassword(
2516            'anybody', 'temp_spwd')
2517        self.browser.open(self.login_path)
2518        self.browser.getControl(name="form.login").value = self.student_id
2519        self.browser.getControl(name="form.password").value = 'spwd'
2520        self.browser.getControl("Login").click()
2521        self.assertMatches(
2522            '...Your account has been temporarily deactivated...',
2523            self.browser.contents)
2524        # The student can login with the temporary password
2525        self.browser.open(self.login_path)
2526        self.browser.getControl(name="form.login").value = self.student_id
2527        self.browser.getControl(name="form.password").value = 'temp_spwd'
2528        self.browser.getControl("Login").click()
2529        self.assertMatches(
2530            '...You logged in...', self.browser.contents)
2531        # Student can view the base data
2532        self.browser.open(self.student_path)
2533        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2534        self.assertEqual(self.browser.url, self.student_path)
2535        # When the password expires ...
2536        delta = timedelta(minutes=11)
2537        self.app['students'][self.student_id].temp_password[
2538            'timestamp'] = datetime.utcnow() - delta
2539        self.app['students'][self.student_id]._p_changed = True
2540        # ... the student will be automatically logged out
2541        self.assertRaises(
2542            Unauthorized, self.browser.open, self.student_path)
2543        # Then the student can login with the original password
2544        self.browser.open(self.login_path)
2545        self.browser.getControl(name="form.login").value = self.student_id
2546        self.browser.getControl(name="form.password").value = 'spwd'
2547        self.browser.getControl("Login").click()
2548        self.assertMatches(
2549            '...You logged in...', self.browser.contents)
2550
2551    def test_maintenance_mode(self):
2552        config = grok.getSite()['configuration']
2553        self.browser.open(self.login_path)
2554        self.browser.getControl(name="form.login").value = self.student_id
2555        self.browser.getControl(name="form.password").value = 'spwd'
2556        self.browser.getControl("Login").click()
2557        # Student logged in.
2558        self.assertTrue('You logged in' in self.browser.contents)
2559        self.assertTrue("Anna Tester" in self.browser.contents)
2560        # If maintenance mode is enabled, student is immediately logged out.
2561        config.maintmode_enabled_by = u'any_user'
2562        self.assertRaises(
2563            Unauthorized, self.browser.open, 'http://localhost/app/faculties')
2564        self.browser.open('http://localhost/app/login')
2565        self.assertTrue('The portal is in maintenance mode' in self.browser.contents)
2566        # Student really can't login if maintenance mode is enabled.
2567        self.browser.open(self.login_path)
2568        self.browser.getControl(name="form.login").value = self.student_id
2569        self.browser.getControl(name="form.password").value = 'spwd'
2570        self.browser.getControl("Login").click()
2571        # A second warning is raised.
2572        self.assertTrue(
2573            'The portal is in maintenance mode. You can\'t login!'
2574            in self.browser.contents)
2575        return
2576
2577    def test_student_clearance(self):
2578        # Student cant login if their password is not set
2579        IWorkflowInfo(self.student).fireTransition('admit')
2580        self.browser.open(self.login_path)
2581        self.browser.getControl(name="form.login").value = self.student_id
2582        self.browser.getControl(name="form.password").value = 'spwd'
2583        self.browser.getControl("Login").click()
2584        self.assertMatches(
2585            '...You logged in...', self.browser.contents)
2586        # Admitted student can upload a passport picture
2587        self.browser.open(self.student_path + '/change_portrait')
2588        ctrl = self.browser.getControl(name='passportuploadedit')
2589        file_obj = open(SAMPLE_IMAGE, 'rb')
2590        file_ctrl = ctrl.mech_control
2591        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
2592        self.browser.getControl(
2593            name='upload_passportuploadedit').click()
2594        self.assertTrue(
2595            'src="http://localhost/app/students/K1000000/passport.jpg"'
2596            in self.browser.contents)
2597        # Students can open admission letter
2598        self.browser.getLink("Base Data").click()
2599        self.browser.getLink("Download admission letter").click()
2600        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2601        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2602        path = os.path.join(samples_dir(), 'admission_slip.pdf')
2603        open(path, 'wb').write(self.browser.contents)
2604        print "Sample PDF admission_slip.pdf written to %s" % path
2605        # Student can view the clearance data
2606        self.browser.open(self.student_path)
2607        self.browser.getLink("Clearance Data").click()
2608        # Student can't open clearance edit form before starting clearance
2609        self.browser.open(self.student_path + '/cedit')
2610        self.assertMatches('...The requested form is locked...',
2611                           self.browser.contents)
2612        self.browser.getLink("Clearance Data").click()
2613        self.browser.getLink("Start clearance").click()
2614        self.student.phone = None
2615        # Uups, we forgot to fill the phone fields
2616        self.browser.getControl("Start clearance").click()
2617        self.assertMatches('...Phone number is missing...',
2618                           self.browser.contents)
2619        self.browser.open(self.student_path + '/edit_base')
2620        self.browser.getControl(name="form.phone.ext").value = '12345'
2621        self.browser.getControl("Save").click()
2622        self.browser.open(self.student_path + '/start_clearance')
2623        self.browser.getControl(name="ac_series").value = '3'
2624        self.browser.getControl(name="ac_number").value = '4444444'
2625        self.browser.getControl("Start clearance now").click()
2626        self.assertMatches('...Activation code is invalid...',
2627                           self.browser.contents)
2628        self.browser.getControl(name="ac_series").value = self.existing_clrseries
2629        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
2630        # Owner is Hans Wurst, AC can't be invalidated
2631        self.browser.getControl("Start clearance now").click()
2632        self.assertMatches('...You are not the owner of this access code...',
2633                           self.browser.contents)
2634        # Set the correct owner
2635        self.existing_clrac.owner = self.student_id
2636        # clr_code might be set (and thus returns None) due importing
2637        # an empty clr_code column.
2638        self.student.clr_code = None
2639        self.browser.getControl("Start clearance now").click()
2640        self.assertMatches('...Clearance process has been started...',
2641                           self.browser.contents)
2642        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
2643        self.browser.getControl("Save", index=0).click()
2644        # Student can view the clearance data
2645        self.browser.getLink("Clearance Data").click()
2646        # and go back to the edit form
2647        self.browser.getLink("Edit").click()
2648        # Students can upload documents
2649        ctrl = self.browser.getControl(name='birthcertificateupload')
2650        file_obj = open(SAMPLE_IMAGE, 'rb')
2651        file_ctrl = ctrl.mech_control
2652        file_ctrl.add_file(file_obj, filename='my_birth_certificate.jpg')
2653        self.browser.getControl(
2654            name='upload_birthcertificateupload').click()
2655        self.assertTrue(
2656            'href="http://localhost/app/students/K1000000/birth_certificate"'
2657            in self.browser.contents)
2658        # Students can open clearance slip
2659        self.browser.getLink("View").click()
2660        self.browser.getLink("Download clearance slip").click()
2661        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2662        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2663        # Students can request clearance
2664        self.browser.open(self.edit_clearance_path)
2665        self.browser.getControl("Save and request clearance").click()
2666        self.browser.getControl(name="ac_series").value = self.existing_clrseries
2667        self.browser.getControl(name="ac_number").value = self.existing_clrnumber
2668        self.browser.getControl("Request clearance now").click()
2669        self.assertMatches('...Clearance has been requested...',
2670                           self.browser.contents)
2671        # Student can't reopen clearance form after requesting clearance
2672        self.browser.open(self.student_path + '/cedit')
2673        self.assertMatches('...The requested form is locked...',
2674                           self.browser.contents)
2675
2676    def test_student_course_registration(self):
2677        # Student cant login if their password is not set
2678        IWorkflowInfo(self.student).fireTransition('admit')
2679        self.browser.open(self.login_path)
2680        self.browser.getControl(name="form.login").value = self.student_id
2681        self.browser.getControl(name="form.password").value = 'spwd'
2682        self.browser.getControl("Login").click()
2683        # Student can't add study level if not in state 'school fee paid'
2684        self.browser.open(self.student_path + '/studycourse/add')
2685        self.assertMatches('...The requested form is locked...',
2686                           self.browser.contents)
2687        # ... and must be transferred first
2688        IWorkflowState(self.student).setState('school fee paid')
2689        # Now students can add the current study level
2690        self.browser.getLink("Study Course").click()
2691        self.student['studycourse'].current_level = None
2692        self.browser.getLink("Add course list").click()
2693        self.assertMatches('...Your data are incomplete...',
2694                           self.browser.contents)
2695        self.student['studycourse'].current_level = 100
2696        self.browser.getLink("Add course list").click()
2697        self.assertMatches('...Add current level 100 (Year 1)...',
2698                           self.browser.contents)
2699        self.browser.getControl("Create course list now").click()
2700        # A level with one course ticket was created
2701        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
2702        self.browser.getLink("100").click()
2703        self.browser.getLink("Edit course list").click()
2704        self.browser.getLink("here").click()
2705        self.browser.getControl(name="form.course").value = ['COURSE1']
2706        self.browser.getControl("Add course ticket").click()
2707        self.assertMatches('...The ticket exists...',
2708                           self.browser.contents)
2709        self.student['studycourse'].current_level = 200
2710        self.browser.getLink("Study Course").click()
2711        self.browser.getLink("Add course list").click()
2712        self.assertMatches('...Add current level 200 (Year 2)...',
2713                           self.browser.contents)
2714        self.browser.getControl("Create course list now").click()
2715        self.browser.getLink("200").click()
2716        self.browser.getLink("Edit course list").click()
2717        self.browser.getLink("here").click()
2718        self.browser.getControl(name="form.course").value = ['COURSE1']
2719        self.course.credits = 100
2720        self.browser.getControl("Add course ticket").click()
2721        self.assertMatches(
2722            '...Maximum credits exceeded...', self.browser.contents)
2723        self.course.credits = 10
2724        self.browser.getControl("Add course ticket").click()
2725        self.assertMatches('...The ticket exists...',
2726                           self.browser.contents)
2727        # Indeed the ticket exists as carry-over course from level 100
2728        # since its score was 0
2729        self.assertTrue(
2730            self.student['studycourse']['200']['COURSE1'].carry_over is True)
2731        self.assertTrue(
2732            self.student['studycourse']['200']['COURSE1'].course_category is None)
2733        # Students can open the pdf course registration slip
2734        self.browser.open(self.student_path + '/studycourse/200')
2735        self.browser.getLink("Download course registration slip").click()
2736        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2737        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2738        # Students can remove course tickets
2739        self.browser.open(self.student_path + '/studycourse/200/edit')
2740        self.browser.getControl("Remove selected", index=0).click()
2741        self.assertTrue('No ticket selected' in self.browser.contents)
2742        # No ticket can be selected since the carry-over course is a core course
2743        self.assertRaises(
2744            LookupError, self.browser.getControl, name='val_id')
2745        self.student['studycourse']['200']['COURSE1'].mandatory = False
2746        self.browser.open(self.student_path + '/studycourse/200/edit')
2747        # Course list can't be registered if total_credits exceeds max_credits
2748        self.student['studycourse']['200']['COURSE1'].credits = 60
2749        self.browser.getControl("Register course list").click()
2750        self.assertTrue('Maximum credits exceeded' in self.browser.contents)
2751        # Student can now remove the ticket
2752        ctrl = self.browser.getControl(name='val_id')
2753        ctrl.getControl(value='COURSE1').selected = True
2754        self.browser.getControl("Remove selected", index=0).click()
2755        self.assertTrue('Successfully removed' in self.browser.contents)
2756        # Removing course tickets is properly logged
2757        logfile = os.path.join(
2758            self.app['datacenter'].storage, 'logs', 'students.log')
2759        logcontent = open(logfile).read()
2760        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage '
2761        '- K1000000 - removed: COURSE1 at 200' in logcontent)
2762        # They can add the same ticket using the edit page directly.
2763        # We can do the same by adding the course on the manage page directly
2764        self.browser.getControl(name="course").value = 'COURSE1'
2765        self.browser.getControl("Add course ticket").click()
2766        # Adding course tickets is logged
2767        logfile = os.path.join(
2768            self.app['datacenter'].storage, 'logs', 'students.log')
2769        logcontent = open(logfile).read()
2770        self.assertTrue('K1000000 - students.browser.StudyLevelEditFormPage - '
2771            'K1000000 - added: COURSE1|200|2004' in logcontent)
2772        # Course list can be registered
2773        self.browser.getControl("Register course list").click()
2774        self.assertTrue('Course list has been registered' in self.browser.contents)
2775        self.assertEqual(self.student.state, 'courses registered')
2776        # Course list can be unregistered
2777        self.browser.getLink("Unregister courses").click()
2778        self.assertEqual(self.student.state, 'school fee paid')
2779        self.assertTrue('Course list has been unregistered' in self.browser.contents)
2780        self.browser.open(self.student_path + '/studycourse/200/unregister_courses')
2781        self.assertTrue('You are in the wrong state' in self.browser.contents)
2782        # Students can view the transcript
2783        #self.browser.open(self.studycourse_path)
2784        #self.browser.getLink("Transcript").click()
2785        #self.browser.getLink("Academic Transcript").click()
2786        #self.assertEqual(self.browser.headers['Status'], '200 Ok')
2787        #self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2788        return
2789
2790    def test_student_ticket_update(self):
2791        IWorkflowState(self.student).setState('school fee paid')
2792        self.student['studycourse'].current_level = 100
2793        self.browser.open(self.login_path)
2794        self.browser.getControl(name="form.login").value = self.student_id
2795        self.browser.getControl(name="form.password").value = 'spwd'
2796        self.browser.getControl("Login").click()
2797        # Now students can add the current study level
2798        self.browser.getLink("Study Course").click()
2799        self.browser.getLink("Add course list").click()
2800        self.assertMatches('...Add current level 100 (Year 1)...',
2801                           self.browser.contents)
2802        self.browser.getControl("Create course list now").click()
2803        # A level with one course ticket was created
2804        self.assertEqual(
2805            self.student['studycourse']['100'].number_of_tickets, 1)
2806        self.browser.getLink("100").click()
2807        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
2808        self.browser.getLink("Edit course list").click()
2809        self.browser.getControl("Update all tickets").click()
2810        self.assertTrue('All course tickets updated.' in self.browser.contents)
2811        # ... nothing has changed
2812        self.assertTrue('<td>Unnamed Course</td>' in self.browser.contents)
2813        # We change the title of the course
2814        self.course.title = u'New Title'
2815        self.browser.getControl("Update all tickets").click()
2816        self.assertTrue('<td>New Title</td>' in self.browser.contents)
2817        # We remove the course
2818        del self.app['faculties']['fac1']['dep1'].courses['COURSE1']
2819        self.browser.getControl("Update all tickets").click()
2820        self.assertTrue(' <td>New Title (course cancelled)</td>'
2821            in self.browser.contents)
2822        # Course ticket invalidation has been logged
2823        logfile = os.path.join(
2824            self.app['datacenter'].storage, 'logs', 'students.log')
2825        logcontent = open(logfile).read()
2826        self.assertTrue(
2827            'K1000000 - students.browser.StudyLevelEditFormPage - '
2828            'K1000000 - course tickets invalidated: COURSE1'
2829            in logcontent)
2830        return
2831
2832    def test_student_course_registration_outstanding(self):
2833        self.course = createObject('waeup.Course')
2834        self.course.code = 'COURSE2'
2835        self.course.semester = 1
2836        self.course.credits = 45
2837        self.course.passmark = 40
2838        self.app['faculties']['fac1']['dep1'].courses.addCourse(
2839            self.course)
2840        IWorkflowState(self.student).setState('school fee paid')
2841        self.browser.open(self.login_path)
2842        self.browser.getControl(name="form.login").value = self.student_id
2843        self.browser.getControl(name="form.password").value = 'spwd'
2844        self.browser.getControl("Login").click()
2845        self.browser.open(self.student_path + '/studycourse/add')
2846        self.browser.getControl("Create course list now").click()
2847        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
2848        self.student['studycourse'].current_level = 200
2849        self.browser.getLink("Study Course").click()
2850        self.browser.getLink("Add course list").click()
2851        self.assertMatches('...Add current level 200 (Year 2)...',
2852                           self.browser.contents)
2853        self.browser.getControl("Create course list now").click()
2854        self.browser.getLink("200").click()
2855        self.browser.getLink("Edit course list").click()
2856        self.browser.getLink("here").click()
2857        self.browser.getControl(name="form.course").value = ['COURSE2']
2858        self.browser.getControl("Add course ticket").click()
2859        # Carryover COURSE1 in level 200 already has 10 credits
2860        self.assertMatches(
2861            '...Maximum credits exceeded...', self.browser.contents)
2862        # If COURSE1 is outstanding, its credits won't be considered
2863        self.student['studycourse']['200']['COURSE1'].outstanding = True
2864        self.browser.getControl("Add course ticket").click()
2865        self.assertMatches(
2866            '...Successfully added COURSE2...', self.browser.contents)
2867        return
2868
2869    def test_postgraduate_student_access(self):
2870        self.certificate.study_mode = 'pg_ft'
2871        self.certificate.start_level = 999
2872        self.certificate.end_level = 999
2873        self.student['studycourse'].current_level = 999
2874        IWorkflowState(self.student).setState('school fee paid')
2875        self.browser.open(self.login_path)
2876        self.browser.getControl(name="form.login").value = self.student_id
2877        self.browser.getControl(name="form.password").value = 'spwd'
2878        self.browser.getControl("Login").click()
2879        self.assertTrue(
2880            'You logged in.' in self.browser.contents)
2881        # Now students can add the current study level
2882        self.browser.getLink("Study Course").click()
2883        self.browser.getLink("Add course list").click()
2884        self.assertMatches('...Add current level Postgraduate Level...',
2885                           self.browser.contents)
2886        self.browser.getControl("Create course list now").click()
2887        self.assertTrue("You successfully created a new course list"
2888            in self.browser.contents)
2889        # A level with one course ticket was created
2890        self.assertEqual(self.student['studycourse']['999'].number_of_tickets, 0)
2891        self.browser.getLink("Edit course list").click()
2892        self.browser.getLink("here").click()
2893        self.browser.getControl(name="form.course").value = ['COURSE1']
2894        self.browser.getControl("Add course ticket").click()
2895        self.assertMatches('...Successfully added COURSE1...',
2896                           self.browser.contents)
2897        # Postgraduate students can't register course lists
2898        self.browser.getControl("Register course list").click()
2899        self.assertTrue("your course list can't bee registered"
2900            in self.browser.contents)
2901        self.assertEqual(self.student.state, 'school fee paid')
2902        return
2903
2904    def test_student_clearance_wo_clrcode(self):
2905        IWorkflowState(self.student).setState('clearance started')
2906        self.browser.open(self.login_path)
2907        self.browser.getControl(name="form.login").value = self.student_id
2908        self.browser.getControl(name="form.password").value = 'spwd'
2909        self.browser.getControl("Login").click()
2910        self.browser.open(self.edit_clearance_path)
2911        self.browser.getControl(name="form.date_of_birth").value = '09/10/1961'
2912        self.browser.getControl("Save and request clearance").click()
2913        self.assertMatches('...Clearance has been requested...',
2914                           self.browser.contents)
2915
2916    def test_student_clearance_payment(self):
2917        # Login
2918        self.browser.open(self.login_path)
2919        self.browser.getControl(name="form.login").value = self.student_id
2920        self.browser.getControl(name="form.password").value = 'spwd'
2921        self.browser.getControl("Login").click()
2922
2923        # Students can add online clearance payment tickets
2924        self.browser.open(self.payments_path + '/addop')
2925        self.browser.getControl(name="form.p_category").value = ['clearance']
2926        self.browser.getControl("Create ticket").click()
2927        self.assertMatches('...ticket created...',
2928                           self.browser.contents)
2929
2930        # Students can't approve the payment
2931        self.assertEqual(len(self.app['accesscodes']['CLR-0']),0)
2932        ctrl = self.browser.getControl(name='val_id')
2933        value = ctrl.options[0]
2934        self.browser.getLink(value).click()
2935        payment_url = self.browser.url
2936        self.assertRaises(
2937            Unauthorized, self.browser.open, payment_url + '/approve')
2938        # In the base package they can 'use' a fake approval view.
2939        # XXX: I tried to use
2940        # self.student['payments'][value].approveStudentPayment() instead.
2941        # But this function fails in
2942        # w.k.accesscodes.accesscode.create_accesscode.
2943        # grok.getSite returns None in tests.
2944        self.browser.open(payment_url + '/fake_approve')
2945        self.assertMatches('...Payment approved...',
2946                          self.browser.contents)
2947        expected = '''...
2948        <td>
2949          <span>Paid</span>
2950        </td>...'''
2951        expected = '''...
2952        <td>
2953          <span>Paid</span>
2954        </td>...'''
2955        self.assertMatches(expected,self.browser.contents)
2956        payment_id = self.student['payments'].keys()[0]
2957        payment = self.student['payments'][payment_id]
2958        self.assertEqual(payment.p_state, 'paid')
2959        self.assertEqual(payment.r_amount_approved, 3456.0)
2960        self.assertEqual(payment.r_code, 'AP')
2961        self.assertEqual(payment.r_desc, u'Payment approved by Anna Tester')
2962        # The new CLR-0 pin has been created
2963        self.assertEqual(len(self.app['accesscodes']['CLR-0']),1)
2964        pin = self.app['accesscodes']['CLR-0'].keys()[0]
2965        ac = self.app['accesscodes']['CLR-0'][pin]
2966        self.assertEqual(ac.owner, self.student_id)
2967        self.assertEqual(ac.cost, 3456.0)
2968
2969        # Students can open the pdf payment slip
2970        self.browser.open(payment_url + '/payment_slip.pdf')
2971        self.assertEqual(self.browser.headers['Status'], '200 Ok')
2972        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
2973
2974        # The new CLR-0 pin can be used for starting clearance
2975        # but they have to upload a passport picture first
2976        # which is only possible in state admitted
2977        self.browser.open(self.student_path + '/change_portrait')
2978        self.assertMatches('...form is locked...',
2979                          self.browser.contents)
2980        IWorkflowInfo(self.student).fireTransition('admit')
2981        self.browser.open(self.student_path + '/change_portrait')
2982        image = open(SAMPLE_IMAGE, 'rb')
2983        ctrl = self.browser.getControl(name='passportuploadedit')
2984        file_ctrl = ctrl.mech_control
2985        file_ctrl.add_file(image, filename='my_photo.jpg')
2986        self.browser.getControl(
2987            name='upload_passportuploadedit').click()
2988        self.browser.open(self.student_path + '/start_clearance')
2989        parts = pin.split('-')[1:]
2990        clrseries, clrnumber = parts
2991        self.browser.getControl(name="ac_series").value = clrseries
2992        self.browser.getControl(name="ac_number").value = clrnumber
2993        self.browser.getControl("Start clearance now").click()
2994        self.assertMatches('...Clearance process has been started...',
2995                           self.browser.contents)
2996
2997    def test_student_schoolfee_payment(self):
2998        configuration = createObject('waeup.SessionConfiguration')
2999        configuration.academic_session = 2005
3000        self.app['configuration'].addSessionConfiguration(configuration)
3001        # Login
3002        self.browser.open(self.login_path)
3003        self.browser.getControl(name="form.login").value = self.student_id
3004        self.browser.getControl(name="form.password").value = 'spwd'
3005        self.browser.getControl("Login").click()
3006
3007        # Students can add online school fee payment tickets.
3008        IWorkflowState(self.student).setState('returning')
3009        self.browser.open(self.payments_path)
3010        self.assertRaises(
3011            LookupError, self.browser.getControl, name='val_id')
3012        self.browser.getLink("Add current session payment ticket").click()
3013        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3014        self.browser.getControl("Create ticket").click()
3015        self.assertMatches('...ticket created...',
3016                           self.browser.contents)
3017        ctrl = self.browser.getControl(name='val_id')
3018        value = ctrl.options[0]
3019        self.browser.getLink(value).click()
3020        self.assertMatches('...Amount Authorized...',
3021                           self.browser.contents)
3022        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
3023        # Payment session and will be calculated as defined
3024        # in w.k.students.utils because we set changed the state
3025        # to returning
3026        self.assertEqual(self.student['payments'][value].p_session, 2005)
3027        self.assertEqual(self.student['payments'][value].p_level, 200)
3028
3029        # Student is the payer of the payment ticket.
3030        payer = IPayer(self.student['payments'][value])
3031        self.assertEqual(payer.display_fullname, 'Anna Tester')
3032        self.assertEqual(payer.id, self.student_id)
3033        self.assertEqual(payer.faculty, 'fac1')
3034        self.assertEqual(payer.department, 'dep1')
3035
3036        # We simulate the approval
3037        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
3038        self.browser.open(self.browser.url + '/fake_approve')
3039        self.assertMatches('...Payment approved...',
3040                          self.browser.contents)
3041
3042        ## The new SFE-0 pin can be used for starting new session
3043        #self.browser.open(self.studycourse_path)
3044        #self.browser.getLink('Start new session').click()
3045        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
3046        #parts = pin.split('-')[1:]
3047        #sfeseries, sfenumber = parts
3048        #self.browser.getControl(name="ac_series").value = sfeseries
3049        #self.browser.getControl(name="ac_number").value = sfenumber
3050        #self.browser.getControl("Start now").click()
3051        #self.assertMatches('...Session started...',
3052        #                   self.browser.contents)
3053
3054        self.assertTrue(self.student.state == 'school fee paid')
3055        return
3056
3057    def test_student_bedallocation_payment(self):
3058        # Login
3059        self.browser.open(self.login_path)
3060        self.browser.getControl(name="form.login").value = self.student_id
3061        self.browser.getControl(name="form.password").value = 'spwd'
3062        self.browser.getControl("Login").click()
3063        self.browser.open(self.payments_path)
3064        self.browser.open(self.payments_path + '/addop')
3065        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
3066        self.browser.getControl("Create ticket").click()
3067        self.assertMatches('...ticket created...',
3068                           self.browser.contents)
3069        # Students can remove only online payment tickets which have
3070        # not received a valid callback
3071        self.browser.open(self.payments_path)
3072        ctrl = self.browser.getControl(name='val_id')
3073        value = ctrl.options[0]
3074        ctrl.getControl(value=value).selected = True
3075        self.browser.getControl("Remove selected", index=0).click()
3076        self.assertTrue('Successfully removed' in self.browser.contents)
3077
3078    def test_student_maintenance_payment(self):
3079        # Login
3080        self.browser.open(self.login_path)
3081        self.browser.getControl(name="form.login").value = self.student_id
3082        self.browser.getControl(name="form.password").value = 'spwd'
3083        self.browser.getControl("Login").click()
3084        self.browser.open(self.payments_path)
3085        self.browser.open(self.payments_path + '/addop')
3086        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
3087        self.browser.getControl("Create ticket").click()
3088        self.assertMatches('...You have not yet booked accommodation...',
3089                           self.browser.contents)
3090        # We continue this test in test_student_accommodation
3091
3092    def test_student_previous_payments(self):
3093        configuration = createObject('waeup.SessionConfiguration')
3094        configuration.academic_session = 2000
3095        configuration.clearance_fee = 3456.0
3096        configuration.booking_fee = 123.4
3097        self.app['configuration'].addSessionConfiguration(configuration)
3098        configuration2 = createObject('waeup.SessionConfiguration')
3099        configuration2.academic_session = 2003
3100        configuration2.clearance_fee = 3456.0
3101        configuration2.booking_fee = 123.4
3102        self.app['configuration'].addSessionConfiguration(configuration2)
3103        configuration3 = createObject('waeup.SessionConfiguration')
3104        configuration3.academic_session = 2005
3105        configuration3.clearance_fee = 3456.0
3106        configuration3.booking_fee = 123.4
3107        self.app['configuration'].addSessionConfiguration(configuration3)
3108        self.student['studycourse'].entry_session = 2002
3109
3110        # Login
3111        self.browser.open(self.login_path)
3112        self.browser.getControl(name="form.login").value = self.student_id
3113        self.browser.getControl(name="form.password").value = 'spwd'
3114        self.browser.getControl("Login").click()
3115
3116        # Students can add previous school fee payment tickets in any state.
3117        IWorkflowState(self.student).setState('courses registered')
3118        self.browser.open(self.payments_path)
3119        self.browser.getLink("Add previous session payment ticket").click()
3120
3121        # Previous session payment form is provided
3122        self.assertEqual(self.student.current_session, 2004)
3123        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3124        self.browser.getControl(name="form.p_session").value = ['2000']
3125        self.browser.getControl(name="form.p_level").value = ['300']
3126        self.browser.getControl("Create ticket").click()
3127        self.assertMatches('...The previous session must not fall below...',
3128                           self.browser.contents)
3129        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3130        self.browser.getControl(name="form.p_session").value = ['2005']
3131        self.browser.getControl(name="form.p_level").value = ['300']
3132        self.browser.getControl("Create ticket").click()
3133        self.assertMatches('...This is not a previous session...',
3134                           self.browser.contents)
3135
3136        # Students can pay current session school fee.
3137        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3138        self.browser.getControl(name="form.p_session").value = ['2004']
3139        self.browser.getControl(name="form.p_level").value = ['300']
3140        self.browser.getControl("Create ticket").click()
3141        self.assertMatches('...ticket created...',
3142                           self.browser.contents)
3143        ctrl = self.browser.getControl(name='val_id')
3144        value = ctrl.options[0]
3145        self.browser.getLink(value).click()
3146        self.assertMatches('...Amount Authorized...',
3147                           self.browser.contents)
3148        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
3149
3150        # Ticket creation is logged.
3151        logfile = os.path.join(
3152            self.app['datacenter'].storage, 'logs', 'students.log')
3153        logcontent = open(logfile).read()
3154        self.assertTrue(
3155            'K1000000 - students.browser.PreviousPaymentAddFormPage - '
3156            'K1000000 - added: %s' % value
3157            in logcontent)
3158
3159        # Payment session is properly set
3160        self.assertEqual(self.student['payments'][value].p_session, 2004)
3161        self.assertEqual(self.student['payments'][value].p_level, 300)
3162
3163        # We simulate the approval
3164        self.browser.open(self.browser.url + '/fake_approve')
3165        self.assertMatches('...Payment approved...',
3166                          self.browser.contents)
3167
3168        # No AC has been created
3169        self.assertEqual(len(self.app['accesscodes']['SFE-0'].keys()), 0)
3170        self.assertTrue(self.student['payments'][value].ac is None)
3171
3172        # Current payment flag is set False
3173        self.assertFalse(self.student['payments'][value].p_current)
3174
3175        # Button and form are not available for students who are in
3176        # states up to cleared
3177        self.student['studycourse'].entry_session = 2004
3178        IWorkflowState(self.student).setState('cleared')
3179        self.browser.open(self.payments_path)
3180        self.assertFalse(
3181            "Add previous session payment ticket" in self.browser.contents)
3182        self.browser.open(self.payments_path + '/addpp')
3183        self.assertTrue(
3184            "No previous payment to be made" in self.browser.contents)
3185        return
3186
3187    def test_postgraduate_student_payments(self):
3188        configuration = createObject('waeup.SessionConfiguration')
3189        configuration.academic_session = 2005
3190        self.app['configuration'].addSessionConfiguration(configuration)
3191        self.certificate.study_mode = 'pg_ft'
3192        self.certificate.start_level = 999
3193        self.certificate.end_level = 999
3194        self.student['studycourse'].current_level = 999
3195        # Login
3196        self.browser.open(self.login_path)
3197        self.browser.getControl(name="form.login").value = self.student_id
3198        self.browser.getControl(name="form.password").value = 'spwd'
3199        self.browser.getControl("Login").click()
3200        # Students can add online school fee payment tickets.
3201        IWorkflowState(self.student).setState('cleared')
3202        self.browser.open(self.payments_path)
3203        self.browser.getLink("Add current session payment ticket").click()
3204        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3205        self.browser.getControl("Create ticket").click()
3206        self.assertMatches('...ticket created...',
3207                           self.browser.contents)
3208        ctrl = self.browser.getControl(name='val_id')
3209        value = ctrl.options[0]
3210        self.browser.getLink(value).click()
3211        self.assertMatches('...Amount Authorized...',
3212                           self.browser.contents)
3213        # Payment session and level are current ones.
3214        # Postgrads have to pay school_fee_1.
3215        self.assertEqual(self.student['payments'][value].amount_auth, 40000.0)
3216        self.assertEqual(self.student['payments'][value].p_session, 2004)
3217        self.assertEqual(self.student['payments'][value].p_level, 999)
3218
3219        # We simulate the approval
3220        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
3221        self.browser.open(self.browser.url + '/fake_approve')
3222        self.assertMatches('...Payment approved...',
3223                          self.browser.contents)
3224
3225        ## The new SFE-0 pin can be used for starting session
3226        #self.browser.open(self.studycourse_path)
3227        #self.browser.getLink('Start new session').click()
3228        #pin = self.app['accesscodes']['SFE-0'].keys()[0]
3229        #parts = pin.split('-')[1:]
3230        #sfeseries, sfenumber = parts
3231        #self.browser.getControl(name="ac_series").value = sfeseries
3232        #self.browser.getControl(name="ac_number").value = sfenumber
3233        #self.browser.getControl("Start now").click()
3234        #self.assertMatches('...Session started...',
3235        #                   self.browser.contents)
3236
3237        self.assertTrue(self.student.state == 'school fee paid')
3238
3239        # Postgrad students do not need to register courses the
3240        # can just pay for the next session.
3241        self.browser.open(self.payments_path)
3242        # Remove first payment to be sure that we access the right ticket
3243        del self.student['payments'][value]
3244        self.browser.getLink("Add current session payment ticket").click()
3245        self.browser.getControl(name="form.p_category").value = ['schoolfee']
3246        self.browser.getControl("Create ticket").click()
3247        ctrl = self.browser.getControl(name='val_id')
3248        value = ctrl.options[0]
3249        self.browser.getLink(value).click()
3250        # Payment session has increased by one, payment level remains the same.
3251        # Returning Postgraduates have to pay school_fee_2.
3252        self.assertEqual(self.student['payments'][value].amount_auth, 20000.0)
3253        self.assertEqual(self.student['payments'][value].p_session, 2005)
3254        self.assertEqual(self.student['payments'][value].p_level, 999)
3255
3256        # Student is still in old session
3257        self.assertEqual(self.student.current_session, 2004)
3258
3259        # We do not need to pay the ticket if any other
3260        # SFE pin is provided
3261        pin_container = self.app['accesscodes']
3262        pin_container.createBatch(
3263            datetime.utcnow(), 'some_userid', 'SFE', 9.99, 1)
3264        pin = pin_container['SFE-1'].values()[0].representation
3265        sfeseries, sfenumber = pin.split('-')[1:]
3266        # The new SFE-1 pin can be used for starting new session
3267        self.browser.open(self.studycourse_path)
3268        self.browser.getLink('Start new session').click()
3269        self.browser.getControl(name="ac_series").value = sfeseries
3270        self.browser.getControl(name="ac_number").value = sfenumber
3271        self.browser.getControl("Start now").click()
3272        self.assertMatches('...Session started...',
3273                           self.browser.contents)
3274        self.assertTrue(self.student.state == 'school fee paid')
3275        # Student is in new session
3276        self.assertEqual(self.student.current_session, 2005)
3277        self.assertEqual(self.student['studycourse'].current_level, 999)
3278        return
3279
3280    def test_student_accommodation(self):
3281        # Create a second hostel with one bed
3282        hostel = Hostel()
3283        hostel.hostel_id = u'hall-2'
3284        hostel.hostel_name = u'Hall 2'
3285        self.app['hostels'].addHostel(hostel)
3286        bed = Bed()
3287        bed.bed_id = u'hall-2_A_101_A'
3288        bed.bed_number = 1
3289        bed.owner = NOT_OCCUPIED
3290        bed.bed_type = u'regular_female_fr'
3291        self.app['hostels'][hostel.hostel_id].addBed(bed)
3292        self.app['hostels'].allocation_expiration = 7
3293
3294        self.browser.open(self.login_path)
3295        self.browser.getControl(name="form.login").value = self.student_id
3296        self.browser.getControl(name="form.password").value = 'spwd'
3297        self.browser.getControl("Login").click()
3298        # Students can add online booking fee payment tickets and open the
3299        # callback view (see test_manage_payments).
3300        self.browser.getLink("Payments").click()
3301        self.browser.getLink("Add current session payment ticket").click()
3302        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
3303        self.browser.getControl("Create ticket").click()
3304        ctrl = self.browser.getControl(name='val_id')
3305        value = ctrl.options[0]
3306        self.browser.getLink(value).click()
3307        self.browser.open(self.browser.url + '/fake_approve')
3308        # The new HOS-0 pin has been created.
3309        self.assertEqual(len(self.app['accesscodes']['HOS-0']),1)
3310        pin = self.app['accesscodes']['HOS-0'].keys()[0]
3311        ac = self.app['accesscodes']['HOS-0'][pin]
3312        parts = pin.split('-')[1:]
3313        sfeseries, sfenumber = parts
3314        # Students can use HOS code and book a bed space with it ...
3315        self.browser.open(self.acco_path)
3316        # ... but not if booking period has expired ...
3317        self.app['hostels'].enddate = datetime.now(pytz.utc)
3318        self.browser.getControl("Book accommodation").click()
3319        self.assertMatches('...Outside booking period: ...',
3320                           self.browser.contents)
3321        self.app['hostels'].enddate = datetime.now(pytz.utc) + timedelta(days=10)
3322        # ... or student data are incomplete ...
3323        self.student['studycourse'].current_level = None
3324        self.browser.getControl("Book accommodation").click()
3325        self.assertMatches('...Your data are incomplete...',
3326            self.browser.contents)
3327        self.student['studycourse'].current_level = 100
3328        # ... or student is not the an allowed state ...
3329        self.browser.getControl("Book accommodation").click()
3330        self.assertMatches('...You are in the wrong...',
3331                           self.browser.contents)
3332        # Students can still not see the disired hostel selector.
3333        self.assertFalse('desired hostel' in self.browser.contents)
3334        IWorkflowInfo(self.student).fireTransition('admit')
3335        # Students can now see the disired hostel selector.
3336        self.browser.reload()
3337        self.browser.open(self.acco_path)
3338        self.assertTrue('desired hostel' in self.browser.contents)
3339        self.browser.getControl(name="hostel").value = ['hall-2']
3340        self.browser.getControl("Save").click()
3341        self.assertTrue('selection has been saved' in self.browser.contents)
3342        self.assertTrue('<option selected="selected" value="hall-2">'
3343            in self.browser.contents)
3344        self.browser.getControl("Book accommodation").click()
3345        self.assertMatches('...Activation Code:...',
3346                           self.browser.contents)
3347        # Student can't use faked ACs ...
3348        self.browser.getControl(name="ac_series").value = u'nonsense'
3349        self.browser.getControl(name="ac_number").value = sfenumber
3350        self.browser.getControl("Create bed ticket").click()
3351        self.assertMatches('...Activation code is invalid...',
3352                           self.browser.contents)
3353        # ... or ACs owned by somebody else.
3354        ac.owner = u'Anybody'
3355        self.browser.getControl(name="ac_series").value = sfeseries
3356        self.browser.getControl(name="ac_number").value = sfenumber
3357        self.browser.getControl("Create bed ticket").click()
3358        # Hostel 2 has only a bed for women.
3359        self.assertTrue('There is no free bed in your desired hostel'
3360            in self.browser.contents)
3361        self.browser.getControl(name="hostel").value = ['hall-1']
3362        self.browser.getControl("Save").click()
3363        self.browser.getControl("Book accommodation").click()
3364        # Student can't use faked ACs ...
3365        self.browser.getControl(name="ac_series").value = sfeseries
3366        self.browser.getControl(name="ac_number").value = sfenumber
3367        self.browser.getControl("Create bed ticket").click()
3368        self.assertMatches('...You are not the owner of this access code...',
3369                           self.browser.contents)
3370        # The bed remains empty.
3371        bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
3372        self.assertTrue(bed.owner == NOT_OCCUPIED)
3373        ac.owner = self.student_id
3374        self.browser.getControl(name="ac_series").value = sfeseries
3375        self.browser.getControl(name="ac_number").value = sfenumber
3376        self.browser.getControl("Create bed ticket").click()
3377        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
3378                           self.browser.contents)
3379        # Bed has been allocated.
3380        self.assertTrue(bed.owner == self.student_id)
3381        # BedTicketAddPage is now blocked.
3382        self.browser.getControl("Book accommodation").click()
3383        self.assertMatches('...You already booked a bed space...',
3384            self.browser.contents)
3385        # The bed ticket displays the data correctly.
3386        self.browser.open(self.acco_path + '/2004')
3387        self.assertMatches('...Hall 1, Block A, Room 101, Bed A...',
3388                           self.browser.contents)
3389        self.assertMatches('...2004/2005...', self.browser.contents)
3390        self.assertMatches('...regular_male_fr...', self.browser.contents)
3391        self.assertMatches('...%s...' % pin, self.browser.contents)
3392        # Students can open the pdf slip.
3393        self.browser.open(self.browser.url + '/bed_allocation_slip.pdf')
3394        self.assertEqual(self.browser.headers['Status'], '200 Ok')
3395        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
3396        path = os.path.join(samples_dir(), 'bed_allocation_slip.pdf')
3397        open(path, 'wb').write(self.browser.contents)
3398        print "Sample PDF bed_allocation_slip.pdf written to %s" % path
3399        # Students can't relocate themselves.
3400        self.assertFalse('Relocate' in self.browser.contents)
3401        relocate_path = self.acco_path + '/2004/relocate'
3402        self.assertRaises(
3403            Unauthorized, self.browser.open, relocate_path)
3404        # Students can't see the Remove button and check boxes.
3405        self.browser.open(self.acco_path)
3406        self.assertFalse('Remove' in self.browser.contents)
3407        self.assertFalse('val_id' in self.browser.contents)
3408        # Students can pay maintenance fee now.
3409        self.browser.open(self.payments_path)
3410        self.browser.open(self.payments_path + '/addop')
3411        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
3412        self.browser.getControl("Create ticket").click()
3413        self.assertMatches('...Payment ticket created...',
3414                           self.browser.contents)
3415        ctrl = self.browser.getControl(name='val_id')
3416        value = ctrl.options[0]
3417        # Maintennace fee is taken from the hostel object.
3418        self.assertEqual(self.student['payments'][value].amount_auth, 876.0)
3419        # If the hostel's maintenance fee isn't set, the fee is
3420        # taken from the session configuration object.
3421        self.app['hostels']['hall-1'].maint_fee = 0.0
3422        self.browser.open(self.payments_path + '/addop')
3423        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
3424        self.browser.getControl("Create ticket").click()
3425        ctrl = self.browser.getControl(name='val_id')
3426        value = ctrl.options[1]
3427        self.assertEqual(self.student['payments'][value].amount_auth, 987.0)
3428        # The bedticket is aware of successfull maintenance fee payment
3429        bedticket = self.student['accommodation']['2004']
3430        self.assertFalse(bedticket.maint_payment_made)
3431        self.student['payments'][value].approve()
3432        self.assertTrue(bedticket.maint_payment_made)
3433        return
3434
3435    def test_change_password_request(self):
3436        self.browser.open('http://localhost/app/changepw')
3437        self.browser.getControl(name="form.identifier").value = '123'
3438        self.browser.getControl(name="form.email").value = 'aa@aa.ng'
3439        self.browser.getControl("Send login credentials").click()
3440        self.assertTrue('An email with' in self.browser.contents)
3441
3442    def test_student_expired_personal_data(self):
3443        # Login
3444        IWorkflowState(self.student).setState('school fee paid')
3445        delta = timedelta(days=180)
3446        self.student.personal_updated = datetime.utcnow() - delta
3447        self.browser.open(self.login_path)
3448        self.browser.getControl(name="form.login").value = self.student_id
3449        self.browser.getControl(name="form.password").value = 'spwd'
3450        self.browser.getControl("Login").click()
3451        self.assertEqual(self.browser.url, self.student_path)
3452        self.assertTrue(
3453            'You logged in' in self.browser.contents)
3454        # Students don't see personal_updated field in edit form
3455        self.browser.open(self.edit_personal_path)
3456        self.assertFalse('Updated' in self.browser.contents)
3457        self.browser.open(self.personal_path)
3458        self.assertTrue('Updated' in self.browser.contents)
3459        self.browser.getLink("Logout").click()
3460        delta = timedelta(days=181)
3461        self.student.personal_updated = datetime.utcnow() - delta
3462        self.browser.open(self.login_path)
3463        self.browser.getControl(name="form.login").value = self.student_id
3464        self.browser.getControl(name="form.password").value = 'spwd'
3465        self.browser.getControl("Login").click()
3466        self.assertEqual(self.browser.url, self.edit_personal_path)
3467        self.assertTrue(
3468            'Your personal data record is outdated.' in self.browser.contents)
3469
3470    def test_request_transcript(self):
3471        IWorkflowState(self.student).setState('graduated')
3472        self.browser.open(self.login_path)
3473        self.browser.getControl(name="form.login").value = self.student_id
3474        self.browser.getControl(name="form.password").value = 'spwd'
3475        self.browser.getControl("Login").click()
3476        self.assertMatches(
3477            '...You logged in...', self.browser.contents)
3478        # Create payment ticket
3479        self.browser.open(self.payments_path)
3480        self.browser.open(self.payments_path + '/addop')
3481        self.browser.getControl(name="form.p_category").value = ['transcript']
3482        self.browser.getControl("Create ticket").click()
3483        ctrl = self.browser.getControl(name='val_id')
3484        value = ctrl.options[0]
3485        self.browser.getLink(value).click()
3486        self.assertMatches('...Amount Authorized...',
3487                           self.browser.contents)
3488        self.assertEqual(self.student['payments'][value].amount_auth, 4567.0)
3489        # Student is the payer of the payment ticket.
3490        payer = IPayer(self.student['payments'][value])
3491        self.assertEqual(payer.display_fullname, 'Anna Tester')
3492        self.assertEqual(payer.id, self.student_id)
3493        self.assertEqual(payer.faculty, 'fac1')
3494        self.assertEqual(payer.department, 'dep1')
3495        # We simulate the approval and fetch the pin
3496        self.assertEqual(len(self.app['accesscodes']['TSC-0']),0)
3497        self.browser.open(self.browser.url + '/fake_approve')
3498        self.assertMatches('...Payment approved...',
3499                          self.browser.contents)
3500        pin = self.app['accesscodes']['TSC-0'].keys()[0]
3501        parts = pin.split('-')[1:]
3502        tscseries, tscnumber = parts
3503        # Student can use the pin to send the transcript request
3504        self.browser.open(self.student_path)
3505        self.browser.getLink("Request transcript").click()
3506        self.browser.getControl(name="ac_series").value = tscseries
3507        self.browser.getControl(name="ac_number").value = tscnumber
3508        self.browser.getControl(name="comment").value = 'Comment line 1 \nComment line2'
3509        self.browser.getControl(name="address").value = 'Address line 1 \nAddress line2'
3510        self.browser.getControl("Submit").click()
3511        self.assertMatches('...Transcript processing has been started...',
3512                          self.browser.contents)
3513        self.assertEqual(self.student.state, 'transcript requested')
3514        self.assertMatches(
3515            '... UTC K1000000 wrote:\n\nComment line 1 \n'
3516            'Comment line2\n\nDispatch Address:\nAddress line 1 \n'
3517            'Address line2\n\n', self.student['studycourse'].transcript_comment)
3518        # The comment has been logged
3519        logfile = os.path.join(
3520            self.app['datacenter'].storage, 'logs', 'students.log')
3521        logcontent = open(logfile).read()
3522        self.assertTrue(
3523            'K1000000 - students.browser.StudentTranscriptRequestPage - '
3524            'K1000000 - comment: Comment line 1 <br>Comment line2\n'
3525            in logcontent)
3526
3527    def test_late_registration(self):
3528        # Login
3529        delta = timedelta(days=10)
3530        self.app['configuration'][
3531            '2004'].coursereg_deadline = datetime.now(pytz.utc) - delta
3532        IWorkflowState(self.student).setState('school fee paid')
3533        self.browser.open(self.login_path)
3534        self.browser.getControl(name="form.login").value = self.student_id
3535        self.browser.getControl(name="form.password").value = 'spwd'
3536        self.browser.getControl("Login").click()
3537        self.browser.open(self.payments_path)
3538        self.browser.open(self.payments_path + '/addop')
3539        self.browser.getControl(name="form.p_category").value = ['late_registration']
3540        self.browser.getControl("Create ticket").click()
3541        self.assertMatches('...ticket created...',
3542                           self.browser.contents)
3543        self.browser.open(self.payments_path)
3544        ctrl = self.browser.getControl(name='val_id')
3545        value = ctrl.options[0]
3546        self.browser.getLink("Study Course").click()
3547        self.browser.getLink("Add course list").click()
3548        self.assertMatches('...Add current level 100 (Year 1)...',
3549                           self.browser.contents)
3550        self.browser.getControl("Create course list now").click()
3551        self.browser.getLink("100").click()
3552        self.browser.getLink("Edit course list").click()
3553        self.browser.getControl("Register course list").click()
3554        self.assertTrue('Course registration has ended. Please pay' in self.browser.contents)
3555        self.student['payments'][value].approve()
3556        self.browser.getControl("Register course list").click()
3557        self.assertTrue('Course list has been registered' in self.browser.contents)
3558        self.assertEqual(self.student.state, 'courses registered')
3559
3560
3561class StudentRequestPWTests(StudentsFullSetup):
3562    # Tests for student registration
3563
3564    layer = FunctionalLayer
3565
3566    def test_request_pw(self):
3567        # Student with wrong number can't be found.
3568        self.browser.open('http://localhost/app/requestpw')
3569        self.browser.getControl(name="form.lastname").value = 'Tester'
3570        self.browser.getControl(name="form.number").value = 'anynumber'
3571        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
3572        self.browser.getControl("Send login credentials").click()
3573        self.assertTrue('No student record found.'
3574            in self.browser.contents)
3575        # Anonymous is not informed that lastname verification failed.
3576        # It seems that the record doesn't exist.
3577        self.browser.open('http://localhost/app/requestpw')
3578        self.browser.getControl(name="form.lastname").value = 'Johnny'
3579        self.browser.getControl(name="form.number").value = '123'
3580        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
3581        self.browser.getControl("Send login credentials").click()
3582        self.assertTrue('No student record found.'
3583            in self.browser.contents)
3584        # Even with the correct lastname we can't register if a
3585        # password has been set and used.
3586        self.browser.getControl(name="form.lastname").value = 'Tester'
3587        self.browser.getControl(name="form.number").value = '123'
3588        self.browser.getControl("Send login credentials").click()
3589        self.assertTrue('Your password has already been set and used.'
3590            in self.browser.contents)
3591        self.browser.open('http://localhost/app/requestpw')
3592        self.app['students'][self.student_id].password = None
3593        # The lastname field, used for verification, is not case-sensitive.
3594        self.browser.getControl(name="form.lastname").value = 'tESTer'
3595        self.browser.getControl(name="form.number").value = '123'
3596        self.browser.getControl(name="form.email").value = 'new@yy.zz'
3597        self.browser.getControl("Send login credentials").click()
3598        # Yeah, we succeded ...
3599        self.assertTrue('Your password request was successful.'
3600            in self.browser.contents)
3601        # We can also use the matric_number instead.
3602        self.browser.open('http://localhost/app/requestpw')
3603        self.browser.getControl(name="form.lastname").value = 'tESTer'
3604        self.browser.getControl(name="form.number").value = '234'
3605        self.browser.getControl(name="form.email").value = 'new@yy.zz'
3606        self.browser.getControl("Send login credentials").click()
3607        self.assertTrue('Your password request was successful.'
3608            in self.browser.contents)
3609        # ... and  student can be found in the catalog via the email address
3610        cat = queryUtility(ICatalog, name='students_catalog')
3611        results = list(
3612            cat.searchResults(
3613            email=('new@yy.zz', 'new@yy.zz')))
3614        self.assertEqual(self.student,results[0])
3615        logfile = os.path.join(
3616            self.app['datacenter'].storage, 'logs', 'main.log')
3617        logcontent = open(logfile).read()
3618        self.assertTrue('zope.anybody - students.browser.StudentRequestPasswordPage - '
3619                        '234 (K1000000) - new@yy.zz' in logcontent)
3620        return
3621
3622    def test_student_locked_level_forms(self):
3623
3624        # Add two study levels, one current and one previous
3625        studylevel = createObject(u'waeup.StudentStudyLevel')
3626        studylevel.level = 100
3627        self.student['studycourse'].addStudentStudyLevel(
3628            self.certificate, studylevel)
3629        studylevel = createObject(u'waeup.StudentStudyLevel')
3630        studylevel.level = 200
3631        self.student['studycourse'].addStudentStudyLevel(
3632            self.certificate, studylevel)
3633        IWorkflowState(self.student).setState('school fee paid')
3634        self.student['studycourse'].current_level = 200
3635
3636        self.browser.open(self.login_path)
3637        self.browser.getControl(name="form.login").value = self.student_id
3638        self.browser.getControl(name="form.password").value = 'spwd'
3639        self.browser.getControl("Login").click()
3640
3641        self.browser.open(self.student_path + '/studycourse/200/edit')
3642        self.assertFalse('The requested form is locked' in self.browser.contents)
3643        self.browser.open(self.student_path + '/studycourse/100/edit')
3644        self.assertTrue('The requested form is locked' in self.browser.contents)
3645
3646        self.browser.open(self.student_path + '/studycourse/200/ctadd')
3647        self.assertFalse('The requested form is locked' in self.browser.contents)
3648        self.browser.open(self.student_path + '/studycourse/100/ctadd')
3649        self.assertTrue('The requested form is locked' in self.browser.contents)
3650
3651        IWorkflowState(self.student).setState('courses registered')
3652        self.browser.open(self.student_path + '/studycourse/200/edit')
3653        self.assertTrue('The requested form is locked' in self.browser.contents)
3654        self.browser.open(self.student_path + '/studycourse/200/ctadd')
3655        self.assertTrue('The requested form is locked' in self.browser.contents)
3656
3657
3658class PublicPagesTests(StudentsFullSetup):
3659    # Tests for simple webservices
3660
3661    layer = FunctionalLayer
3662
3663    def test_paymentrequest(self):
3664        payment = createObject('waeup.StudentOnlinePayment')
3665        payment.p_category = u'schoolfee'
3666        payment.p_session = self.student.current_session
3667        payment.p_item = u'My Certificate'
3668        payment.p_id = u'anyid'
3669        self.student['payments']['anykey'] = payment
3670        # Request information about unpaid payment ticket
3671        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
3672        self.assertEqual(self.browser.contents, '-1')
3673        # Request information about paid payment ticket
3674        payment.p_state = u'paid'
3675        notify(grok.ObjectModifiedEvent(payment))
3676        self.browser.open('http://localhost/app/paymentrequest?P_ID=anyid')
3677        self.assertEqual(self.browser.contents,
3678            'FULL_NAME=Anna Tester&FACULTY=fac1&DEPARTMENT=dep1'
3679            '&PAYMENT_ITEM=My Certificate&PAYMENT_CATEGORY=School Fee'
3680            '&ACADEMIC_SESSION=2004/2005&MATRIC_NUMBER=234&REG_NUMBER=123'
3681            '&FEE_AMOUNT=0.0')
3682        self.browser.open('http://localhost/app/paymentrequest?NONSENSE=nonsense')
3683        self.assertEqual(self.browser.contents, '-1')
3684        self.browser.open('http://localhost/app/paymentrequest?P_ID=nonsense')
3685        self.assertEqual(self.browser.contents, '-1')
3686
3687class StudentDataExportTests(StudentsFullSetup, FunctionalAsyncTestCase):
3688    # Tests for StudentsContainer class views and pages
3689
3690    layer = FunctionalLayer
3691
3692    def wait_for_export_job_completed(self):
3693        # helper function waiting until the current export job is completed
3694        manager = getUtility(IJobManager)
3695        job_id = self.app['datacenter'].running_exports[0][0]
3696        job = manager.get(job_id)
3697        wait_for_result(job)
3698        return job_id
3699
3700    def add_payment(self, student):
3701        # get a payment with all fields set
3702        payment = StudentOnlinePayment()
3703        payment.creation_date = datetime(2012, 12, 13)
3704        payment.p_id = 'my-id'
3705        payment.p_category = u'schoolfee'
3706        payment.p_state = 'paid'
3707        payment.ac = u'666'
3708        payment.p_item = u'p-item'
3709        payment.p_level = 100
3710        payment.p_session = curr_year - 6
3711        payment.payment_date = datetime(2012, 12, 13)
3712        payment.amount_auth = 12.12
3713        payment.r_amount_approved = 12.12
3714        payment.r_code = u'r-code'
3715        # XXX: there is no addPayment method to give predictable names
3716        self.payment = student['payments']['my-payment'] = payment
3717        return payment
3718
3719    def test_datacenter_export(self):
3720        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3721        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
3722        self.browser.getControl(name="exporter").value = ['bursary']
3723        self.browser.getControl(name="session").value = ['2004']
3724        self.browser.getControl(name="level").value = ['100']
3725        self.browser.getControl(name="mode").value = ['ug_ft']
3726        self.browser.getControl(name="payments_start").value = '13/12/2012'
3727        self.browser.getControl(name="payments_end").value = '14/12/2012'
3728        self.browser.getControl("Create CSV file").click()
3729
3730        # When the job is finished and we reload the page...
3731        job_id = self.wait_for_export_job_completed()
3732        # ... the csv file can be downloaded ...
3733        self.browser.open('http://localhost/app/datacenter/@@export')
3734        self.browser.getLink("Download").click()
3735        self.assertEqual(self.browser.headers['content-type'],
3736            'text/csv; charset=UTF-8')
3737        self.assertTrue(
3738            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
3739            self.browser.headers['content-disposition'])
3740        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3741        job_id = self.app['datacenter'].running_exports[0][0]
3742        # ... and discarded
3743        self.browser.open('http://localhost/app/datacenter/@@export')
3744        self.browser.getControl("Discard").click()
3745        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3746        # Creation, downloading and discarding is logged
3747        logfile = os.path.join(
3748            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3749        logcontent = open(logfile).read()
3750        self.assertTrue(
3751            'zope.mgr - students.browser.DatacenterExportJobContainerJobConfig '
3752            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
3753            '13/12/2012, 14/12/2012, all, all, all, all), job_id=%s'
3754            % job_id in logcontent
3755            )
3756        self.assertTrue(
3757            'zope.mgr - browser.pages.ExportCSVView '
3758            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
3759            % (job_id, job_id) in logcontent
3760            )
3761        self.assertTrue(
3762            'zope.mgr - browser.pages.ExportCSVPage '
3763            '- discarded: job_id=%s' % job_id in logcontent
3764            )
3765
3766    def test_datacenter_export_selected(self):
3767        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3768        self.browser.open('http://localhost/app/datacenter/@@exportselected')
3769        self.browser.getControl(name="exporter").value = ['students']
3770        self.browser.getControl(name="students").value = 'K1000000'
3771        self.browser.getControl("Create CSV file").click()
3772        # When the job is finished and we reload the page...
3773        job_id = self.wait_for_export_job_completed()
3774        # ... the csv file can be downloaded ...
3775        self.browser.open('http://localhost/app/datacenter/@@export')
3776        self.browser.getLink("Download").click()
3777        self.assertEqual(self.browser.headers['content-type'],
3778            'text/csv; charset=UTF-8')
3779        self.assertTrue(
3780            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3781            self.browser.headers['content-disposition'])
3782        self.assertTrue(
3783            'adm_code,clr_code,date_of_birth,email,employer,'
3784            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
3785            'officer_comment,perm_address,personal_updated,phone,reg_number,'
3786            'sex,student_id,suspended,suspended_comment,'
3787            'password,state,history,certcode,is_postgrad,'
3788            'current_level,current_session\r\n'
3789            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,'
3790            '1234#,123,m,K1000000,0,,{SSHA}' in self.browser.contents)
3791        self.browser.open('http://localhost/app/datacenter/@@export')
3792        self.browser.getControl("Discard").click()
3793
3794    def test_payment_dates(self):
3795        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3796        self.browser.open('http://localhost/app/datacenter/@@exportconfig')
3797        self.browser.getControl(name="exporter").value = ['bursary']
3798        self.browser.getControl(name="session").value = ['2004']
3799        self.browser.getControl(name="level").value = ['100']
3800        self.browser.getControl(name="mode").value = ['ug_ft']
3801        self.browser.getControl(name="payments_start").value = '13/12/2012'
3802        # If one payment date is missing, an error message appears
3803        self.browser.getControl(name="payments_end").value = ''
3804        self.browser.getControl("Create CSV file").click()
3805        self.assertTrue('Payment dates do not match format d/m/Y'
3806            in self.browser.contents)
3807
3808    def test_faculties_export(self):
3809        self.add_payment(self.student)
3810        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3811        facs_path = 'http://localhost/app/faculties'
3812        self.browser.open(facs_path)
3813        self.browser.getLink("Export student data").click()
3814        self.browser.getControl("Set export parameters").click()
3815        self.browser.getControl(name="exporter").value = ['bursary']
3816        self.browser.getControl(name="session").value = ['2004']
3817        self.browser.getControl(name="level").value = ['100']
3818        self.browser.getControl(name="mode").value = ['ug_ft']
3819        self.browser.getControl(name="payments_start").value = '13/12/2012'
3820        self.browser.getControl(name="payments_end").value = '14/12/2012'
3821        self.browser.getControl(name="paycat").value = ['schoolfee']
3822        self.browser.getControl("Create CSV file").click()
3823
3824        # When the job is finished and we reload the page...
3825        job_id = self.wait_for_export_job_completed()
3826        self.browser.open(facs_path + '/exports')
3827        # ... the csv file can be downloaded ...
3828        self.browser.getLink("Download").click()
3829        self.assertEqual(self.browser.headers['content-type'],
3830            'text/csv; charset=UTF-8')
3831        self.assertTrue(
3832            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
3833            self.browser.headers['content-disposition'])
3834        self.assertTrue(
3835            '666,12.12,2012-12-13 00:00:00#,schoolfee,1,my-id,p-item,100,2013,'
3836            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
3837            'Tester,created,2004,2004,,fac1,dep1,CERT1' in self.browser.contents)
3838        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3839        job_id = self.app['datacenter'].running_exports[0][0]
3840        # ... and discarded
3841        self.browser.open(facs_path + '/exports')
3842        self.browser.getControl("Discard").click()
3843        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3844        # Creation, downloading and discarding is logged
3845        logfile = os.path.join(
3846            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3847        logcontent = open(logfile).read()
3848        self.assertTrue(
3849            'zope.mgr - students.browser.FacultiesExportJobContainerJobConfig '
3850            '- exported: bursary (2004, 100, ug_ft, None, None, None, '
3851            '13/12/2012, 14/12/2012, all, all, schoolfee, all), job_id=%s'
3852            % job_id in logcontent
3853            )
3854        self.assertTrue(
3855            'zope.mgr - students.browser.ExportJobContainerDownload '
3856            '- downloaded: WAeUP.Kofa_bursary_%s.csv, job_id=%s'
3857            % (job_id, job_id) in logcontent
3858            )
3859        self.assertTrue(
3860            'zope.mgr - students.browser.ExportJobContainerOverview '
3861            '- discarded: job_id=%s' % job_id in logcontent
3862            )
3863        # Officer can also enter student id and gets the same export file
3864        self.browser.open(facs_path)
3865        self.browser.getLink("Export student data").click()
3866        self.browser.getControl("Enter student ids or matric numbers").click()
3867        self.browser.getControl(name="exporter").value = ['bursary']
3868        self.browser.getControl(name="students").value = 'K1000000'
3869        self.browser.getControl("Create CSV file").click()
3870        # When the job is finished and we reload the page...
3871        job_id = self.wait_for_export_job_completed()
3872        # ... the csv file can be downloaded ...
3873        self.browser.open('http://localhost/app/faculties/exports')
3874        self.browser.getLink("Download").click()
3875        self.assertEqual(self.browser.headers['content-type'],
3876            'text/csv; charset=UTF-8')
3877        self.assertTrue(
3878            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
3879            self.browser.headers['content-disposition'])
3880        self.assertTrue(
3881            '666,12.12,2012-12-13 00:00:00#,schoolfee,1,my-id,p-item,100,2013,'
3882            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
3883            'Tester,created,2004,2004,,fac1,dep1,CERT1' in self.browser.contents)
3884
3885    def test_faculty_export(self):
3886        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3887        fac1_path = 'http://localhost/app/faculties/fac1'
3888        self.browser.open(fac1_path)
3889        self.browser.getLink("Export student data").click()
3890        self.browser.getControl("Set export parameters").click()
3891        self.browser.getControl(name="exporter").value = ['students']
3892        self.browser.getControl(name="session").value = ['2004']
3893        self.browser.getControl(name="level").value = ['100']
3894        self.browser.getControl(name="mode").value = ['ug_ft']
3895        # The testbrowser does not hide the payment period fields, but
3896        # values are ignored when using the students exporter.
3897        self.browser.getControl(name="payments_start").value = '13/12/2012'
3898        self.browser.getControl(name="payments_end").value = '14/12/2012'
3899        self.browser.getControl("Create CSV file").click()
3900        # When the job is finished and we reload the page...
3901        job_id = self.wait_for_export_job_completed()
3902        self.browser.open(fac1_path + '/exports')
3903        # ... the csv file can be downloaded ...
3904        self.browser.getLink("Download").click()
3905        self.assertEqual(self.browser.headers['content-type'],
3906            'text/csv; charset=UTF-8')
3907        self.assertTrue(
3908            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3909            self.browser.headers['content-disposition'])
3910        self.assertTrue(
3911            'adm_code,clr_code,date_of_birth,email,employer,'
3912            'firstname,flash_notice,lastname,matric_number,middlename,nationality,'
3913            'officer_comment,perm_address,personal_updated,phone,reg_number,'
3914            'sex,student_id,suspended,suspended_comment,'
3915            'password,state,history,certcode,is_postgrad,'
3916            'current_level,current_session\r\n'
3917            ',,1981-02-04#,aa@aa.ng,,Anna,,Tester,234,,,,,,'
3918            '1234#,123,m,K1000000,0,,{SSHA}' in self.browser.contents)
3919        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3920        job_id = self.app['datacenter'].running_exports[0][0]
3921        # ... and discarded
3922        self.browser.open(fac1_path + '/exports')
3923        self.browser.getControl("Discard").click()
3924        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3925        # Creation, downloading and discarding is logged
3926        logfile = os.path.join(
3927            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3928        logcontent = open(logfile).read()
3929        self.assertTrue(
3930            'zope.mgr - students.browser.FacultyExportJobContainerJobConfig '
3931            '- exported: students (2004, 100, ug_ft, fac1, None, None, '
3932            '13/12/2012, 14/12/2012, all, all, all, all), job_id=%s'
3933            % job_id in logcontent
3934            )
3935        self.assertTrue(
3936            'zope.mgr - students.browser.ExportJobContainerDownload '
3937            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3938            % (job_id, job_id) in logcontent
3939            )
3940        self.assertTrue(
3941            'zope.mgr - students.browser.ExportJobContainerOverview '
3942            '- discarded: job_id=%s' % job_id in logcontent
3943            )
3944        # Officer can set export parameters but cannot enter student id
3945        # at faculty level
3946        self.browser.open(fac1_path + '/exports')
3947        self.assertTrue("Set export parameters"
3948                         in self.browser.contents)
3949        self.assertFalse("Enter student ids or matric numbers"
3950                         in self.browser.contents)
3951
3952    def test_department_export(self):
3953        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
3954        dep1_path = 'http://localhost/app/faculties/fac1/dep1'
3955        self.browser.open(dep1_path)
3956        self.browser.getLink("Export student data").click()
3957        self.browser.getControl("Set export parameters").click()
3958        self.browser.getControl(name="exporter").value = ['students']
3959        self.browser.getControl(name="session").value = ['2004']
3960        self.browser.getControl(name="level").value = ['100']
3961        self.browser.getControl(name="mode").value = ['ug_ft']
3962        # The testbrowser does not hide the payment period fields, but
3963        # values are ignored when using the students exporter.
3964        self.browser.getControl(name="payments_start").value = '13/12/2012'
3965        self.browser.getControl(name="payments_end").value = '14/12/2012'
3966        self.browser.getControl("Create CSV file").click()
3967
3968        # When the job is finished and we reload the page...
3969        job_id = self.wait_for_export_job_completed()
3970        self.browser.open(dep1_path + '/exports')
3971        # ... the csv file can be downloaded ...
3972        self.browser.getLink("Download").click()
3973        self.assertEqual(self.browser.headers['content-type'],
3974            'text/csv; charset=UTF-8')
3975        self.assertTrue(
3976            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
3977            self.browser.headers['content-disposition'])
3978        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
3979        job_id = self.app['datacenter'].running_exports[0][0]
3980        # ... and discarded
3981        self.browser.open(dep1_path + '/exports')
3982        self.browser.getControl("Discard").click()
3983        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
3984        # Creation, downloading and discarding is logged
3985        logfile = os.path.join(
3986            self.app['datacenter'].storage, 'logs', 'datacenter.log')
3987        logcontent = open(logfile).read()
3988        self.assertTrue(
3989            'zope.mgr - students.browser.DepartmentExportJobContainerJobConfig '
3990            '- exported: students (2004, 100, ug_ft, None, dep1, None, '
3991            '13/12/2012, 14/12/2012, all, all, all, all), job_id=%s'
3992            % job_id in logcontent
3993            )
3994        self.assertTrue(
3995            'zope.mgr - students.browser.ExportJobContainerDownload '
3996            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
3997            % (job_id, job_id) in logcontent
3998            )
3999        self.assertTrue(
4000            'zope.mgr - students.browser.ExportJobContainerOverview '
4001            '- discarded: job_id=%s' % job_id in logcontent
4002            )
4003
4004    def test_certificate_export(self):
4005        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
4006        cert1_path = 'http://localhost/app/faculties/fac1/dep1/certificates/CERT1'
4007        self.browser.open(cert1_path)
4008        self.browser.getLink("Export student data").click()
4009        self.browser.getControl("Set export parameters").click()
4010        self.browser.getControl(name="exporter").value = ['students']
4011        self.browser.getControl(name="session").value = ['2004']
4012        self.browser.getControl(name="level").value = ['100']
4013        self.browser.getControl("Create CSV file").click()
4014
4015        # When the job is finished and we reload the page...
4016        job_id = self.wait_for_export_job_completed()
4017        self.browser.open(cert1_path + '/exports')
4018        # ... the csv file can be downloaded ...
4019        self.browser.getLink("Download").click()
4020        self.assertEqual(self.browser.headers['content-type'],
4021            'text/csv; charset=UTF-8')
4022        self.assertTrue(
4023            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
4024            self.browser.headers['content-disposition'])
4025        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4026        job_id = self.app['datacenter'].running_exports[0][0]
4027        # ... and discarded
4028        self.browser.open(cert1_path + '/exports')
4029        self.browser.getControl("Discard").click()
4030        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4031        # Creation, downloading and discarding is logged
4032        logfile = os.path.join(
4033            self.app['datacenter'].storage, 'logs', 'datacenter.log')
4034        logcontent = open(logfile).read()
4035        self.assertTrue(
4036            'zope.mgr - students.browser.CertificateExportJobContainerJobConfig '
4037            '- exported: students '
4038            '(2004, 100, None, None, None, CERT1, , , None, None, None, None), '
4039            'job_id=%s'
4040            % job_id in logcontent
4041            )
4042        self.assertTrue(
4043            'zope.mgr - students.browser.ExportJobContainerDownload '
4044            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
4045            % (job_id, job_id) in logcontent
4046            )
4047        self.assertTrue(
4048            'zope.mgr - students.browser.ExportJobContainerOverview '
4049            '- discarded: job_id=%s' % job_id in logcontent
4050            )
4051
4052    def deprecated_test_course_export_students(self):
4053        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
4054        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
4055        self.browser.open(course1_path)
4056        self.browser.getLink("Export student data").click()
4057        self.browser.getControl("Set export parameters").click()
4058        self.browser.getControl(name="exporter").value = ['students']
4059        self.browser.getControl(name="session").value = ['2004']
4060        self.browser.getControl(name="level").value = ['100']
4061        self.browser.getControl("Create CSV file").click()
4062
4063        # When the job is finished and we reload the page...
4064        job_id = self.wait_for_export_job_completed()
4065        self.browser.open(course1_path + '/exports')
4066        # ... the csv file can be downloaded ...
4067        self.browser.getLink("Download").click()
4068        self.assertEqual(self.browser.headers['content-type'],
4069            'text/csv; charset=UTF-8')
4070        self.assertTrue(
4071            'filename="WAeUP.Kofa_students_%s.csv' % job_id in
4072            self.browser.headers['content-disposition'])
4073        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4074        job_id = self.app['datacenter'].running_exports[0][0]
4075        # ... and discarded
4076        self.browser.open(course1_path + '/exports')
4077        self.browser.getControl("Discard").click()
4078        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4079        # Creation, downloading and discarding is logged
4080        logfile = os.path.join(
4081            self.app['datacenter'].storage, 'logs', 'datacenter.log')
4082        logcontent = open(logfile).read()
4083        self.assertTrue(
4084            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
4085            '- exported: students (2004, 100, COURSE1), job_id=%s'
4086            % job_id in logcontent
4087            )
4088        self.assertTrue(
4089            'zope.mgr - students.browser.ExportJobContainerDownload '
4090            '- downloaded: WAeUP.Kofa_students_%s.csv, job_id=%s'
4091            % (job_id, job_id) in logcontent
4092            )
4093        self.assertTrue(
4094            'zope.mgr - students.browser.ExportJobContainerOverview '
4095            '- discarded: job_id=%s' % job_id in logcontent
4096            )
4097
4098    def test_course_export_lecturer(self):
4099        # We add study level 100 to the student's studycourse
4100        studylevel = StudentStudyLevel()
4101        studylevel.level = 100
4102        studylevel.level_session = 2004
4103        IWorkflowState(self.student).setState('courses validated')
4104        self.student['studycourse'].addStudentStudyLevel(
4105            self.certificate,studylevel)
4106        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
4107        course1_path = 'http://localhost/app/faculties/fac1/dep1/courses/COURSE1'
4108        self.browser.open(course1_path)
4109        self.browser.getLink("Export student data").click()
4110        self.browser.getControl("Set export parameters").click()
4111        self.assertTrue(
4112            'Academic session not set. Please contact the administrator.'
4113            in self.browser.contents)
4114        self.app['configuration'].current_academic_session = 2004
4115        self.browser.getControl("Set export parameters").click()
4116        self.browser.getControl(name="exporter").value = ['lecturer']
4117        self.browser.getControl(name="session").value = ['2004']
4118        self.browser.getControl(name="level").value = ['100']
4119        self.browser.getControl("Create CSV file").click()
4120        # When the job is finished and we reload the page...
4121        job_id = self.wait_for_export_job_completed()
4122        self.browser.open(course1_path + '/exports')
4123        # ... the csv file can be downloaded ...
4124        self.browser.getLink("Download").click()
4125        self.assertEqual(self.browser.headers['content-type'],
4126            'text/csv; charset=UTF-8')
4127        self.assertTrue(
4128            'filename="WAeUP.Kofa_lecturer_%s.csv' % job_id in
4129            self.browser.headers['content-disposition'])
4130        # ... and contains the course ticket COURSE1
4131        self.assertEqual(self.browser.contents,
4132            'matric_number,student_id,display_fullname,level,code,'
4133            'level_session,score\r\n234,K1000000,Anna Tester,'
4134            '100,COURSE1,2004,\r\n')
4135        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4136        job_id = self.app['datacenter'].running_exports[0][0]
4137        # Thew job can be discarded
4138        self.browser.open(course1_path + '/exports')
4139        self.browser.getControl("Discard").click()
4140        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4141        # Creation, downloading and discarding is logged
4142        logfile = os.path.join(
4143            self.app['datacenter'].storage, 'logs', 'datacenter.log')
4144        logcontent = open(logfile).read()
4145        self.assertTrue(
4146            'zope.mgr - students.browser.CourseExportJobContainerJobConfig '
4147            '- exported: lecturer (2004, 100, COURSE1), job_id=%s'
4148            % job_id in logcontent
4149            )
4150        self.assertTrue(
4151            'zope.mgr - students.browser.ExportJobContainerDownload '
4152            '- downloaded: WAeUP.Kofa_lecturer_%s.csv, job_id=%s'
4153            % (job_id, job_id) in logcontent
4154            )
4155        self.assertTrue(
4156            'zope.mgr - students.browser.ExportJobContainerOverview '
4157            '- discarded: job_id=%s' % job_id in logcontent
4158            )
4159
4160    def test_export_departmet_officers(self):
4161        # Create department officer
4162        self.app['users'].addUser('mrdepartment', SECRET)
4163        self.app['users']['mrdepartment'].email = 'mrdepartment@foo.ng'
4164        self.app['users']['mrdepartment'].title = 'Carlo Pitter'
4165        # Assign local role
4166        department = self.app['faculties']['fac1']['dep1']
4167        prmlocal = IPrincipalRoleManager(department)
4168        prmlocal.assignRoleToPrincipal('waeup.local.DepartmentOfficer', 'mrdepartment')
4169        # Login as department officer
4170        self.browser.open(self.login_path)
4171        self.browser.getControl(name="form.login").value = 'mrdepartment'
4172        self.browser.getControl(name="form.password").value = SECRET
4173        self.browser.getControl("Login").click()
4174        self.assertMatches('...You logged in...', self.browser.contents)
4175        self.browser.open("http://localhost/app/faculties/fac1/dep1")
4176        self.browser.getLink("Export student data").click()
4177        self.browser.getControl("Set export parameters").click()
4178        # Only the sfpaymentsoverview exporter is available for department officers
4179        self.assertFalse('<option value="students">' in self.browser.contents)
4180        self.assertTrue(
4181            '<option value="sfpaymentsoverview">' in self.browser.contents)
4182        self.browser.getControl(name="exporter").value = ['sfpaymentsoverview']
4183        self.browser.getControl(name="session").value = ['2004']
4184        self.browser.getControl(name="level").value = ['100']
4185        self.browser.getControl("Create CSV file").click()
4186        self.assertTrue('Export started' in self.browser.contents)
4187        # Thew job can be discarded
4188        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4189        self.wait_for_export_job_completed()
4190        self.browser.open("http://localhost/app/faculties/fac1/dep1/exports")
4191        self.browser.getControl("Discard").click()
4192        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4193
4194    def test_export_bursary_officers(self):
4195        self.add_payment(self.student)
4196        # Create bursary officer
4197        self.app['users'].addUser('mrbursary', SECRET)
4198        self.app['users']['mrbursary'].email = 'mrbursary@foo.ng'
4199        self.app['users']['mrbursary'].title = 'Carlo Pitter'
4200        prmglobal = IPrincipalRoleManager(self.app)
4201        prmglobal.assignRoleToPrincipal('waeup.BursaryOfficer', 'mrbursary')
4202        # Login as bursary officer
4203        self.browser.open(self.login_path)
4204        self.browser.getControl(name="form.login").value = 'mrbursary'
4205        self.browser.getControl(name="form.password").value = SECRET
4206        self.browser.getControl("Login").click()
4207        self.assertMatches('...You logged in...', self.browser.contents)
4208        self.browser.getLink("Academics").click()
4209        self.browser.getLink("Export student data").click()
4210        self.browser.getControl("Set export parameters").click()
4211        # Only the bursary exporter is available for bursary officers
4212        # not only at facultiescontainer level ...
4213        self.assertFalse('<option value="students">' in self.browser.contents)
4214        self.assertTrue('<option value="bursary">' in self.browser.contents)
4215        self.browser.getControl(name="exporter").value = ['bursary']
4216        self.browser.getControl(name="session").value = ['2004']
4217        self.browser.getControl(name="level").value = ['100']
4218        self.browser.getControl("Create CSV file").click()
4219        self.assertTrue('Export started' in self.browser.contents)
4220        # ... but also at other levels
4221        self.browser.open('http://localhost/app/faculties/fac1/dep1')
4222        self.browser.getLink("Export student data").click()
4223        self.browser.getControl("Set export parameters").click()
4224        self.assertFalse('<option value="students">' in self.browser.contents)
4225        self.assertTrue('<option value="bursary">' in self.browser.contents)
4226        # Thew job can be downloaded
4227        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4228        job_id = self.wait_for_export_job_completed()
4229        self.browser.open('http://localhost/app/faculties/exports')
4230        self.browser.getLink("Download").click()
4231        self.assertEqual(self.browser.headers['content-type'],
4232            'text/csv; charset=UTF-8')
4233        self.assertTrue(
4234            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
4235            self.browser.headers['content-disposition'])
4236        self.assertTrue(
4237            '666,12.12,2012-12-13 00:00:00#,schoolfee,1,my-id,p-item,100,2013,'
4238            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
4239            'Tester,created,2004,2004,,fac1,dep1,CERT1' in self.browser.contents)
4240        # ... and discarded
4241        self.browser.open('http://localhost/app/faculties/exports')
4242        self.browser.getControl("Discard").click()
4243        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4244        # At Academics level bursary officers can also enter student ids
4245        self.browser.getLink("Academics").click()
4246        self.browser.getLink("Export student data").click()
4247        self.browser.getControl("Enter student ids or matric numbers").click()
4248        self.assertFalse('<option value="students">' in self.browser.contents)
4249        self.browser.getControl(name="exporter").value = ['bursary']
4250        self.browser.getControl(name="students").value = 'K1000000'
4251        self.browser.getControl("Create CSV file").click()
4252        # When the job is finished and we reload the page...
4253        job_id = self.wait_for_export_job_completed()
4254        # ... the csv file can be downloaded ...
4255        self.browser.open('http://localhost/app/faculties/exports')
4256        self.browser.getLink("Download").click()
4257        self.assertEqual(self.browser.headers['content-type'],
4258            'text/csv; charset=UTF-8')
4259        self.assertTrue(
4260            'filename="WAeUP.Kofa_bursary_%s.csv' % job_id in
4261            self.browser.headers['content-disposition'])
4262        self.assertTrue(
4263            '666,12.12,2012-12-13 00:00:00#,schoolfee,1,my-id,p-item,100,2013,'
4264            'paid,2012-12-13 00:00:00#,12.12,r-code,,K1000000,234,123,Anna,,'
4265            'Tester,created,2004,2004,,fac1,dep1,CERT1' in self.browser.contents)
4266
4267    def test_export_accommodation_officers(self):
4268        # Create bursary officer
4269        self.app['users'].addUser('mracco', SECRET)
4270        self.app['users']['mracco'].email = 'mracco@foo.ng'
4271        self.app['users']['mracco'].title = 'Carlo Pitter'
4272        prmglobal = IPrincipalRoleManager(self.app)
4273        prmglobal.assignRoleToPrincipal('waeup.AccommodationOfficer', 'mracco')
4274        # Login as bursary officer
4275        self.browser.open(self.login_path)
4276        self.browser.getControl(name="form.login").value = 'mracco'
4277        self.browser.getControl(name="form.password").value = SECRET
4278        self.browser.getControl("Login").click()
4279        self.assertMatches('...You logged in...', self.browser.contents)
4280        self.browser.getLink("Academics").click()
4281        self.browser.getLink("Export student data").click()
4282        self.browser.getControl("Set export parameters").click()
4283        # accommodationpayments and beds exporters are available
4284        # not only at facultiescontainer level ...
4285        self.assertFalse('<option value="students">' in self.browser.contents)
4286        self.assertTrue('<option value="accommodationpayments">'
4287            in self.browser.contents)
4288        self.assertTrue('<option value="bedtickets">' in self.browser.contents)
4289        self.browser.getControl(
4290            name="exporter").value = ['accommodationpayments']
4291        self.browser.getControl(name="session").value = ['2004']
4292        self.browser.getControl(name="level").value = ['100']
4293        self.browser.getControl("Create CSV file").click()
4294        self.assertTrue('Export started' in self.browser.contents)
4295        # ... but also at other levels
4296        self.browser.open('http://localhost/app/faculties/fac1/dep1')
4297        self.browser.getLink("Export student data").click()
4298        self.browser.getControl("Set export parameters").click()
4299        self.assertFalse('<option value="students">' in self.browser.contents)
4300        self.assertTrue('<option value="accommodationpayments">'
4301            in self.browser.contents)
4302        self.assertTrue('<option value="bedtickets">' in self.browser.contents)
4303        # Thew job can be discarded
4304        self.assertEqual(len(self.app['datacenter'].running_exports), 1)
4305        self.wait_for_export_job_completed()
4306        self.browser.open('http://localhost/app/faculties/exports')
4307        self.browser.getControl("Discard").click()
4308        self.assertEqual(len(self.app['datacenter'].running_exports), 0)
4309
4310
4311UPLOAD_CSV_TEMPLATE = (
4312    'matric_number,student_id,display_fullname,level,code,level_session,'
4313    'score\r\n'
4314    '234,K1000000,Anna Tester,100,COURSE1,2004,%s\r\n')
4315
4316class LecturerUITests(StudentsFullSetup):
4317    # Tests for UI actions when acting as lecturer.
4318
4319    def login_as_lecturer(self):
4320        self.app['users'].addUser('mrslecturer', SECRET)
4321        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
4322        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
4323        # Add course ticket
4324        studylevel = createObject(u'waeup.StudentStudyLevel')
4325        studylevel.level = 100
4326        studylevel.level_session = 2004
4327        self.student['studycourse'].addStudentStudyLevel(
4328            self.certificate, studylevel)
4329        # Assign local Lecturer role for a course.
4330        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
4331        prmlocal = IPrincipalRoleManager(course)
4332        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
4333        notify(LocalRoleSetEvent(
4334            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
4335        # Login as lecturer.
4336        self.browser.open(self.login_path)
4337        self.browser.getControl(name="form.login").value = 'mrslecturer'
4338        self.browser.getControl(
4339            name="form.password").value = SECRET
4340        self.browser.getControl("Login").click()
4341        # Store reused urls/paths
4342        self.course_url = (
4343            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1')
4344        self.edit_scores_url = '%s/edit_scores' % self.course_url
4345        # Set standard parameters
4346        self.app['configuration'].current_academic_session = 2004
4347        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
4348        IWorkflowState(self.student).setState(VALIDATED)
4349
4350    @property
4351    def stud_log_path(self):
4352        return os.path.join(
4353            self.app['datacenter'].storage, 'logs', 'students.log')
4354
4355    def test_lecturer_lands_on_landing_page(self):
4356        # lecturers can login and will be led to landing page.
4357        self.login_as_lecturer()
4358        self.assertMatches('...You logged in...', self.browser.contents)
4359        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
4360        self.assertTrue(
4361            "<span>Unnamed Course (COURSE1)</span>"
4362            in self.browser.contents)
4363
4364    def test_lecturer_department_role(self):
4365        # lecturers can login and will be led to landing page also if
4366        # role is assigned at department level.
4367        self.login_as_lecturer()
4368        # we remove the role granted above
4369        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
4370        prmlocal = IPrincipalRoleManager(course)
4371        prmlocal.removeRoleFromPrincipal('waeup.local.Lecturer', 'mrslecturer')
4372        notify(LocalRoleSetEvent(
4373            course, 'waeup.local.Lecturer', 'mrslecturer', granted=False))
4374        self.browser.open(URL_LECTURER_LANDING)
4375        # no course appears
4376        self.assertFalse(
4377            "<span>Unnamed Course (COURSE1)</span>"
4378            in self.browser.contents)
4379        # we assign lecturer at department level
4380        dep = self.app['faculties']['fac1']['dep1']
4381        prmlocal = IPrincipalRoleManager(dep)
4382        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
4383        notify(LocalRoleSetEvent(
4384            dep, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
4385        self.browser.open(URL_LECTURER_LANDING)
4386        # course appears again
4387        self.assertTrue(
4388            "<span>Unnamed Course (COURSE1)</span>"
4389            in self.browser.contents)
4390
4391    def test_my_roles_link_works(self):
4392        # lecturers can see their roles
4393        self.login_as_lecturer()
4394        self.browser.getLink("My Roles").click()
4395        self.assertTrue(
4396            "<div>Academics Officer (view only)</div>"
4397            in self.browser.contents)
4398        self.assertTrue(
4399            '<a href="%s">' % self.course_url in self.browser.contents)
4400
4401    def test_my_roles_page_contains_backlink(self):
4402        # we can get back from 'My Roles' view to landing page
4403        self.login_as_lecturer()
4404        self.browser.getLink("My Roles").click()
4405        self.browser.getLink("My Courses").click()
4406        self.assertEqual(self.browser.url, URL_LECTURER_LANDING)
4407
4408    def test_lecturers_can_reach_their_courses(self):
4409        # lecturers get links to their courses on the landing page
4410        self.login_as_lecturer()
4411        self.browser.getLink("COURSE1").click()
4412        self.assertEqual(self.browser.url, self.course_url)
4413
4414    def test_lecturers_student_access_is_restricted(self):
4415        # lecturers are not able to change other student data
4416        self.login_as_lecturer()
4417        # Lecturers can neither filter students ...
4418        self.assertRaises(
4419            Unauthorized, self.browser.open, '%s/students' % self.course_url)
4420        # ... nor access the student ...
4421        self.assertRaises(
4422            Unauthorized, self.browser.open, self.student_path)
4423        # ... nor the respective course ticket since editing course
4424        # tickets by lecturers is not feasible.
4425        self.assertTrue('COURSE1' in self.student['studycourse']['100'].keys())
4426        course_ticket_path = self.student_path + '/studycourse/100/COURSE1'
4427        self.assertRaises(
4428            Unauthorized, self.browser.open, course_ticket_path)
4429
4430    def test_score_editing_requires_department_permit(self):
4431        # we get a warning if we try to update score while we are not allowed
4432        self.login_as_lecturer()
4433        self.app['faculties']['fac1']['dep1'].score_editing_disabled = True
4434        self.browser.open(self.course_url)
4435        self.browser.getLink("Update session 2004/2005 scores").click()
4436        self.assertTrue('Score editing disabled' in self.browser.contents)
4437        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
4438        self.browser.open(self.course_url)
4439        self.browser.getLink("Update session 2004/2005 scores").click()
4440        self.assertFalse('Score editing disabled' in self.browser.contents)
4441
4442    def test_score_editing_requires_validated_students(self):
4443        # we can edit only scores of students whose courses have been
4444        # validated.
4445        self.login_as_lecturer()
4446        # set invalid student state
4447        IWorkflowState(self.student).setState(CREATED)
4448        self.browser.open(self.edit_scores_url)
4449        self.assertRaises(
4450            LookupError, self.browser.getControl, name="scores")
4451        # set valid student state
4452        IWorkflowState(self.student).setState(VALIDATED)
4453        self.browser.open(self.edit_scores_url)
4454        self.assertTrue(
4455            self.browser.getControl(name="scores:list") is not None)
4456
4457    def test_score_editing_offers_only_current_scores(self):
4458        # only scores from current academic session can be edited
4459        self.login_as_lecturer()
4460        IWorkflowState(self.student).setState('courses validated')
4461        # with no academic session set
4462        self.app['configuration'].current_academic_session = None
4463        self.browser.open(self.edit_scores_url)
4464        self.assertRaises(
4465            LookupError, self.browser.getControl, name="scores")
4466        # with wrong academic session set
4467        self.app['configuration'].current_academic_session = 1999
4468        self.browser.open(self.edit_scores_url)
4469        self.assertRaises(
4470            LookupError, self.browser.getControl, name="scores")
4471        # with right academic session set
4472        self.app['configuration'].current_academic_session = 2004
4473        self.browser.reload()
4474        self.assertTrue(
4475            self.browser.getControl(name="scores:list") is not None)
4476
4477    def test_score_editing_can_change_scores(self):
4478        # we can really change scores via edit_scores view
4479        self.login_as_lecturer()
4480        self.assertEqual(
4481            self.student['studycourse']['100']['COURSE1'].score, None)
4482        self.browser.open(self.edit_scores_url)
4483        self.browser.getControl(name="scores:list", index=0).value = '55'
4484        self.browser.getControl("Update scores").click()
4485        # the new value is stored in data
4486        self.assertEqual(
4487            self.student['studycourse']['100']['COURSE1'].score, 55)
4488        # the new value is displayed on page/prefilled in form
4489        self.assertEqual(
4490            self.browser.getControl(name="scores:list", index=0).value, '55')
4491        # The change has been logged
4492        with open(self.stud_log_path, 'r') as fd:
4493            self.assertTrue(
4494                'mrslecturer - students.browser.EditScoresPage - '
4495                'K1000000 100/COURSE1 score updated (55)' in fd.read())
4496
4497    def test_scores_editing_scores_must_be_integers(self):
4498        # Non-integer scores won't be accepted.
4499        self.login_as_lecturer()
4500        self.browser.open(self.edit_scores_url)
4501        self.browser.getControl(name="scores:list", index=0).value = 'abc'
4502        self.browser.getControl("Update scores").click()
4503        self.assertTrue(
4504            'Error: Score(s) of following students have not been updated '
4505            '(only integers are allowed): Anna Tester.'
4506            in self.browser.contents)
4507
4508    def test_scores_editing_allows_score_removal(self):
4509        # we can remove scores, once they were set
4510        self.login_as_lecturer()
4511        # without a prior value, we cannot remove
4512        self.student['studycourse']['100']['COURSE1'].score = None
4513        self.browser.open(self.edit_scores_url)
4514        self.browser.getControl(name="scores:list", index=0).value = ''
4515        self.browser.getControl("Update scores").click()
4516        logcontent = open(self.stud_log_path, 'r').read()
4517        self.assertFalse('COURSE1 score updated (None)' in logcontent)
4518        # now retry with some value set
4519        self.student['studycourse']['100']['COURSE1'].score = 55
4520        self.browser.getControl(name="scores:list", index=0).value = ''
4521        self.browser.getControl("Update scores").click()
4522        logcontent = open(self.stud_log_path, 'r').read()
4523        self.assertTrue('COURSE1 score updated (None)' in logcontent)
4524
4525    def test_lecturer_can_validate_courses(self):
4526        # the form is locked after validation
4527        self.login_as_lecturer()
4528        self.student['studycourse']['100']['COURSE1'].score = None
4529        self.browser.open(self.edit_scores_url)
4530        self.browser.getControl(name="scores:list", index=0).value = ''
4531        self.browser.getControl("Update scores").click()
4532        self.browser.getControl("Validate").click()
4533        self.assertTrue(
4534            'You successfully validated the course results'
4535            in self.browser.contents)
4536        self.assertEqual(self.course.results_validation_session, 2004)
4537        self.assertEqual(self.course.results_validated_by, 'Mercedes Benz')
4538        self.assertEqual(self.browser.url, self.course_url)
4539        # Lecturer can't open edit_scores again
4540        self.browser.getLink("Update session 2004/2005 scores").click()
4541        self.assertEqual(self.browser.url, self.course_url)
4542        self.assertTrue(
4543            'Course results have already been validated'
4544            ' and can no longer be changed.'
4545            in self.browser.contents)
4546        # Also DownloadScoresView is blocked
4547        self.browser.open(self.browser.url + '/download_scores')
4548        self.assertEqual(self.browser.url, self.course_url)
4549        self.assertTrue(
4550            'Course results have already been validated'
4551            ' and can no longer be changed.'
4552            in self.browser.contents)
4553        # Students Manager can open page ...
4554        prmlocal = IPrincipalRoleManager(self.course)
4555        prmlocal.assignRoleToPrincipal(
4556            'waeup.local.LocalStudentsManager', 'mrslecturer')
4557        self.browser.getLink("Update session 2004/2005 scores").click()
4558        self.assertEqual(self.browser.url, self.edit_scores_url)
4559        self.browser.getLink("Download csv file").click()
4560        self.assertEqual(self.browser.headers['Status'], '200 Ok')
4561        self.assertEqual(self.browser.headers['Content-Type'],
4562                         'text/csv; charset=UTF-8')
4563        # ... but can't validate courses a second time
4564        self.browser.open(self.edit_scores_url)
4565        self.browser.getControl("Validate").click()
4566        self.assertTrue(
4567            'Course results have already been validated.'
4568            in self.browser.contents)
4569
4570    def test_lecturers_can_download_course_tickets(self):
4571        # A course ticket slip can be downloaded
4572        self.course.title = (u'Lorem ipsum     dolor sit amet, consectetur     adipisici, '
4573                             u'sed         eiusmod tempor    incidunt ut  labore et dolore')
4574        self.login_as_lecturer()
4575        pdf_url = '%s/coursetickets.pdf' % self.course_url
4576        self.browser.open(pdf_url)
4577        self.assertEqual(self.browser.headers['Status'], '200 Ok')
4578        self.assertEqual(
4579            self.browser.headers['Content-Type'], 'application/pdf')
4580        path = os.path.join(samples_dir(), 'coursetickets.pdf')
4581        open(path, 'wb').write(self.browser.contents)
4582        print "Sample PDF coursetickets.pdf written to %s" % path
4583
4584    def test_lecturers_can_download_attendance_sheet(self):
4585        # A course ticket slip can be downloaded
4586        self.course.title = (u'Lorem ipsum     dolor sit amet, consectetur     adipisici, '
4587                             u'sed         eiusmod tempor    incidunt ut  labore et dolore')
4588        self.login_as_lecturer()
4589        pdf_url = '%s/attendance.pdf' % self.course_url
4590        self.browser.open(pdf_url)
4591        self.assertEqual(self.browser.headers['Status'], '200 Ok')
4592        self.assertEqual(
4593            self.browser.headers['Content-Type'], 'application/pdf')
4594        path = os.path.join(samples_dir(), 'attendance.pdf')
4595        open(path, 'wb').write(self.browser.contents)
4596        print "Sample PDF attendance.pdf written to %s" % path
4597
4598
4599    def test_lecturers_can_download_scores_as_csv(self):
4600        # Lecturers can download course scores as CSV.
4601        self.login_as_lecturer()
4602        self.browser.open(self.edit_scores_url)
4603        self.browser.getLink("Download csv file").click()
4604        self.assertEqual(self.browser.headers['Status'], '200 Ok')
4605        self.assertEqual(self.browser.headers['Content-Type'],
4606                         'text/csv; charset=UTF-8')
4607        self.assertEqual(self.browser.contents, 'matric_number,student_id,'
4608            'display_fullname,level,code,level_session,score\r\n234,'
4609            'K1000000,Anna Tester,100,COURSE1,2004,\r\n')
4610
4611    def test_scores_csv_upload_available(self):
4612        # lecturers can upload a CSV file to set values.
4613        self.login_as_lecturer()
4614        # set value to change from
4615        self.student['studycourse']['100']['COURSE1'].score = 55
4616        self.browser.open(self.edit_scores_url)
4617        upload_ctrl = self.browser.getControl(name='uploadfile:file')
4618        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % '65')
4619        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
4620        self.browser.getControl("Update editable scores from").click()
4621        # value changed
4622        self.assertEqual(
4623            self.student['studycourse']['100']['COURSE1'].score, 65)
4624
4625    def test_scores_csv_upload_ignored(self):
4626        # for many type of file contents we simply ignore uploaded data
4627        self.login_as_lecturer()
4628        self.student['studycourse']['100']['COURSE1'].score = 55
4629        self.browser.open(self.edit_scores_url)
4630        for content, mimetype, name in (
4631                # empty file
4632                ('', 'text/foo', 'my.foo'),
4633                # plain ASCII text, w/o comma
4634                ('abcdef' * 200, 'text/plain', 'my.txt'),
4635                # plain UTF-8 text, with umlauts
4636                ('umlauts: äöü', 'text/plain', 'my.txt'),
4637                # csv file with only a header row
4638                ('student_id,score', 'text/csv', 'my.csv'),
4639                # csv with student_id column missing
4640                ('foo,score\r\nbar,66\r\n', 'text/csv', 'my.csv'),
4641                # csv with score column missing
4642                ('student_id,foo\r\nK1000000,bar\r\n', 'text/csv', 'my.csv'),
4643                # csv with non number as score value
4644                (UPLOAD_CSV_TEMPLATE % 'not-a-number', 'text/csv', 'my.csv'),
4645                ):
4646            upload_ctrl = self.browser.getControl(name='uploadfile:file')
4647            upload_ctrl.add_file(StringIO(content), mimetype, name)
4648            self.browser.getControl("Update scores").click()
4649            self.assertEqual(
4650                self.student['studycourse']['100']['COURSE1'].score, 55)
4651            self.assertFalse(
4652                'Uploaded file contains illegal data' in self.browser.contents)
4653
4654    def test_scores_csv_upload_warn_illegal_chars(self):
4655        # for some types of files we issue a warning if upload data
4656        # contains illegal chars (and ignore the data)
4657        self.login_as_lecturer()
4658        self.student['studycourse']['100']['COURSE1'].score = 55
4659        self.browser.open(self.edit_scores_url)
4660        for content, mimetype, name in (
4661                # plain ASCII text, commas, control chars
4662                ('abv,qwe\n\r\r\t\b\n' * 20, 'text/plain', 'my.txt'),
4663                # image data (like a JPEG image)
4664                (open(SAMPLE_IMAGE, 'rb').read(), 'image/jpg', 'my.jpg'),
4665                ):
4666            upload_ctrl = self.browser.getControl(name='uploadfile:file')
4667            upload_ctrl.add_file(StringIO(content), mimetype, name)
4668            self.browser.getControl("Update editable scores").click()
4669            self.assertEqual(
4670                self.student['studycourse']['100']['COURSE1'].score, 55)
4671            self.assertTrue(
4672                'Uploaded file contains illegal data' in self.browser.contents)
4673
4674class ParentsUITests(StudentsFullSetup):
4675    # Tests for UI actions when acting as parents.
4676
4677    def test_login_as_parents(self):
4678        # Sstudent login still works after all the changes made
4679        self.browser.open(self.login_path)
4680        self.browser.getControl(name="form.login").value = self.student_id
4681        self.browser.getControl(name="form.password").value = 'spwd'
4682        self.browser.getControl("Login").click()
4683        self.assertTrue('You logged in' in self.browser.contents)
4684        self.browser.open(self.edit_personal_path)
4685        self.browser.getLink("Logout").click()
4686        self.assertTrue('You have been logged out' in self.browser.contents)
4687        # We set parents password
4688        self.app['students'][self.student_id].setParentsPassword('ppwd')
4689        self.browser.open(self.login_path)
4690        # Student can't login with original password
4691        self.browser.getControl(name="form.login").value = self.student_id
4692        self.browser.getControl(name="form.password").value = 'spwd'
4693        self.browser.getControl("Login").click()
4694        self.assertEqual(self.browser.url, self.login_path)
4695        self.assertTrue('Your account has been temporarily deactivated '
4696            'because your parents have logged in.' in self.browser.contents)
4697        # Parents can login with their password
4698        self.browser.open(self.login_path)
4699        self.browser.getControl(name="form.login").value = self.student_id
4700        self.browser.getControl(name="form.password").value = 'ppwd'
4701        self.browser.getControl("Login").click()
4702        self.assertTrue(
4703            'You logged in.' in self.browser.contents)
4704        self.assertTrue(
4705            '<a href="http://localhost/app/students/K1000000">Base Data</a>'
4706            in self.browser.contents)
4707        # They do not see all links ...
4708        self.assertFalse(
4709            '<a href="http://localhost/app/students/K1000000/history">History</a>'
4710            in self.browser.contents)
4711        # ... and can't change anything
4712        self.assertRaises(
4713            Unauthorized, self.browser.open, self.edit_personal_path)
4714        # If the password has expired, parents are logged out and the
4715        # student can login again with the original password
4716        delta = timedelta(minutes=11)
4717        self.app['students'][self.student_id].parents_password[
4718            'timestamp'] = datetime.utcnow() - delta
4719        self.app['students'][self.student_id]._p_changed = True
4720        self.assertRaises(
4721            Unauthorized, self.browser.open, self.student_path)
4722        # Parents login is written to log file
4723        logfile = os.path.join(
4724            self.app['datacenter'].storage, 'logs', 'students.log')
4725        logcontent = open(logfile).read()
4726        self.assertTrue(
4727            'K1000000 - browser.pages.LoginPage - K1000000 - Parents logged in'
4728            in logcontent)
Note: See TracBrowser for help on using the repository browser.