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

Last change on this file since 17497 was 17494, checked in by Henrik Bettermann, 15 months ago

100_ now indicates first year only payments.

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