source: main/waeup.aaue/trunk/src/waeup/aaue/students/tests/test_browser.py @ 17661

Last change on this file since 17661 was 17614, checked in by Henrik Bettermann, 13 months ago

Update fees.

  • Property svn:keywords set to Id
File size: 75.5 KB
Line 
1## $Id: test_browser.py 17614 2023-10-16 04:11:00Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18import os
19import shutil
20import tempfile
21import pytz
22import grok
23from StringIO import StringIO
24from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
25from zope.securitypolicy.interfaces import IPrincipalRoleManager
26from datetime import datetime, timedelta, date
27from mechanize import LinkNotFoundError, ItemNotFoundError
28from hurry.workflow.interfaces import IWorkflowState
29from zope.event import notify
30from zope.component.hooks import setSite, clearSite
31from zope.component import getUtility, createObject, queryUtility
32from zope.catalog.interfaces import ICatalog
33from waeup.kofa.app import University
34from waeup.kofa.interfaces import VALIDATED, PAID
35from waeup.kofa.students.tests.test_browser import StudentsFullSetup
36from waeup.kofa.students.accommodation import BedTicket
37from waeup.kofa.testing import FunctionalTestCase
38from waeup.kofa.authentication import LocalRoleSetEvent
39from waeup.kofa.browser.tests.test_pdf import samples_dir
40from waeup.kofa.applicants.container import ApplicantsContainer
41from waeup.aaue.testing import FunctionalLayer
42
43SAMPLE_IMAGE = os.path.join(os.path.dirname(__file__), 'test_image.jpg')
44
45
46UPLOAD_CSV_TEMPLATE = (
47    'matric_number,student_id,display_fullname,level,code,level_session,'
48    'score,ca,imported_ts\r\n'
49    '234,E1000000,Anna Tester,100,COURSE1,2004,%s,%s,%s\r\n')
50
51class OfficerUITests(StudentsFullSetup):
52    # Tests for Student class views and pages
53
54    layer = FunctionalLayer
55
56    def setUp(self):
57        super(OfficerUITests, self).setUp()
58        self.app['faculties']['fac1']['dep1'].certificates[
59            'CERT1']['COURSE1_100'].course_category = 'C'
60
61    def login_as_lecturer(self):
62        self.app['users'].addUser('mrslecturer', 'mrslecturerSecret1')
63        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
64        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
65        # Add course ticket
66        studylevel = createObject(u'waeup.StudentStudyLevel')
67        studylevel.level = 100
68        studylevel.level_session = 2004
69        self.student['studycourse'].addStudentStudyLevel(
70            self.certificate, studylevel)
71        # Assign local Lecturer role for a certificate.
72        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
73        prmlocal = IPrincipalRoleManager(course)
74        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
75        notify(LocalRoleSetEvent(
76            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
77        # Login as lecturer.
78        self.browser.open(self.login_path)
79        self.browser.getControl(name="form.login").value = 'mrslecturer'
80        self.browser.getControl(
81            name="form.password").value = 'mrslecturerSecret1'
82        self.browser.getControl("Login").click()
83        # Store reused urls/paths
84        self.course_url = (
85            'http://localhost/app/faculties/fac1/dep1/courses/COURSE1')
86        self.edit_scores_url = '%s/edit_scores' % self.course_url
87        self.edit_prev_scores_url = '%s/edit_prev_scores' % self.course_url
88        # Set standard parameters
89        self.app['configuration'].current_academic_session = 2004
90        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
91        IWorkflowState(self.student).setState(VALIDATED)
92
93
94    def test_gpa_calculation(self):
95        studylevel = createObject(u'waeup.StudentStudyLevel')
96        studylevel.level = 100
97        studylevel.level_session = 2005
98        self.student['studycourse'].entry_mode = 'ug_ft'
99        self.student['studycourse'].addStudentStudyLevel(
100            self.certificate, studylevel)
101        # First course has been added automatically.
102        # Set score.
103        studylevel['COURSE1'].score = 35
104        studylevel['COURSE1'].ca = 20
105        # GPA is 3.0.
106        self.assertEqual(studylevel.gpa_params[0], 3.0)
107        courseticket = createObject('waeup.CourseTicket')
108        courseticket.code = 'ANYCODE'
109        courseticket.title = u'Any TITLE'
110        courseticket.credits = 13
111        courseticket.score = 44
112        courseticket.ca = 22
113        courseticket.semester = 1
114        courseticket.dcode = u'ANYDCODE'
115        courseticket.fcode = u'ANYFCODE'
116        studylevel['COURSE2'] = courseticket
117        # total credits
118        self.assertEqual(self.student['studycourse']['100'].gpa_params[1], 23)
119        # weigheted credits = 3 * 10 + 4 * 13
120        self.assertEqual(self.student['studycourse']['100'].gpa_params[2], 82.0)
121        # sgpa = 82 / 23
122        self.assertEqual(
123            self.student['studycourse']['100'].gpa_params[0], 3.5652173913043477)
124        # imported gpa values override calculated values
125        studylevel.imported_gpa = 4.3
126        studylevel.imported_cgpa = 5.4
127        self.assertEqual(self.student['studycourse']['100'].gpa_params[0], 4.3)
128        self.assertEqual(
129            self.student['studycourse']['100'].cumulative_params[0], 5.4)
130        self.assertEqual(self.student['studycourse']['100'].gpa, '4.30')
131        self.student['studycourse'].imported_cgpa = 6.6
132        self.assertEqual(
133            self.student['studycourse'].getTranscriptData()[1], 6.6)
134        return
135
136    def test_grade_weight(self):
137        studylevel = createObject(u'waeup.StudentStudyLevel')
138        studylevel.level = 100
139        studylevel.level_session = 2005
140        self.course.passmark = 40
141        self.student['studycourse'].entry_mode = 'ug_ft'
142        self.student['studycourse'].addStudentStudyLevel(
143            self.certificate, studylevel)
144        studylevel['COURSE1'].score = 42
145        studylevel['COURSE1'].ca = 0
146        courseticket = createObject('waeup.CourseTicket')
147        self.assertEqual(studylevel['COURSE1'].weight, 1)
148        self.assertEqual(studylevel['COURSE1'].grade, 'E')
149        self.assertEqual(studylevel['COURSE1'].weight, 1)
150        self.assertEqual(studylevel['COURSE1'].grade, 'E')
151        studylevel['COURSE1'].score = 45
152        self.assertEqual(studylevel['COURSE1'].weight, 2)
153        self.assertEqual(studylevel['COURSE1'].grade, 'D')
154        return
155
156    def test_manage_payments(self):
157        # Add missing configuration data
158        self.app['configuration']['2004'].gown_fee = 150.0
159        self.app['configuration']['2004'].transfer_fee = 90.0
160        self.app['configuration']['2004'].booking_fee = 150.0
161        self.app['configuration']['2004'].maint_fee = 180.0
162        self.app['configuration']['2004'].late_fee = 80.0
163
164        # Managers can add online payment tickets
165        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
166        self.browser.open(self.payments_path)
167        self.browser.getLink("Add current session payment ticket").click()
168        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
169        self.browser.getControl("Create ticket").click()
170        self.assertMatches('...Wrong state...',
171                           self.browser.contents)
172        IWorkflowState(self.student).setState('cleared')
173        self.browser.open(self.payments_path + '/addop')
174        self.app['configuration']['2004'].clearance_fee = 666.0
175        self.browser.getControl(name="form.p_category").value = ['clearance_incl']
176        self.browser.getControl("Create ticket").click()
177        self.browser.open(self.payments_path)
178        ctrl = self.browser.getControl(name='val_id')
179        cpt_value = ctrl.options[0]
180        # School fee payment ticket can be added ...
181        #self.student['studycourse'].certificate.school_fee_3 = 6666.0
182        self.student.nationality = u'NG'
183        self.browser.open(self.payments_path + '/addop')
184        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
185        self.browser.getControl("Create ticket").click()
186        self.assertMatches('...ticket created...',
187                           self.browser.contents)
188        # ... but not paid through the query_history page.
189        self.browser.open(self.payments_path)
190        ctrl = self.browser.getControl(name='val_id')
191        sfpt_value = ctrl.options[1]
192        self.student['studycourse'].entry_session = 2013
193        self.student['payments'][sfpt_value].r_company = u'interswitch'
194        self.browser.open(self.payments_path + '/' + sfpt_value)
195
196        # eTranzact payments deactivated on 01/03/2019
197        #self.browser.getLink("Query eTranzact History").click()
198        #self.assertMatches('...alert-danger">Please pay acceptance fee first...',
199        #                   self.browser.contents)
200        # If clearance/acceptance fee is paid ...
201        self.student['payments'][cpt_value].approveStudentPayment()
202        #self.browser.getLink("Query eTranzact History").click()
203        ## ... query_history page is accessible.
204        #self.assertMatches(
205        #    '...<h1 class="kofa-content-label">Requery eTranzact History</h1>...',
206        #    self.browser.contents)
207
208        # Managers can open school fee payment slip
209        self.browser.open(self.payments_path + '/' + sfpt_value)
210        self.browser.getLink("Download payment slip").click()
211        self.assertEqual(self.browser.headers['Status'], '200 Ok')
212        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
213        # If school fee ticket is paid, the student is automatically set to
214        # school fee paid...
215        ticket = self.student['payments'][sfpt_value].approveStudentPayment()
216        self.assertEqual(self.student.state, 'school fee paid')
217        # ... no further school fee ticket can be added.
218        self.browser.open(self.payments_path + '/addop')
219        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
220        self.browser.getControl("Create ticket").click()
221        self.assertMatches('...Wrong state...',
222                           self.browser.contents)
223        self.browser.open(self.payments_path + '/addop')
224        #self.browser.getControl(name="form.p_category").value = ['late_registration']
225        #self.browser.getControl("Create ticket").click()
226        #self.assertMatches('...ticket created...',
227        #                   self.browser.contents)
228        return
229
230    def test_for_instalment_payments(self):
231        configuration_1 = createObject('waeup.SessionConfiguration')
232        configuration_1.academic_session = 2015
233        self.app['configuration'].addSessionConfiguration(configuration_1)
234        self.student['studycourse'].certificate.study_mode = 'ug_pt'
235        self.student.nationality = u'NG'
236        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
237        self.browser.open(self.payments_path + '/addop')
238        #self.assertFalse('schoolfee_1' in self.browser.contents)
239        #self.browser.getControl(name="form.p_category").value = ['schoolfee_1']
240        #self.browser.getControl("Create ticket").click()
241        #self.assertTrue(
242        #    'Part-time students are not allowed' in self.browser.contents)
243        self.student['studycourse'].certificate.study_mode = 'ug_ft'
244        self.browser.open(self.payments_path + '/addop')
245        self.assertTrue('schoolfee_1' in self.browser.contents)
246        self.browser.getControl(name="form.p_category").value = ['schoolfee_1']
247        self.browser.getControl("Create ticket").click()
248        self.assertTrue('You are not allowed to pay by instalments.'
249            in self.browser.contents)
250        IWorkflowState(self.student).setState('cleared')
251        self.student['studycourse'].entry_session = 2015
252        self.student['studycourse'].current_session = 2015
253        self.browser.open(self.payments_path + '/addop')
254        self.browser.getControl(name="form.p_category").value = ['schoolfee_1']
255        self.browser.getControl("Create ticket").click()
256        self.assertTrue('ticket created' in self.browser.contents)
257        # We can't add the 2nd instalment ticket because the
258        # first one has not yet been approved.
259        #self.browser.open(self.payments_path + '/addop')
260        #self.browser.getControl(name="form.p_category").value = ['schoolfee_2']
261        #self.browser.getControl("Create ticket").click()
262        #self.assertMatches('...1st school fee instalment has not yet been paid...',
263        #                   self.browser.contents)
264        # Ok, then we approve the first instalment ...
265        self.browser.open(self.payments_path)
266        ctrl = self.browser.getControl(name='val_id')
267        p_id = ctrl.options[0]
268        self.browser.open(self.payments_path + '/' + p_id + '/approve')
269        # ... add the second instalment ...
270        self.browser.open(self.payments_path + '/addop')
271        self.browser.getControl(name="form.p_category").value = ['schoolfee_2']
272        self.browser.getControl("Create ticket").click()
273        self.assertTrue('ticket created' in self.browser.contents)
274        # ... approve the second instalment ...
275        self.browser.open(self.payments_path)
276        ctrl = self.browser.getControl(name='val_id')
277        p_id = ctrl.options[1]
278        self.browser.open(self.payments_path + '/' + p_id + '/approve')
279        self.assertEqual(self.student['payments'].values()[0].p_category, 'schoolfee_1')
280        self.assertEqual(self.student['payments'].values()[1].p_category, 'schoolfee_2')
281        self.assertEqual(self.student['payments'].values()[0].amount_auth, 144000.0)
282        self.assertEqual(self.student['payments'].values()[1].amount_auth, 92000.0)
283        # The  two payments belong to the same session and level.
284        self.assertEqual(self.student['payments'].values()[0].p_session, 2015)
285        self.assertEqual(self.student['payments'].values()[0].p_level, 100)
286        self.assertEqual(self.student['payments'].values()[1].p_session, 2015)
287        self.assertEqual(self.student['payments'].values()[1].p_level, 100)
288
289        # Returning student
290        configuration_2 = createObject('waeup.SessionConfiguration')
291        configuration_2.academic_session = 2016
292        self.app['configuration'].addSessionConfiguration(configuration_2)
293        self.student.father_name = u'Albert'
294        IWorkflowState(self.student).setState('returning')
295        self.browser.open(self.payments_path + '/addop')
296        self.browser.getControl(name="form.p_category").value = ['schoolfee_1']
297        self.browser.getControl("Create ticket").click()
298        self.browser.open(self.payments_path + '/addop')
299        self.browser.getControl(name="form.p_category").value = ['schoolfee_2']
300        self.browser.getControl("Create ticket").click()
301        # Student is still in the first  session.
302        self.assertTrue('This type of payment' in self.browser.contents)
303        self.browser.open(self.payments_path)
304        ctrl = self.browser.getControl(name='val_id')
305        p_id = ctrl.options[2]
306        self.browser.open(self.payments_path + '/' + p_id + '/approve')
307        self.browser.open(self.payments_path + '/addop')
308        self.browser.getControl(name="form.p_category").value = ['schoolfee_2']
309        self.browser.getControl("Create ticket").click()
310        self.assertTrue('ticket created' in self.browser.contents)
311        self.assertEqual(self.student['payments'].values()[2].amount_auth, 144000.0)
312        self.assertEqual(self.student['payments'].values()[3].amount_auth, 92000.0)
313        # The last two payments belong to the same session and level.
314        self.assertEqual(self.student['payments'].values()[2].p_session, 2016)
315        self.assertEqual(self.student['payments'].values()[2].p_level, 200)
316        self.assertEqual(self.student['payments'].values()[3].p_session, 2016)
317        self.assertEqual(self.student['payments'].values()[3].p_level, 200)
318        return
319
320    def test_manage_payments_bypass_ac_creation(self):
321        #self.student['studycourse'].certificate.school_fee_3 = 6666.0
322        self.student.nationality = u'NG'
323        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
324        self.browser.open(self.payments_path)
325        IWorkflowState(self.student).setState('cleared')
326        self.browser.getLink("Add current session payment ticket").click()
327        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
328        self.browser.getControl("Create ticket").click()
329        self.browser.open(self.payments_path)
330        ctrl = self.browser.getControl(name='val_id')
331        value = ctrl.options[0]
332        self.browser.getLink(value).click()
333        payment_url = self.browser.url
334        logfile = os.path.join(
335            self.app['datacenter'].storage, 'logs', 'students.log')
336        # The ticket can be found in the payments_catalog
337        cat = queryUtility(ICatalog, name='payments_catalog')
338        results = list(cat.searchResults(p_state=('unpaid', 'unpaid')))
339        self.assertTrue(len(results), 1)
340        self.assertTrue(results[0] is self.student['payments'][value])
341        # Managers can approve the payment
342        self.browser.open(payment_url)
343        self.browser.getLink("Approve payment").click()
344        self.assertMatches('...Payment approved...',
345                          self.browser.contents)
346        # Approval is logged in students.log ...
347        logcontent = open(logfile).read()
348        self.assertTrue(
349            'zope.mgr - students.browser.OnlinePaymentApproveView '
350            '- E1000000 - schoolfee_incl payment approved'
351            in logcontent)
352        # ... and in payments.log
353        logfile = os.path.join(
354            self.app['datacenter'].storage, 'logs', 'payments.log')
355        logcontent = open(logfile).read()
356        self.assertTrue(
357            '"zope.mgr",E1000000,%s,schoolfee_incl,240500.0,AP,,,,,,\n' % value
358            in logcontent)
359        # Student is in state school fee paid, no activation
360        # code was necessary.
361        self.assertEqual(self.student.state, 'school fee paid')
362        self.assertEqual(len(self.app['accesscodes']['SFE-0']),0)
363        return
364
365    def test_scores_csv_upload_available(self):
366        # lecturers can upload a CSV file to set values.
367        self.login_as_lecturer()
368        # set value to change from
369        self.student['studycourse']['100']['COURSE1'].score = 55
370        self.app['configuration']['2004'].score_editing_enabled = ['ug_ft']
371        self.browser.open(self.edit_scores_url)
372        upload_ctrl = self.browser.getControl(name='uploadfile:file')
373        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % ('65','52','77'))
374        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
375        self.browser.getControl("Update editable scores from").click()
376        self.assertTrue('TESTER, Anna have not be updated' in self.browser.contents)
377        # values have not been changed
378        self.assertEqual(
379            self.student['studycourse']['100']['COURSE1'].score, 55)
380        self.assertEqual(
381            self.student['studycourse']['100']['COURSE1'].ca, None)
382        self.assertEqual(
383            self.student['studycourse']['100']['COURSE1'].imported_ts, None)
384        self.browser.open(self.edit_scores_url)
385        upload_ctrl = self.browser.getControl(name='uploadfile:file')
386        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % ('65','22','77'))
387        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
388        self.browser.getControl("Update editable scores from").click()
389        # values changed
390        self.assertEqual(
391            self.student['studycourse']['100']['COURSE1'].score, 65)
392        self.assertEqual(
393            self.student['studycourse']['100']['COURSE1'].ca, 22)
394        self.assertEqual(
395            self.student['studycourse']['100']['COURSE1'].imported_ts, 77)
396
397    def test_scores_previous_session(self):
398        # lecturers can download a CSV file to set values.
399        self.login_as_lecturer()
400        self.student['studycourse']['100']['COURSE1'].score = 55
401        self.browser.open(self.edit_prev_scores_url)
402        self.assertTrue('No student found' in self.browser.contents)
403        configuration = createObject('waeup.SessionConfiguration')
404        configuration.academic_session = 2003
405        self.app['configuration'].addSessionConfiguration(configuration)
406        self.app['configuration']['2003'].score_editing_enabled = ['ug_ft']
407        self.student['studycourse']['100'].level_session = 2003
408        notify(grok.ObjectModifiedEvent(self.student['studycourse']['100']['COURSE1']))
409        self.browser.open(self.edit_prev_scores_url)
410        self.browser.getLink("Download csv file").click()
411        self.assertEqual(self.browser.headers['Status'], '200 Ok')
412        self.assertEqual(self.browser.headers['Content-Type'],
413                         'text/csv; charset=UTF-8')
414        self.assertEqual(self.browser.contents,
415            'matric_number,student_id,display_fullname,'
416            'depcode,faccode,level,code,level_session,ca,score,'
417            'total_score,grade,imported_ts\r\n234,E1000000,"TESTER, Anna",dep1,fac1,'
418            '100,COURSE1,2003,,55,,,\r\n')
419        self.browser.open(self.edit_prev_scores_url)
420        upload_ctrl = self.browser.getControl(name='uploadfile:file')
421        upload_file = StringIO(UPLOAD_CSV_TEMPLATE % ('65','22','77'))
422        upload_ctrl.add_file(upload_file, 'text/csv', 'myscores.csv')
423        self.browser.getControl("Update editable scores from").click()
424        # value changed
425        self.assertEqual(
426            self.student['studycourse']['100']['COURSE1'].score, 65)
427        self.assertEqual(
428            self.student['studycourse']['100']['COURSE1'].ca, 22)
429        self.assertEqual(
430            self.student['studycourse']['100']['COURSE1'].imported_ts, 77)
431
432    def test_lecturers_can_download_course_tickets(self):
433        # A course ticket slip can be downloaded
434        self.login_as_lecturer()
435        self.course.title = (u'Lorem ipsum dolor sit amet, consectetur '
436                            u'adipisici elit, sed eiusmod tempor incidunt')
437        self.student['studycourse']['100']['COURSE1'].score = 55
438        self.student['studycourse']['100']['COURSE1'].ca = 11
439        self.student.matric_number = u'CMS/FBM/NSG/16/28838'
440        self.app['configuration']['2004'].score_editing_enabled = ['ug_ft']
441        pdf_url = '%s/coursetickets.pdf' % self.course_url
442        self.browser.open(pdf_url)
443        self.assertEqual(self.browser.headers['Status'], '200 Ok')
444        self.assertEqual(
445            self.browser.headers['Content-Type'], 'application/pdf')
446        path = os.path.join(samples_dir(), 'coursetickets.pdf')
447        open(path, 'wb').write(self.browser.contents)
448        print "Sample PDF coursetickets.pdf written to %s" % path
449        # The CA column is not shown if CA is not used
450        self.student['studycourse']['100']['COURSE1'].ca = 0
451        self.browser.open(pdf_url)
452        path = os.path.join(samples_dir(), 'coursetickets_wo_ca.pdf')
453        open(path, 'wb').write(self.browser.contents)
454        print "Sample PDF coursetickets_wo_ca.pdf written to %s" % path
455
456    def test_lecturers_do_only_see_selected_students(self):
457        # A course ticket slip can be downloaded
458        self.login_as_lecturer()
459        self.student['studycourse']['100']['COURSE1'].score = 55
460        self.student['studycourse']['100']['COURSE1'].ca = 11
461        self.browser.open(self.edit_scores_url)
462        self.assertTrue('No student found' in self.browser.contents)
463        pdf_url = '%s/coursetickets.pdf' % self.course_url
464        self.browser.open(pdf_url)
465        self.assertEqual(self.browser.headers['Status'], '200 Ok')
466        self.assertEqual(
467            self.browser.headers['Content-Type'], 'application/pdf')
468        path = os.path.join(samples_dir(), 'coursetickets_filtered.pdf')
469        open(path, 'wb').write(self.browser.contents)
470        print "Sample PDF coursetickets_filtered.pdf written to %s" % path
471        self.app['configuration']['2004'].score_editing_enabled = ['ug_ft']
472        self.browser.open(self.edit_scores_url)
473        self.assertFalse('No student found' in self.browser.contents)
474        self.assertTrue('TESTER, Anna' in self.browser.contents)
475
476    def test_lecturers_can_download_attendance_sheet(self):
477        # A course ticket slip can be downloaded
478        self.course.title = (u'Lorem ipsum     dolor sit amet, consectetur     adipisici, '
479                             u'sed         eiusmod tempor    incidunt ut  labore et dolore')
480        self.student.firstname = u'Emmanuella Woyengifigha Mercy Onosemudiana'
481        self.student.lastname = u'OYAKEMIEGBEGHA'
482        self.student.matric_number = u'CMS/FBM/NSG/17/38186'
483        self.login_as_lecturer()
484        pdf_url = '%s/attendance.pdf' % self.course_url
485        self.browser.open(pdf_url)
486        self.assertEqual(self.browser.headers['Status'], '200 Ok')
487        self.assertEqual(
488            self.browser.headers['Content-Type'], 'application/pdf')
489        path = os.path.join(samples_dir(), 'attendance.pdf')
490        open(path, 'wb').write(self.browser.contents)
491        print "Sample PDF attendance.pdf written to %s" % path
492
493    def test_transcripts(self):
494        studylevel = createObject(u'waeup.StudentStudyLevel')
495        studylevel.level = 100
496        studylevel.level_session = 2005
497        self.student['studycourse'].entry_mode = 'ug_ft'
498        self.student['studycourse'].addStudentStudyLevel(
499            self.certificate, studylevel)
500        studylevel2 = createObject(u'waeup.StudentStudyLevel')
501        studylevel2.level = 200
502        studylevel2.level_session = 2006
503        self.student['studycourse']['100']['COURSE1'].score = 33 # no carry-over!
504        self.student['studycourse']['100']['COURSE1'].ca = 22
505        self.student['studycourse'].addStudentStudyLevel(
506            self.certificate, studylevel2)
507        # Add second course (COURSE has been added automatically)
508        courseticket = createObject('waeup.CourseTicket')
509        courseticket.code = 'ANYCODE'
510        courseticket.title = u'Any TITLE'
511        courseticket.credits = 13
512        courseticket.score = 55
513        courseticket.ca = 11
514        courseticket.semester = 1
515        courseticket.dcode = u'ANYDCODE'
516        courseticket.fcode = u'ANYFCODE'
517        self.student['studycourse']['200']['COURSE2'] = courseticket
518        self.assertEqual(self.student['studycourse']['100'].gpa_params_rectified[0], 3.0)
519        self.assertEqual(self.student['studycourse']['200'].gpa_params_rectified[0], 4.0)
520        # Get transcript data
521        td = self.student['studycourse'].getTranscriptData()
522        self.assertEqual(td[0][0]['level_key'], '100')
523        self.assertEqual(td[0][0]['sgpa'], 3.0)
524        self.assertEqual(td[0][0]['level'].level, 100)
525        self.assertEqual(td[0][0]['level'].level_session, 2005)
526        self.assertEqual(td[0][0]['tickets_1'][0].code, 'COURSE1')
527        self.assertEqual(td[0][1]['level_key'], '200')
528        self.assertEqual(td[0][1]['sgpa'], 4.0)
529        self.assertEqual(td[0][1]['level'].level, 200)
530        self.assertEqual(td[0][1]['level'].level_session, 2006)
531        self.assertEqual(td[0][1]['tickets_1'][0].code, 'ANYCODE')
532        self.assertEqual(td[1], 3.5652173913043477)
533        # Set Officer 1
534        self.app['faculties']['fac1']['dep1'].officer_1 = u'Transcript Boss'
535        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
536        self.browser.open(self.student_path + '/studycourse/transcript')
537        self.assertEqual(self.browser.headers['Status'], '200 Ok')
538        self.assertTrue('Transcript' in self.browser.contents)
539        # Officers can open the pdf transcript
540        self.browser.open(self.student_path + '/studycourse/transcript.pdf')
541        self.assertEqual(self.browser.headers['Status'], '200 Ok')
542        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
543        path = os.path.join(samples_dir(), 'transcript.pdf')
544        open(path, 'wb').write(self.browser.contents)
545        print "Sample PDF transcript.pdf written to %s" % path
546        # Officers can open the pdf student viewtranscript
547        self.browser.open(self.student_path + '/studycourse/transcript_studview.pdf')
548        self.assertEqual(self.browser.headers['Status'], '200 Ok')
549        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
550        path = os.path.join(samples_dir(), 'transcript_studview.pdf')
551        open(path, 'wb').write(self.browser.contents)
552        print "Sample PDF transcript_studview.pdf written to %s" % path
553
554    def test_payment_disabled(self):
555        #self.student['studycourse'].certificate.school_fee_3 = 6666.0
556        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
557        self.browser.open(self.payments_path)
558        IWorkflowState(self.student).setState('cleared')
559        self.browser.getLink("Add current session payment ticket").click()
560        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
561        self.browser.getControl("Create ticket").click()
562        self.assertMatches('...ticket created...', self.browser.contents)
563        self.app['configuration']['2004'].payment_disabled = ['sf_all']
564        self.browser.open(self.payments_path)
565        self.browser.getLink("Add current session payment ticket").click()
566        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
567        self.browser.getControl("Create ticket").click()
568        self.assertMatches('...This category of payments has been disabled...',
569                           self.browser.contents)
570
571        self.app['configuration']['2004'].payment_disabled = ['sf_ug_pt']
572        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
573        self.browser.getControl("Create ticket").click()
574        self.assertMatches('...ticket created...', self.browser.contents)
575        self.certificate.study_mode = 'ug_pt'
576        self.browser.open(self.payments_path)
577        self.browser.getLink("Add current session payment ticket").click()
578        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
579        self.browser.getControl("Create ticket").click()
580        self.assertMatches('...This category of payments has been disabled...',
581                           self.browser.contents)
582
583        #self.app['configuration']['2004'].payment_disabled = ['sf_pg']
584        #self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
585        #self.browser.getControl("Create ticket").click()
586        #self.assertMatches('...ticket created...', self.browser.contents)
587        #self.certificate.study_mode = 'special_pg_ft'
588        #self.browser.open(self.payments_path)
589        #self.browser.getLink("Add current session payment ticket").click()
590        #self.browser.getControl(name="form.p_category").value = ['schoolfee']
591        #self.browser.getControl("Create ticket").click()
592        #self.assertMatches('...This category of payments has been disabled...',
593        #                   self.browser.contents)
594        return
595
596class StudentUITests(StudentsFullSetup):
597    """Tests for customized student class views and pages
598    """
599
600    layer = FunctionalLayer
601
602    def setUp(self):
603        super(StudentUITests, self).setUp()
604        self.app['faculties']['fac1']['dep1'].certificates[
605            'CERT1']['COURSE1_100'].course_category = 'C'
606        bedticket = BedTicket()
607        bedticket.booking_session = 2004
608        bedticket.bed_type = u'any bed type'
609        bedticket.bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
610        bedticket.bed_coordinates = u'My bed coordinates'
611        self.student['accommodation'].addBedTicket(bedticket)
612
613    def test_maintenance_fee_payment(self):
614        self.certificate.study_mode = 'ug_ft'
615        self.student['studycourse'].entry_session = 2013
616        self.student.nationality = u'NG'
617        IWorkflowState(self.student).setState('cleared')
618        self.browser.open(self.login_path)
619        self.browser.getControl(name="form.login").value = self.student_id
620        self.browser.getControl(name="form.password").value = 'spwd'
621        self.browser.getControl("Login").click()
622        self.browser.open(self.student_path + '/payments')
623        self.browser.getLink("Add current session payment ticket").click()
624        self.browser.getControl(name="form.p_category").value = ['hostel_maintenance']
625        self.browser.getControl("Create ticket").click()
626        self.assertTrue('ticket created' in self.browser.contents)
627        value = self.student['payments'].keys()[0]
628        self.browser.getLink(value).click()
629        self.assertTrue('<span>My bed coordinates</span>' in self.browser.contents)
630        self.assertEqual(self.student['payments'][value].amount_auth, 876.0)
631        return
632
633    def test_student_schoolfee_payments(self):
634        configuration_1 = createObject('waeup.SessionConfiguration')
635        configuration_1.academic_session = 2018
636        self.app['configuration'].addSessionConfiguration(configuration_1)
637        self.certificate.study_mode = 'ug_ft'
638        self.student['studycourse'].entry_session = 2018
639        self.student['studycourse'].current_session = 2018
640        self.student['studycourse'].entry_mode = 'ug_ft'
641        #self.student['studycourse'].certificate.school_fee_2 = 50200.0
642        self.student.nationality = u'NG'
643        # Login
644        IWorkflowState(self.student).setState('cleared')
645        self.browser.open(self.login_path)
646        self.browser.getControl(name="form.login").value = self.student_id
647        self.browser.getControl(name="form.password").value = 'spwd'
648        self.browser.getControl("Login").click()
649        # Test school fee payments
650        self.browser.open(self.student_path + '/payments')
651        self.browser.getLink("Add current session payment ticket").click()
652        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
653        self.browser.getControl("Create ticket").click()
654        self.assertTrue('ticket created' in self.browser.contents)
655        value = self.student['payments'].keys()[0]
656        self.student['payments'][value].p_state = 'paid'
657        self.browser.getLink(value).click()
658        self.assertTrue('Amount Authorized' in self.browser.contents)
659        self.assertEqual(self.student['payments'][value].amount_auth, 236000.0)
660        self.student['payments'][value].r_company = u'interswitch'
661        self.browser.getLink("Download").click()
662        self.assertEqual(self.browser.headers['Status'], '200 Ok')
663        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
664        path = os.path.join(samples_dir(), 'payment_slip.pdf')
665        open(path, 'wb').write(self.browser.contents)
666        print "Sample PDF payment_slip.pdf written to %s" % path
667        # Another school fee payment cannot be added
668        #self.student['payments'][value].approve()
669        #self.browser.open(self.student_path + '/payments')
670        #self.browser.getLink("Add current session payment ticket").click()
671        #self.browser.getControl(name="form.p_category").value = ['schoolfee']
672        #self.browser.getControl("Create ticket").click()
673        #self.assertTrue(
674        #    'You must choose a payment which includes additional fees'
675        #    in self.browser.contents)
676        configuration_2 = createObject('waeup.SessionConfiguration')
677        configuration_2.academic_session = 2019
678        self.app['configuration'].addSessionConfiguration(configuration_2)
679        self.student.father_name = u'Rudolf'
680        IWorkflowState(self.student).setState('returning')
681        self.browser.open(self.student_path + '/payments')
682        self.browser.getLink("Add current session payment ticket").click()
683        self.browser.getControl(name="form.p_category").value = ['schoolfee_incl']
684        self.browser.getControl("Create ticket").click()
685        self.assertTrue('ticket created' in self.browser.contents)
686        value = self.student['payments'].keys()[1]
687        self.student['payments'][value].p_state = 'paid'
688        self.browser.getLink(value).click()
689        self.assertTrue('Amount Authorized' in self.browser.contents)
690        # It's 1000 less
691        self.assertEqual(self.student['payments'][value].amount_auth, 236000.0)
692        return
693
694    def test_late_registration(self):
695        delta = timedelta(days=10)
696        self.app['configuration'][
697            '2004'].coursereg_deadline = datetime.now(pytz.utc) - delta
698        IWorkflowState(self.student).setState('school fee paid')
699        # Current session is 2004. Here we test course registration for
700        # returning students.
701        self.student['studycourse'].entry_session = 2003
702        self.student['studycourse'].current_session = 2016
703        # Login
704        self.browser.open(self.login_path)
705        self.browser.getControl(name="form.login").value = self.student_id
706        self.browser.getControl(name="form.password").value = 'spwd'
707        self.browser.getControl("Login").click()
708        # Make restitution fee payment
709        # Ivie: The restitution was only for returning students of 2016/2017.
710        # Hence, it is only valid for 2016 payment session returning students.
711        #configuration = createObject('waeup.SessionConfiguration')
712        #configuration.academic_session = 2016
713        #self.app['configuration'].addSessionConfiguration(configuration)
714        #self.app['configuration']['2016'].restitution_fee = 9999.0
715        #self.browser.open(self.payments_path + '/addop')
716        #self.browser.getControl(name="form.p_category").value = ['restitution']
717        #self.browser.getControl("Create ticket").click()
718        #self.student['payments'].values()[0].approveStudentPayment()
719        # Make late registration fee payment
720        self.student['studycourse'].current_session = 2004
721        self.browser.open(self.payments_path + '/addop')
722        self.browser.getControl(name="form.p_category").value = ['late_registration']
723        self.browser.getControl("Create ticket").click()
724        self.assertMatches('...ticket created...',
725                           self.browser.contents)
726        self.browser.getLink("Study Course").click()
727        self.browser.getLink("Add course list").click()
728        self.assertMatches('...Add current level 100 (Year 1)...',
729                           self.browser.contents)
730        self.browser.getControl("Create course list now").click()
731        self.student['studycourse']['100']['COURSE1'].score = 67
732        self.browser.getLink("100").click()
733        # Course results can't be seen
734        self.assertFalse('<td>67</td>' in self.browser.contents)
735        self.browser.getLink("Edit course list").click()
736        self.assertFalse('<td>67</td>' in self.browser.contents)
737        self.app['configuration']['2004'].late_registration_fee = 0.0
738        self.browser.getControl("Register course list").click()
739        self.assertTrue('Course registration has been disabled.' in self.browser.contents)
740        self.app['configuration']['2004'].late_registration_fee = 345.0
741        self.browser.getControl("Register course list").click()
742        self.assertTrue('Course registration has ended. Please pay' in self.browser.contents)
743        self.student['payments'].values()[0].approve()
744        self.browser.getControl("Register course list").click()
745        self.assertTrue('Course list has been registered' in self.browser.contents)
746        self.assertEqual(self.student.state, 'courses registered')
747        # Reset student and check if fresh students are always allowed to
748        # register courses.
749        #self.student['studycourse'].entry_session = 2004
750        #del self.student['payments'][self.student['payments'].keys()[1]]
751        #IWorkflowState(self.student).setState('school fee paid')
752        #self.browser.open(self.studycourse_path + '/100/edit')
753        #self.browser.getControl("Register course list").click()
754        #self.assertTrue('Course list has been registered' in self.browser.contents)
755        return
756
757
758    def test_student_course_registration(self):
759        # Add more courses
760        self.course2 = createObject('waeup.Course')
761        self.course2.code = 'COURSE2'
762        self.course2.semester = 2
763        self.course2.credits = 10
764        self.course2.passmark = 40
765        self.app['faculties']['fac1']['dep1'].courses.addCourse(
766            self.course2)
767        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
768            self.course2, level=100)
769        self.course3 = createObject('waeup.Course')
770        self.course3.code = 'COURSE3'
771        self.course3.semester = 3
772        self.course3.credits = 10
773        self.course3.passmark = 40
774        self.app['faculties']['fac1']['dep1'].courses.addCourse(
775            self.course3)
776        self.app['faculties']['fac1']['dep1'].certificates['CERT1'].addCertCourse(
777            self.course3, level=100)
778
779        # Login as student
780        self.browser.open(self.login_path)
781        IWorkflowState(self.student).setState('school fee paid')
782        self.browser.open(self.login_path)
783        self.browser.getControl(name="form.login").value = self.student_id
784        self.browser.getControl(name="form.password").value = 'spwd'
785        self.browser.getControl("Login").click()
786        # Students can add the current study level
787        self.browser.getLink("Study Course").click()
788        self.browser.getLink("Add course list").click()
789        self.assertMatches('...Add current level 100 (Year 1)...',
790                           self.browser.contents)
791        self.browser.getControl("Create course list now").click()
792        # Student has not paid second instalment, therefore a level
793        # with two course ticket was created (semester 1 and combined)
794        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 2)
795        self.browser.getLink("100").click()
796        self.browser.getLink("Edit course list").click()
797        self.browser.getControl("Add course ticket").click()
798        # Student can't add second semester course
799        self.assertTrue('<option value="COURSE1">' in self.browser.contents)
800        self.assertTrue('<option value="COURSE3">' in self.browser.contents)
801        self.assertFalse('<option value="COURSE2">' in self.browser.contents)
802
803        # Let's remove level and see what happens after 2nd instalment payment
804        del(self.student['studycourse']['100'])
805        payment2 = createObject('waeup.StudentOnlinePayment')
806        payment2.p_category = u'schoolfee_2'
807        payment2.p_session = self.student.current_session
808        self.student['payments']['anykey'] = payment2
809        self.browser.open(self.studycourse_path)
810        self.browser.getLink("Add course list").click()
811        self.browser.getControl("Create course list now").click()
812        # Still only 2 tickets have been created since payment ticket
813        # was not paid
814        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 2)
815        payment2.p_state = u'paid'
816        del(self.student['studycourse']['100'])
817        self.browser.open(self.studycourse_path)
818        self.browser.getLink("Add course list").click()
819        self.browser.getControl("Create course list now").click()
820        # Now 2nd semester course has been added
821        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 3)
822        # Student can add second semester course
823        self.browser.getLink("100").click()
824        self.browser.getLink("Edit course list").click()
825        self.browser.getControl("Add course ticket").click()
826        self.assertTrue('<option value="COURSE1">' in self.browser.contents)
827        self.assertTrue('<option value="COURSE2">' in self.browser.contents)
828        self.assertTrue('<option value="COURSE3">' in self.browser.contents)
829        return
830
831    def test_set_matric_number(self):
832        #payment = createObject('waeup.StudentOnlinePayment')
833        #payment.p_category = u'concessional'
834        #payment.p_id = u'anyid'
835        #payment.p_state = u'paid'
836        #self.student['payments']['anykey'] = payment
837        # Login as student
838        self.browser.open(self.login_path)
839        IWorkflowState(self.student).setState('school fee paid')
840        self.browser.open(self.login_path)
841        self.browser.getControl(name="form.login").value = self.student_id
842        self.browser.getControl(name="form.password").value = 'spwd'
843        self.browser.getControl("Login").click()
844        self.assertRaises(
845            LinkNotFoundError,
846            self.browser.getLink, 'Get Matriculation Number')
847        self.student.matric_number = None
848        site = grok.getSite()
849        site['configuration'].next_matric_integer = 1
850        self.student['studycourse'].certificate.study_mode = 'ug_pt'
851        self.browser.open(self.student_path)
852        self.assertRaises(
853            LinkNotFoundError,
854            self.browser.getLink, 'Download matriculation number slip')
855        self.browser.getLink("Get Matriculation Number").click()
856        self.assertTrue('Matriculation number PTP/fac1/dep1/04/00001 assigned.'
857            in self.browser.contents)
858        self.assertEqual(self.student.matric_number, 'PTP/fac1/dep1/04/00001')
859        self.assertRaises(
860            LinkNotFoundError,
861            self.browser.getLink, 'Get Matriculation Number')
862        # Setting matric number is logged.
863        logfile = os.path.join(
864            self.app['datacenter'].storage, 'logs', 'students.log')
865        logcontent = open(logfile).read()
866        self.assertTrue('E1000000 - waeup.aaue.students.browser.StudentGetMatricNumberPage - '
867                        'E1000000 - PTP/fac1/dep1/04/00001 assigned' in logcontent)
868        # Matric Number Slip can be downloaded
869        self.browser.getLink("Download matriculation number slip").click()
870        self.assertEqual(self.browser.headers['Status'], '200 Ok')
871        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
872        path = os.path.join(samples_dir(), 'matric_number_slip.pdf')
873        open(path, 'wb').write(self.browser.contents)
874        print "Sample PDF matric_number_slip.pdf written to %s" % path
875        return
876
877    def test_personal_data_slip(self):
878        # Login as student
879        self.browser.open(self.login_path)
880        IWorkflowState(self.student).setState('school fee paid')
881        self.browser.open(self.login_path)
882        self.browser.getControl(name="form.login").value = self.student_id
883        self.browser.getControl(name="form.password").value = 'spwd'
884        self.browser.getControl("Login").click()
885        self.browser.getLink("Personal Data").click()
886        self.assertRaises(
887            LinkNotFoundError,
888            self.browser.getLink, 'Download personal data slip')
889        self.student.father_name = u'Rudolf'
890        self.browser.open(self.personal_path)
891        self.browser.getLink("Download personal data slip").click()
892        self.assertEqual(self.browser.headers['Status'], '200 Ok')
893        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
894        path = os.path.join(samples_dir(), 'personal_data_slip.pdf')
895        open(path, 'wb').write(self.browser.contents)
896        print "Sample PDF personal_data_slip.pdf written to %s" % path
897        return
898
899    def test_student_course_registration(self):
900        IWorkflowState(self.student).setState('school fee paid')
901        self.browser.open(self.login_path)
902        self.browser.getControl(name="form.login").value = self.student_id
903        self.browser.getControl(name="form.password").value = 'spwd'
904        self.browser.getControl("Login").click()
905        # Now students can add the current study level
906        self.browser.getLink("Study Course").click()
907        self.browser.getLink("Add course list").click()
908        self.assertMatches('...Add current level 100 (Year 1)...',
909                           self.browser.contents)
910        self.browser.getControl("Create course list now").click()
911        # Students can't open the customized pdf course registration slip
912        self.browser.open(
913            self.student_path + '/studycourse/100/course_registration_slip.pdf')
914        self.assertTrue('Forbidden' in self.browser.contents)
915        # They can open slips from the previous session ...
916        self.student['studycourse'].current_level = 200
917        self.browser.open(self.student_path + '/studycourse/100')
918        self.browser.getLink("Download course registration slip").click()
919        self.assertEqual(self.browser.headers['Status'], '200 Ok')
920        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
921        # or if they have registered their course list
922        self.student['studycourse'].current_level = 200
923        IWorkflowState(self.student).setState('courses registered')
924        self.browser.open(self.student_path + '/studycourse/100')
925        self.browser.getLink("Download course registration slip").click()
926        self.assertEqual(self.browser.headers['Status'], '200 Ok')
927        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
928        path = os.path.join(samples_dir(), 'ft_course_registration_slip.pdf')
929        open(path, 'wb').write(self.browser.contents)
930        print "Sample PDF ft_course_registration_slip.pdf written to %s" % path
931
932        self.certificate.study_mode = 'ug_pt'
933        self.browser.open(self.student_path + '/studycourse/100')
934        self.browser.getLink("Download course registration slip").click()
935        self.assertEqual(self.browser.headers['Status'], '200 Ok')
936        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
937        path = os.path.join(samples_dir(), 'pt_course_registration_slip.pdf')
938        open(path, 'wb').write(self.browser.contents)
939        print "Sample PDF pt_course_registration_slip.pdf written to %s" % path
940
941        self.certificate.study_mode = 'special_pg_ft'
942        self.student.matric_number = u'AAU/SPS/FLW/LAW/15/PHD/09504'
943        self.browser.open(self.student_path + '/studycourse/100')
944        self.browser.getLink("Download course registration slip").click()
945        self.assertEqual(self.browser.headers['Status'], '200 Ok')
946        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
947        path = os.path.join(samples_dir(), 'pg_course_registration_slip.pdf')
948        open(path, 'wb').write(self.browser.contents)
949        print "Sample PDF pg_course_registration_slip.pdf written to %s" % path
950
951        # Students cant' view scores, cas and grades.
952        self.browser.open(self.student_path + '/studycourse/100')
953        self.assertFalse('Score' in self.browser.contents)
954        self.assertFalse('CA' in self.browser.contents)
955        self.assertFalse('Grade' in self.browser.contents)
956        self.browser.getLink("COURSE1").click()
957        self.browser.open(self.student_path + '/studycourse/100')
958        self.assertFalse('Score' in self.browser.contents)
959        self.assertFalse('CA' in self.browser.contents)
960        self.assertFalse('Grade' in self.browser.contents)
961
962    def test_student_2nd_semester_course_registration(self):
963        IWorkflowState(self.student).setState('school fee paid')
964        self.student['studycourse'].entry_session = 2015
965        self.course.semester = 2
966        self.browser.open(self.login_path)
967        self.browser.getControl(name="form.login").value = self.student_id
968        self.browser.getControl(name="form.password").value = 'spwd'
969        self.browser.getControl("Login").click()
970        self.browser.getLink("Study Course").click()
971        self.browser.getLink("Add course list").click()
972        self.browser.getControl("Create course list now").click()
973        self.assertFalse('COURSE1' in self.browser.contents)
974        # 2nd semester tickets can't be added manually
975        self.browser.getLink("Edit course list").click()
976        self.browser.getLink("here").click()
977        self.browser.getControl(name="form.course").value = ['COURSE1']
978        self.browser.getControl("Add course ticket").click()
979        self.assertTrue(
980            'COURSE1 is a 2nd semester course which can only '
981            'be added if school fees have been fully paid.'
982            in self.browser.contents)
983        # 2nd instalment has to be paid first
984        #self.certificate.school_fee_3 = 678.0
985        self.browser.open(self.payments_path + '/addop')
986        self.browser.getControl(name="form.p_category").value = ['schoolfee_2']
987        self.browser.getControl("Create ticket").click()
988        self.student['payments'].values()[0].approve()
989        self.browser.open(self.studycourse_path + '/100/ctadd')
990        self.browser.getControl(name="form.course").value = ['COURSE1']
991        self.browser.getControl("Add course ticket").click()
992        self.assertTrue('Successfully added COURSE1' in self.browser.contents)
993        return
994
995    def test_student_GST_registration(self):
996        configuration_1 = createObject('waeup.SessionConfiguration')
997        configuration_1.academic_session = 2016
998        configuration_1.gst_registration_1_fee = 3333.0
999        configuration_1.gst_text_book_1_fee = 4444.0
1000        configuration_1.gst_text_book_0_fee = 2222.0
1001        self.app['configuration'].addSessionConfiguration(configuration_1)
1002        course = createObject('waeup.Course')
1003        course.code = 'GST101'
1004        course.semester = 1
1005        course.credits = 10
1006        course.passmark = 40
1007        self.app['faculties']['fac1']['dep1'].courses.addCourse(
1008            course)
1009        self.app['faculties']['fac1']['dep1'].certificates[
1010            'CERT1'].addCertCourse(course, level=100)
1011        IWorkflowState(self.student).setState('school fee paid')
1012        self.student['studycourse'].entry_session = 2016
1013        self.student['studycourse'].current_session = 2016
1014        self.course.semester = 2
1015        self.browser.open(self.login_path)
1016        self.browser.getControl(name="form.login").value = self.student_id
1017        self.browser.getControl(name="form.password").value = 'spwd'
1018        self.browser.getControl("Login").click()
1019        self.browser.getLink("Study Course").click()
1020        self.browser.getLink("Add course list").click()
1021        self.browser.getControl("Create course list now").click()
1022        self.assertFalse('GST101' in self.browser.contents)
1023        # GST101 tickets can't be added manually
1024        self.browser.getLink("Edit course list").click()
1025        self.browser.getLink("here").click()
1026        self.browser.getControl(name="form.course").value = ['GST101']
1027        self.browser.getControl("Add course ticket").click()
1028        self.assertTrue(
1029            'GST101 can only be added if both registration fee and text'
1030            in self.browser.contents)
1031        # GST fees have to be paid first
1032        self.browser.open(self.payments_path + '/addop')
1033        self.browser.getControl(name="form.p_category").value = ['gst_registration_1']
1034        self.browser.getControl("Create ticket").click()
1035        self.student['payments'].values()[0].approve()
1036        self.browser.open(self.studycourse_path + '/100/ctadd')
1037        self.browser.getControl(name="form.course").value = ['GST101']
1038        self.browser.getControl("Add course ticket").click()
1039        self.assertTrue(
1040            'GST101 can only be added if both registration fee and text'
1041            in self.browser.contents)
1042        self.browser.open(self.payments_path + '/addop')
1043        self.browser.getControl(name="form.p_category").value = ['gst_text_book_0']
1044        self.browser.getControl("Create ticket").click()
1045        self.student['payments'].values()[1].approve()
1046        self.browser.open(self.studycourse_path + '/100/ctadd')
1047        self.browser.getControl(name="form.course").value = ['GST101']
1048        self.browser.getControl("Add course ticket").click()
1049        self.assertTrue('Successfully added GST101' in self.browser.contents)
1050        return
1051
1052    def test_course_registration_forbidden(self):
1053        IWorkflowState(self.student).setState('school fee paid')
1054        self.student['studycourse'].entry_session = 2016
1055        self.student['studycourse'].current_session = 2016
1056        self.browser.open(self.login_path)
1057        self.browser.getControl(name="form.login").value = self.student_id
1058        self.browser.getControl(name="form.password").value = 'spwd'
1059        self.browser.getControl("Login").click()
1060        self.browser.getLink("Study Course").click()
1061        self.browser.getLink("Add course list").click()
1062        self.browser.getControl("Create course list now").click()
1063        self.browser.getLink("Edit course list").click()
1064        self.browser.getControl("Register course list").click()
1065        #self.assertTrue('Please pay faculty and departmental dues first'
1066        #    in self.browser.contents)
1067        #configuration_1 = createObject('waeup.SessionConfiguration')
1068        #configuration_1.academic_session = 2016
1069        #configuration_1.fac_dep_fee = 9999.0
1070        #self.app['configuration'].addSessionConfiguration(configuration_1)
1071        #self.browser.open(self.payments_path + '/addop')
1072        #self.browser.getControl(name="form.p_category").value = ['fac_dep']
1073        #self.browser.getControl("Create ticket").click()
1074        #self.student['payments'].values()[0].approveStudentPayment()
1075        #self.browser.open(self.studycourse_path + '/100/edit')
1076        #self.browser.getControl("Register course list").click()
1077
1078        ######################################################
1079        # Temporarily disabled ug_ft course registration
1080        #self.assertTrue('Course registration has been disabled'
1081        #    in self.browser.contents)
1082        #return
1083        ######################################################
1084
1085        self.assertTrue('Course list has been registered'
1086            in self.browser.contents)
1087        return
1088
1089    def deactivated_test_course_registration_forbidden_2(self):
1090        IWorkflowState(self.student).setState('school fee paid')
1091        self.student['studycourse'].entry_session = 2004
1092        self.student['studycourse'].current_session = 2016
1093        self.browser.open(self.login_path)
1094        self.browser.getControl(name="form.login").value = self.student_id
1095        self.browser.getControl(name="form.password").value = 'spwd'
1096        self.browser.getControl("Login").click()
1097        self.browser.getLink("Study Course").click()
1098        self.browser.getLink("Add course list").click()
1099        self.browser.getControl("Create course list now").click()
1100        self.browser.getLink("Edit course list").click()
1101        self.browser.getControl("Register course list").click()
1102
1103        ######################################################
1104        # Temporarily disabled ug_ft course registration
1105        #self.assertTrue('Course registration has been disabled'
1106        #    in self.browser.contents)
1107        #return
1108        ######################################################
1109
1110        self.assertTrue('Please pay restitution fee first'
1111            in self.browser.contents)
1112        configuration = createObject('waeup.SessionConfiguration')
1113        configuration.academic_session = 2016
1114        self.app['configuration'].addSessionConfiguration(configuration)
1115        self.app['configuration']['2016'].restitution_fee = 9999.0
1116        self.browser.open(self.payments_path + '/addop')
1117        self.browser.getControl(name="form.p_category").value = ['restitution']
1118        self.browser.getControl("Create ticket").click()
1119        self.student['payments'].values()[0].approveStudentPayment()
1120        self.browser.open(self.studycourse_path + '/100/edit')
1121        self.browser.getControl("Register course list").click()
1122        self.assertTrue('Course list has been registered'
1123            in self.browser.contents)
1124        return
1125
1126    def test_student_access_course_results(self):
1127        IWorkflowState(self.student).setState('school fee paid')
1128        self.student['studycourse'].entry_session = 2004
1129        self.student['studycourse'].current_session = 2004
1130        self.browser.open(self.login_path)
1131        self.browser.getControl(name="form.login").value = self.student_id
1132        self.browser.getControl(name="form.password").value = 'spwd'
1133        self.browser.getControl("Login").click()
1134        self.browser.getLink("Study Course").click()
1135        self.browser.getLink("Add course list").click()
1136        self.browser.getControl("Create course list now").click()
1137        self.assertFalse('Score' in self.browser.contents)
1138        IWorkflowState(self.student).setState('returning')
1139        self.browser.open(self.studycourse_path + '/100')
1140        self.assertFalse('Score' in self.browser.contents)
1141        self.app['configuration']['2004'].show_results = ['ug_ft']
1142        self.browser.open(self.studycourse_path + '/100')
1143        self.assertTrue('Score' in self.browser.contents)
1144        return
1145
1146    def test_repair_course_list(self):
1147        IWorkflowState(self.student).setState('school fee paid')
1148        self.student['studycourse'].entry_session = 2016
1149        self.student['studycourse'].current_session = 2016
1150        self.browser.open(self.login_path)
1151        self.browser.getControl(name="form.login").value = self.student_id
1152        self.browser.getControl(name="form.password").value = 'spwd'
1153        self.browser.getControl("Login").click()
1154        self.browser.getLink("Study Course").click()
1155        self.browser.getLink("Add course list").click()
1156        self.browser.getControl("Create course list now").click()
1157        self.assertTrue('Edit course list' in self.browser.contents)
1158        self.assertFalse('Repair course list' in self.browser.contents)
1159        self.student['studycourse'].current_level = 200
1160        self.browser.open(self.studycourse_path + '/100')
1161        self.assertFalse('Edit course list' in self.browser.contents)
1162        self.assertFalse('Repair course list' in self.browser.contents)
1163        configuration = createObject('waeup.SessionConfiguration')
1164        configuration.academic_session = 2016
1165        self.app['configuration'].addSessionConfiguration(configuration)
1166        self.app['configuration']['2016'].studylevel_repair_enabled = True
1167        self.browser.open(self.studycourse_path + '/100')
1168        self.assertFalse('Edit course list' in self.browser.contents)
1169        self.assertTrue('Repair course list' in self.browser.contents)
1170        self.browser.getLink("Repair").click()
1171        self.assertEqual(self.browser.url, self.studycourse_path + '/100/repair')
1172        self.assertTrue('Repair course list of 100' in self.browser.contents)
1173        return
1174
1175    def test_student_clearance(self):
1176        # Student cant login if their password is not set
1177        IWorkflowInfo(self.student).fireTransition('admit')
1178        self.browser.open(self.login_path)
1179        self.browser.getControl(name="form.login").value = self.student_id
1180        self.browser.getControl(name="form.password").value = 'spwd'
1181        self.browser.getControl("Login").click()
1182        self.assertMatches(
1183            '...You logged in...', self.browser.contents)
1184        # Admitted student can upload a passport picture
1185        self.browser.open(self.student_path + '/change_portrait')
1186        ctrl = self.browser.getControl(name='passportuploadedit')
1187        file_obj = open(SAMPLE_IMAGE, 'rb')
1188        file_ctrl = ctrl.mech_control
1189        file_ctrl.add_file(file_obj, filename='my_photo.jpg')
1190        self.browser.getControl(
1191            name='upload_passportuploadedit').click()
1192        self.assertTrue(
1193            'src="http://localhost/app/students/E1000000/passport.jpg"'
1194            in self.browser.contents)
1195        # Student is redirected to the personal data form because
1196        # personal data form is not properly filled.
1197        self.browser.open(self.student_path + '/start_clearance')
1198        self.assertMatches('...Personal data form is not properly filled...',
1199                           self.browser.contents)
1200        self.assertEqual(self.browser.url, self.student_path + '/edit_personal')
1201        self.student.father_name = u'Rudolf'
1202        self.browser.open(self.student_path + '/start_clearance')
1203        self.assertMatches(
1204            '...<h1 class="kofa-content-label">Start clearance</h1>...',
1205            self.browser.contents)
1206
1207    def test_student_accommodation(self):
1208        del self.student['accommodation']['2004']
1209        self.student['studycourse'].certificate.study_mode = 'dp_ft'
1210        # All beds can be assigned
1211        bed1 = self.app['hostels']['hall-1']['hall-1_A_101_A']
1212        bed1.bed_type = u'regular_male_all'
1213        bed2 = self.app['hostels']['hall-1']['hall-1_A_101_B']
1214        bed2.bed_type = u'regular_female_all'
1215        notify(grok.ObjectModifiedEvent(bed1))
1216        notify(grok.ObjectModifiedEvent(bed2))
1217        # Login
1218        self.browser.open(self.login_path)
1219        self.browser.getControl(name="form.login").value = self.student_id
1220        self.browser.getControl(name="form.password").value = 'spwd'
1221        self.browser.getControl("Login").click()
1222        self.browser.open(self.acco_path)
1223        IWorkflowInfo(self.student).fireTransition('admit')
1224        self.browser.getControl("Book accommodation").click()
1225        self.assertTrue(
1226            'You are not eligible to book accommodation.'
1227            in self.browser.contents)
1228        self.student['studycourse'].certificate.study_mode = 'ug_ft'
1229        self.app['hostels'].accommodation_states = [PAID]
1230        self.browser.getControl("Book accommodation").click()
1231        self.assertTrue(
1232            'You are in the wrong registration state.'
1233            in self.browser.contents)
1234        IWorkflowState(self.student).setState(PAID)
1235
1236        self.browser.getLink("Payments").click()
1237        self.browser.getLink("Add current session payment ticket").click()
1238        self.browser.getControl(name="form.p_category").value = ['bed_allocation']
1239        self.browser.getControl("Create ticket").click()
1240        self.student['payments'].values()[0].approveStudentPayment()
1241        pin = self.app['accesscodes']['HOS-0'].keys()[0]
1242        ac = self.app['accesscodes']['HOS-0'][pin]
1243        parts = pin.split('-')[1:]
1244        sfeseries, sfenumber = parts
1245        self.browser.open(self.acco_path)
1246        self.browser.getControl("Book accommodation").click()
1247        #self.assertFalse('Activation Code:' in self.browser.contents)
1248
1249        self.assertMatches('...Activation Code:...',
1250                           self.browser.contents)
1251        self.browser.getControl(name="ac_series").value = sfeseries
1252        self.browser.getControl(name="ac_number").value = sfenumber
1253        self.browser.getControl("Create bed ticket").click()
1254        # Bed is randomly selected but, since there is only
1255        # one bed for this student, we know that
1256        self.assertEqual(self.student['accommodation']['2004'].bed_coordinates,
1257            'Hall 1, Block A, Room 101, Bed A (regular_male_all)')
1258        # Only the hall name is displayed
1259        self.assertEqual(self.student[
1260            'accommodation']['2004'].display_coordinates,
1261            'Hall 1')
1262        self.assertFalse('Hall 1, Block A, Room 101, Bed A'
1263            in self.browser.contents)
1264        self.assertTrue('<td>Hall 1</td>'
1265            in self.browser.contents)
1266        return
1267
1268    def test_handle_courses_by_lecturer(self):
1269        self.app['users'].addUser('mrslecturer', 'mrslecturerSecret1')
1270        self.app['users']['mrslecturer'].email = 'mrslecturer@foo.ng'
1271        self.app['users']['mrslecturer'].title = u'Mercedes Benz'
1272        # Add course ticket
1273        studylevel = createObject(u'waeup.StudentStudyLevel')
1274        studylevel.level = 100
1275        studylevel.level_session = 2004
1276        self.student['studycourse'].addStudentStudyLevel(
1277            self.certificate, studylevel)
1278        # Assign local Lecturer role for a certificate.
1279        course = self.app['faculties']['fac1']['dep1'].courses['COURSE1']
1280        prmlocal = IPrincipalRoleManager(course)
1281        prmlocal.assignRoleToPrincipal('waeup.local.Lecturer', 'mrslecturer')
1282        notify(LocalRoleSetEvent(
1283            course, 'waeup.local.Lecturer', 'mrslecturer', granted=True))
1284        # Login as lecturer.
1285        self.browser.open(self.login_path)
1286        self.browser.getControl(name="form.login").value = 'mrslecturer'
1287        self.browser.getControl(name="form.password").value = 'mrslecturerSecret1'
1288        self.browser.getControl("Login").click()
1289        self.browser.getLink("My Courses").click()
1290        self.browser.getLink("COURSE1").click()
1291        # Course results can be batch edited via the edit_courses view.
1292        self.app['faculties']['fac1']['dep1'].score_editing_disabled = False
1293        self.app['configuration'].current_academic_session = 2004
1294        self.app['configuration']['2004'].score_editing_enabled = ['ug_ft']
1295        IWorkflowState(self.student).setState('courses validated')
1296        self.browser.open(
1297            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1298        self.assertTrue(
1299            'input type="text" name="scores:list"'
1300            in self.browser.contents)
1301        self.browser.getControl(name="scores:list", index=0).value = '55'
1302        self.browser.getControl(name="cas:list", index=0).value = '22'
1303        self.browser.getControl("Update scores from").click()
1304        # New score and ca has been set.
1305        self.assertEqual(
1306            self.student['studycourse']['100']['COURSE1'].score, 55)
1307        self.assertEqual(
1308            self.student['studycourse']['100']['COURSE1'].ca, 22)
1309        # Score editing has been logged.
1310        logfile = os.path.join(
1311            self.app['datacenter'].storage, 'logs', 'students.log')
1312        logcontent = open(logfile).read()
1313        self.assertTrue('mrslecturer - waeup.aaue.students.browser.CustomEditScoresPage - '
1314                        'E1000000 100/COURSE1 score updated (55)' in logcontent)
1315        self.assertTrue('mrslecturer - waeup.aaue.students.browser.CustomEditScoresPage - '
1316                        'E1000000 100/COURSE1 ca updated (22)' in logcontent)
1317        # Non-integer scores won't be accepted.
1318        self.browser.open(
1319            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1320        self.assertTrue('value="55" />' in self.browser.contents)
1321        self.browser.getControl(name="scores:list", index=0).value = 'abc'
1322        self.browser.getControl("Update scores").click()
1323        self.assertTrue('Error: Score(s), CA(s) and Imported TS(s) of TESTER, Anna have not be updated.'
1324            in self.browser.contents)
1325        # Scores can be removed.
1326        self.browser.open(
1327            "http://localhost/app/faculties/fac1/dep1/courses/COURSE1/edit_scores")
1328        self.browser.getControl(name="scores:list", index=0).value = ''
1329        self.browser.getControl("Update scores").click()
1330        self.assertEqual(
1331            self.student['studycourse']['100']['COURSE1'].score, None)
1332        logcontent = open(logfile).read()
1333        self.assertTrue('mrslecturer - waeup.aaue.students.browser.CustomEditScoresPage - '
1334                        'E1000000 100/COURSE1 score updated (None)' in logcontent)
1335
1336
1337    def disabled_test_student_view_transcript(self):
1338        # Student cant login if their password is not set
1339        IWorkflowInfo(self.student).fireTransition('admit')
1340        self.browser.open(self.login_path)
1341        self.browser.getControl(name="form.login").value = self.student_id
1342        self.browser.getControl(name="form.password").value = 'spwd'
1343        self.browser.getControl("Login").click()
1344        self.assertMatches(
1345            '...You logged in...', self.browser.contents)
1346        # Students can view the transcript
1347        self.browser.open(self.studycourse_path)
1348        self.browser.getLink("Transcript").click()
1349        self.browser.getLink("Academic Transcript").click()
1350        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1351        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
1352
1353    def test_alumni_request_pw(self):
1354        # Add an applicants container
1355        applicantscontainer = ApplicantsContainer()
1356        applicantscontainer.code = u'trans2017'
1357        applicantscontainer.prefix = 'trans'
1358        applicantscontainer.year = 2017
1359        applicantscontainer.title = u'This is the trans2017 container'
1360        applicantscontainer.application_category = 'no'
1361        applicantscontainer.mode = 'create'
1362        applicantscontainer.strict_deadline = True
1363        delta = timedelta(days=10)
1364        applicantscontainer.startdate = datetime.now(pytz.utc) - delta
1365        applicantscontainer.enddate = datetime.now(pytz.utc) + delta
1366        self.app['applicants']['trans2017'] = applicantscontainer
1367        self.applicantscontainer = self.app['applicants']['trans2017']
1368        # Student with wrong number can't be found.
1369        # Applicant is redirected to application section.
1370        self.browser.open('http://localhost/app/alumni_requestpw')
1371        self.browser.getControl(name="form.lastname").value = 'Tester'
1372        self.browser.getControl(name="form.number").value = 'anynumber'
1373        self.browser.getControl(name="form.email").value = 'xx@yy.zz'
1374        self.browser.getControl("Send login credentials").click()
1375        self.assertTrue('No student record found.'
1376            in self.browser.contents)
1377        self.assertEqual(self.browser.url,
1378            'http://localhost/app/applicants/trans2017/register')
1379
1380    def test_student_course_registration_outstanding(self):
1381        self.course = createObject('waeup.Course')
1382        self.course.code = 'COURSE2'
1383        self.course.semester = 1
1384        self.course.credits = 39
1385        self.course.passmark = 40
1386        self.app['faculties']['fac1']['dep1'].courses.addCourse(
1387            self.course)
1388        IWorkflowState(self.student).setState('school fee paid')
1389        self.browser.open(self.login_path)
1390        self.browser.getControl(name="form.login").value = self.student_id
1391        self.browser.getControl(name="form.password").value = 'spwd'
1392        self.browser.getControl("Login").click()
1393        self.browser.open(self.student_path + '/studycourse/add')
1394        self.browser.getControl("Create course list now").click()
1395        self.assertEqual(self.student['studycourse']['100'].number_of_tickets, 1)
1396        self.student['studycourse'].current_level = 200
1397        self.browser.getLink("Study Course").click()
1398        self.browser.getLink("Add course list").click()
1399        self.assertMatches('...Add current level 200 (Year 2)...',
1400                           self.browser.contents)
1401        self.browser.getControl("Create course list now").click()
1402        self.browser.getLink("200").click()
1403        self.browser.getLink("Edit course list").click()
1404        self.browser.getLink("here").click()
1405        self.browser.getControl(name="form.course").value = ['COURSE2']
1406        self.browser.getControl("Add course ticket").click()
1407        # Carryover COURSE1 in level 200 already has 10 credits
1408        self.assertMatches(
1409            '...Maximum credits exceeded...', self.browser.contents)
1410        # If COURSE1 is outstanding, its credits won't be considered
1411        self.student['studycourse']['200']['COURSE1'].outstanding = True
1412        self.browser.getControl("Add course ticket").click()
1413        # Corresponding certificate course is missing
1414        self.assertTrue(
1415            'COURSE2 is not part of the CERT1 curriculum.'
1416            in self.browser.contents)
1417        self.app['faculties']['fac1']['dep1'].certificates[
1418            'CERT1'].addCertCourse(self.course, level=100)
1419        self.browser.getControl("Add course ticket").click()
1420        self.assertTrue(
1421            'Successfully added COURSE2' in self.browser.contents)
1422        return
1423
1424    def test_examination_schedule_slip(self):
1425        self.student.flash_notice = u'My Examination Date'
1426        self.browser.open(self.login_path)
1427        self.browser.getControl(name="form.login").value = self.student_id
1428        self.browser.getControl(name="form.password").value = 'spwd'
1429        self.browser.getControl("Login").click()
1430        self.browser.open(self.student_path)
1431        self.browser.getLink("Download examination schedule slip").click()
1432        self.assertEqual(self.browser.headers['Status'], '200 Ok')
1433        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
1434        path = os.path.join(samples_dir(), 'examination_schedule_slip.pdf')
1435        open(path, 'wb').write(self.browser.contents)
1436        print "Sample PDF examination_schedule_slip.pdf written to %s" % path
1437        # If flash_notice does not contain exam' the button does not show up.
1438        self.student.flash_notice = u'anything'
1439        self.browser.open(self.student_path)
1440        self.assertFalse('examination schedule slip' in self.browser.contents)
Note: See TracBrowser for help on using the repository browser.