source: main/waeup.fceokene/trunk/src/waeup/fceokene/students/tests/test_browser.py

Last change on this file was 17734, checked in by Henrik Bettermann, 6 months ago

School fee payments are becoming even more complex.

  • Property svn:keywords set to Id
File size: 23.8 KB
RevLine 
[7419]1## $Id: test_browser.py 17734 2024-04-04 12:19:05Z 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##
[7604]18import os
19import shutil
20import tempfile
[8722]21from StringIO import StringIO
[9156]22from hurry.workflow.interfaces import IWorkflowState, IWorkflowInfo
[7604]23from zope.component.hooks import setSite, clearSite
[8326]24from zope.component import getUtility, createObject
25from zope.interface import verify
[7822]26from waeup.kofa.app import University
[9156]27from waeup.kofa.students.tests.test_browser import (
28    StudentsFullSetup, SAMPLE_IMAGE)
[9434]29from waeup.kofa.students.accommodation import BedTicket
[7822]30from waeup.kofa.testing import FunctionalTestCase
[14591]31from waeup.kofa.browser.tests.test_pdf import samples_dir
[8722]32from waeup.kofa.interfaces import (
33    IExtFileStore, IFileStoreNameChooser)
[8320]34from waeup.kofa.students.interfaces import IStudentsUtils
[8460]35from waeup.fceokene.testing import FunctionalLayer
[6902]36
[8320]37
[7934]38class StudentProcessorTest(FunctionalTestCase):
[7604]39    """Perform some batching tests.
40    """
41
42    layer = FunctionalLayer
43
44    def setUp(self):
[7934]45        super(StudentProcessorTest, self).setUp()
[7604]46        # Setup a sample site for each test
47        app = University()
48        self.dc_root = tempfile.mkdtemp()
49        app['datacenter'].setStoragePath(self.dc_root)
50
51        # Prepopulate the ZODB...
52        self.getRootFolder()['app'] = app
53        # we add the site immediately after creation to the
54        # ZODB. Catalogs and other local utilities are not setup
55        # before that step.
56        self.app = self.getRootFolder()['app']
57        # Set site here. Some of the following setup code might need
58        # to access grok.getSite() and should get our new app then
59        setSite(app)
60
61
62    def tearDown(self):
[7934]63        super(StudentProcessorTest, self).tearDown()
[7604]64        shutil.rmtree(self.workdir)
65        shutil.rmtree(self.dc_root)
66        clearSite()
67        return
68
[6902]69class StudentUITests(StudentsFullSetup):
[7604]70    """Tests for customized student class views and pages
71    """
[6902]72
73    layer = FunctionalLayer
74
[9434]75    def setUp(self):
76        super(StudentUITests, self).setUp()
77
78        bedticket = BedTicket()
79        bedticket.booking_session = 2004
80        bedticket.bed_type = u'any bed type'
81        bedticket.bed = self.app['hostels']['hall-1']['hall-1_A_101_A']
82        bedticket.bed_coordinates = u'My bed coordinates'
83        self.student['accommodation'].addBedTicket(bedticket)
84
[6902]85    def test_manage_payments(self):
[7021]86        # Add missing configuration data
[9143]87        self.app['configuration']['2004'].clearance_fee = 120.0
[8294]88        self.app['configuration']['2004'].booking_fee = 150.0
[7928]89        self.app['configuration']['2004'].maint_fee = 180.0
[6925]90
[6926]91        # Managers can add online payment tickets
92        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
[7995]93        self.browser.open(self.payments_path)
[9525]94        self.browser.getLink("Add current session payment ticket").click()
[9753]95        self.browser.getControl(name="form.p_category").value = ['schoolfee']
[6926]96        self.browser.getControl("Create ticket").click()
[10012]97        self.assertMatches('...Wrong state...',
[6902]98                           self.browser.contents)
[7886]99        IWorkflowState(self.student).setState('cleared')
[7995]100        self.browser.open(self.payments_path + '/addop')
[9753]101        self.browser.getControl(name="form.p_category").value = ['schoolfee']
[6902]102        self.browser.getControl("Create ticket").click()
103        self.assertMatches('...ticket created...',
104                           self.browser.contents)
[16365]105        self.browser.open(self.payments_path)
[7419]106        ctrl = self.browser.getControl(name='val_id')
107        value = ctrl.options[0]
108        self.browser.getLink(value).click()
109        self.assertMatches('...Amount Authorized...',
110                           self.browser.contents)
[15827]111        # Managers can open payment slip because we did not proceed to
112        # any payment gateway
113        self.assertFalse('Download payment slip' in self.browser.contents)
[7891]114        # Set ticket paid
115        ticket = self.student['payments'].items()[0][1]
116        ticket.p_state = 'paid'
[7995]117        self.browser.open(self.payments_path + '/addop')
[9753]118        self.browser.getControl(name="form.p_category").value = ['schoolfee']
[7889]119        self.browser.getControl("Create ticket").click()
[7419]120        self.assertMatches('...This type of payment has already been made...',
[7146]121                           self.browser.contents)
122        # Remove all payments so that we can add a school fee payment again
[7889]123        keys = [i for i in self.student['payments'].keys()]
124        for payment in keys:
[7146]125            del self.student['payments'][payment]
[7995]126        self.browser.open(self.payments_path + '/addop')
[9753]127        self.browser.getControl(name="form.p_category").value = ['schoolfee']
[7146]128        self.browser.getControl("Create ticket").click()
[6925]129        self.assertMatches('...ticket created...',
130                           self.browser.contents)
[16899]131        #self.certificate.study_mode = 'nce_sw'
132        #self.browser.open(self.payments_path + '/addop')
133        #self.browser.getControl(name="form.p_category").value = ['third_semester']
134        #self.browser.getControl("Create ticket").click()
135        #self.assertMatches('...could not be determined...',
136        #                   self.browser.contents)
137        #self.certificate.study_mode = 'nce_ft'
138        #self.browser.open(self.payments_path + '/addop')
139        #self.browser.getControl(name="form.p_category").value = ['third_semester']
140        #self.student['studycourse'].current_level = 300
141        #self.browser.getControl("Create ticket").click()
142        #self.assertMatches('...Amount could not be determined...', self.browser.contents)
[7995]143        self.browser.open(self.payments_path + '/addop')
[7021]144        self.browser.getControl(
145            name="form.p_category").value = ['bed_allocation']
[6926]146        self.browser.getControl("Create ticket").click()
[8294]147        self.assertMatches('...ticket created...',
148                           self.browser.contents)
[7995]149        self.browser.open(self.payments_path + '/addop')
[7021]150        self.browser.getControl(
151            name="form.p_category").value = ['hostel_maintenance']
152        self.browser.getControl("Create ticket").click()
[8294]153        self.assertMatches('...ticket created...',
154                           self.browser.contents)
[7995]155        self.browser.open(self.payments_path + '/addop')
[6950]156        self.browser.getControl(name="form.p_category").value = ['clearance']
157        self.browser.getControl("Create ticket").click()
[8294]158        self.assertMatches('...ticket created...',
159                           self.browser.contents)
[13022]160        self.certificate.study_mode = 'pd_ft'
[7995]161        self.browser.open(self.payments_path + '/addop')
[6926]162        self.browser.getControl(name="form.p_category").value = ['schoolfee']
163        self.browser.getControl("Create ticket").click()
[8306]164        self.assertMatches('...ticket created...',
165                           self.browser.contents)
166        # In state returning we can add a new school fee ticket since
167        # p_session and p_level is different
168        IWorkflowState(self.student).setState('returning')
169        self.browser.open(self.payments_path + '/addop')
170        self.browser.getControl(name="form.p_category").value = ['schoolfee']
171        self.browser.getControl("Create ticket").click()
[9525]172        # Uups, we forgot to add a session configuration for next session
173        self.assertTrue('Session configuration object is not available.'
174            in self.browser.contents)
175        configuration = createObject('waeup.SessionConfiguration')
176        configuration.academic_session = 2005
177        self.app['configuration'].addSessionConfiguration(configuration)
[9753]178        self.browser.getControl(name="form.p_category").value = ['schoolfee']
[9525]179        self.browser.getControl("Create ticket").click()
[8306]180        self.assertMatches('...ticket created...',
181                           self.browser.contents)
[9525]182
[8320]183        # In state admitted school fee can't be determined
[8306]184        IWorkflowState(self.student).setState('admitted')
185        self.browser.open(self.payments_path + '/addop')
186        self.browser.getControl(name="form.p_category").value = ['schoolfee']
187        self.browser.getControl("Create ticket").click()
[10012]188        self.assertMatches('...Wrong state...',
[6926]189                           self.browser.contents)
190
[9525]191    def test_student_payments(self):
192        # Login
193        IWorkflowState(self.student).setState('returning')
194        self.browser.open(self.login_path)
195        self.browser.getControl(name="form.login").value = self.student_id
196        self.browser.getControl(name="form.password").value = 'spwd'
197        self.browser.getControl("Login").click()
198        self.browser.open(self.student_path + '/payments')
199        self.assertTrue(
200          'Add current session payment ticket' in self.browser.contents)
201        self.assertFalse(
202          'Add previous session payment ticket' in self.browser.contents)
203        return
[8267]204
[8320]205    def test_get_returning_data(self):
206        # Student is in level 100, session 2004 with verdict A
207        utils = getUtility(IStudentsUtils)
208        self.assertEqual(utils.getReturningData(self.student),(2005, 200))
209        self.student['studycourse'].current_verdict = 'C'
210        self.assertEqual(utils.getReturningData(self.student),(2005, 110))
211        self.student['studycourse'].current_verdict = 'D'
212        self.assertEqual(utils.getReturningData(self.student),(2005, 100))
[9923]213        self.student['studycourse'].current_verdict = 'O'
214        self.assertEqual(utils.getReturningData(self.student),(2004, 110))
[8320]215        return
[8267]216
[8599]217    def test_set_payment_details(self):
[8320]218        self.app['configuration']['2004'].booking_fee = 150.0
219        self.app['configuration']['2004'].maint_fee = 180.0
[9143]220        self.app['configuration']['2004'].clearance_fee = 120.0
[8320]221        utils = getUtility(IStudentsUtils)
[8599]222
223        error, payment = utils.setPaymentDetails('schoolfee',self.student)
224        self.assertEqual(payment, None)
[10012]225        self.assertEqual(error, u'Wrong state.')
[8599]226
227        IWorkflowState(self.student).setState('cleared')
[13022]228        self.certificate.study_mode = 'pd_ft'
[8599]229        error, payment = utils.setPaymentDetails('schoolfee',self.student)
230        self.assertEqual(payment.p_level, 100)
231        self.assertEqual(payment.p_session, 2004)
[15594]232        self.assertEqual(payment.amount_auth, 70000)
[8599]233        self.assertEqual(payment.p_item, u'CERT1')
234        self.assertEqual(error, None)
235
236        IWorkflowState(self.student).setState('returning')
237        error, payment = utils.setPaymentDetails('schoolfee',self.student)
[9525]238        self.assertEqual('Session configuration object is not available.', error)
239        configuration = createObject('waeup.SessionConfiguration')
240        configuration.academic_session = 2005
241        self.app['configuration'].addSessionConfiguration(configuration)
242        error, payment = utils.setPaymentDetails('schoolfee',self.student)
[9297]243        self.assertEqual(payment.p_level, 200)
244        self.assertEqual(payment.p_session, 2005)
[15594]245        self.assertEqual(payment.amount_auth, 35300)
[8599]246        self.assertEqual(payment.p_item, u'CERT1')
247        self.assertEqual(error, None)
248
[17069]249        # UG returning students pay 70700
[10012]250        self.certificate.study_mode = 'ug_ft'
251        error, payment = utils.setPaymentDetails('schoolfee',self.student)
[17734]252        self.assertEqual(payment.amount_auth,  96700)
[10876]253        self.assertEqual(error, None)
[17069]254        # UG cleared students pay 87200
[10876]255        IWorkflowState(self.student).setState('cleared')
256        error, payment = utils.setPaymentDetails('schoolfee',self.student)
[17734]257        self.assertEqual(payment.amount_auth, 126200)
[10012]258        self.assertEqual(error, None)
259
[10009]260        # NCE student payment can be disabled by
261        # setting the base school fee to -1
262        IWorkflowState(self.student).setState('returning')
263        configuration = createObject('waeup.SessionConfiguration')
264        self.app['configuration']['2004'].school_fee_base = -1.0
265        self.certificate.study_mode = 'nce_ft'
266        error, payment = utils.setPaymentDetails('schoolfee',self.student)
[10010]267        self.assertEqual(error, u'School fee payment is disabled.')
[10009]268
[8599]269        error, payment = utils.setPaymentDetails('clearance',self.student)
[13800]270        self.assertEqual(error, u'Acceptance Fee payments not allowed.')
271        IWorkflowState(self.student).setState('cleared')
272        error, payment = utils.setPaymentDetails('clearance',self.student)
[8599]273        self.assertEqual(payment.p_level, 100)
274        self.assertEqual(payment.p_session, 2004)
[15594]275        self.assertEqual(payment.amount_auth, 120)
[8599]276        self.assertEqual(payment.p_item, u'CERT1')
277        self.assertEqual(error, None)
278
[17183]279        self.app['hostels'].accommodation_session = 2005
[8599]280        error, payment = utils.setPaymentDetails('hostel_maintenance',self.student)
[9611]281        self.assertEqual(payment, None)
[13616]282        self.assertEqual(error, 'No bed space allocated.')
[17183]283        self.app['hostels'].accommodation_session = 2004
[9611]284
285        error, payment = utils.setPaymentDetails('hostel_maintenance',self.student)
[8599]286        self.assertEqual(payment.p_level, 100)
287        self.assertEqual(payment.p_session, 2004)
[17629]288        self.assertEqual(payment.amount_auth, 876.0)
[9611]289        self.assertEqual(payment.p_item, u'My bed coordinates')
[8599]290        self.assertEqual(error, None)
291
[16899]292        #error, payment = utils.setPaymentDetails('third_semester',self.student)
293        #self.assertEqual(error, u'Amount could not be determined.')
294        #self.student['studycourse'].current_level = 300
295        #error, payment = utils.setPaymentDetails('third_semester',self.student)
296        #self.assertEqual(error, u'Amount could not be determined.')
297        #payment = createObject('waeup.StudentOnlinePayment')
298        #payment.p_category = u'schoolfee'
299        #payment.p_session = self.student.current_session
300        #payment.p_item = u'My Certificate'
301        #payment.p_id = u'anyid'
302        #self.student['payments']['anykey'] = payment
303        #payment.p_state = 'paid'
304        #payment.p_level = 300
305        #error, payment = utils.setPaymentDetails('third_semester',self.student)
306        #self.assertEqual(payment.p_level, 300)
307        #self.assertEqual(payment.p_session, 2004)
308        #self.assertEqual(payment.amount_auth, 7938)
309        #self.assertEqual(payment.p_item, u'')
310        #self.assertEqual(error, None)
[11913]311
[9943]312        self.certificate.study_mode = u'nce_sw'
[9612]313        error, payment = utils.setPaymentDetails('hostel_maintenance',self.student)
[16899]314        self.assertEqual(payment.p_level, 100)
[9612]315        self.assertEqual(payment.p_session, 2004)
[17629]316        self.assertEqual(payment.amount_auth, 547.5)  # 62.5% * 876
[9612]317        self.assertEqual(payment.p_item, u'My bed coordinates')
318        self.assertEqual(error, None)
319
[8599]320        error, payment = utils.setPaymentDetails('bed_allocation',self.student)
[16899]321        self.assertEqual(payment.p_level, 100)
[8599]322        self.assertEqual(payment.p_session, 2004)
[15594]323        self.assertEqual(payment.amount_auth, 150)
[17183]324        self.assertEqual(payment.p_item, u'regular_male_fr')
[8599]325        self.assertEqual(error, None)
326
[9153]327        error, payment = utils.setPaymentDetails('schoolfee',self.student, 2004, 100)
328        self.assertEqual(error, u'Previous session payment not yet implemented.')
[9156]329        return
330
331    def test_student_start_clearance(self):
332        self.browser.open(self.login_path)
333        self.browser.getControl(name="form.login").value = self.student_id
334        self.browser.getControl(name="form.password").value = 'spwd'
335        self.browser.getControl("Login").click()
336
337        IWorkflowInfo(self.student).fireTransition('admit')
338        self.browser.open(self.student_path + '/change_portrait')
339        image = open(SAMPLE_IMAGE, 'rb')
340        ctrl = self.browser.getControl(name='passportuploadedit')
341        file_ctrl = ctrl.mech_control
342        file_ctrl.add_file(image, filename='my_photo.jpg')
343        self.browser.getControl(
344            name='upload_passportuploadedit').click()
345        self.browser.open(self.student_path + '/start_clearance')
[9953]346        # In Okene the ug students start clearance with activation code ...
347        self.assertTrue('Activation Code:' in self.browser.contents)
[9156]348        self.browser.getControl("Start clearance now").click()
[9953]349        self.assertTrue('Activation code is invalid' in self.browser.contents)
350        # ... and nce students without.
351        self.certificate.study_mode = 'nce_ft'
352        self.browser.open(self.student_path + '/start_clearance')
353        self.assertFalse('Activation Code:' in self.browser.contents)
354        self.browser.getControl("Start clearance now").click()
355        self.assertTrue(
356            'Clearance process has been started' in self.browser.contents)
[9190]357
[10018]358    def test_open_slips(self):
[10019]359        # Managers can open clearance slip
[10018]360        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
361        self.browser.open(self.student_path + '/view_clearance')
362        self.browser.getLink("Download clearance slip").click()
363        self.assertEqual(self.browser.headers['Status'], '200 Ok')
364        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
365
[9190]366    def test_student_accommodation(self):
[9434]367        del self.student['accommodation']['2004']
[13614]368        self.certificate.study_mode = 'ug_pt'
[9190]369        # Login
370        self.browser.open(self.login_path)
371        self.browser.getControl(name="form.login").value = self.student_id
372        self.browser.getControl(name="form.password").value = 'spwd'
373        self.browser.getControl("Login").click()
374
375        # Students can book accommodation without AC ...
376        self.browser.open(self.acco_path)
377        IWorkflowInfo(self.student).fireTransition('admit')
[13459]378        self.browser.getControl("Book accommodation").click()
[9190]379        self.assertFalse('Activation Code:' in self.browser.contents)
380        self.browser.getControl("Create bed ticket").click()
381        # Bed is randomly selected but, since there is only
382        # one bed for this student, we know that
[9998]383        self.assertEqual(self.student['accommodation']['2004'].bed_coordinates,
384            'Hall 1, Block A, Room 101, Bed A (regular_male_fr)')
385        self.assertEqual(self.student['accommodation']['2004'].display_coordinates,
386            '(see payment slip)')
387        # But the bed coordinates are hidden.
388        self.assertFalse('Hall 1, Block A, Room 101, Bed A'
389            in self.browser.contents)
390        self.assertTrue('<td>(see payment slip)</td>'
391            in self.browser.contents)
[10664]392        return
[10828]393
[15507]394    def test_admission_slip(self):
[10828]395        # Login
396        IWorkflowState(self.student).setState('admitted')
397        self.browser.open(self.login_path)
398        self.browser.getControl(name="form.login").value = self.student_id
399        self.browser.getControl(name="form.password").value = 'spwd'
400        self.browser.getControl("Login").click()
401        self.assertFalse(
402          'Download admission letter' in self.browser.contents)
403        IWorkflowState(self.student).setState('clearance started')
404        self.browser.open(self.student_path)
405        self.assertTrue(
406          'Download admission letter' in self.browser.contents)
[15507]407        # Students can open admission letter
408        self.browser.getLink("Download admission letter").click()
409        self.assertEqual(self.browser.headers['Status'], '200 Ok')
410        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
[15887]411        path = os.path.join(samples_dir(), 'admission_slip_combined.pdf')
412        open(path, 'wb').write(self.browser.contents)
413        print "Sample PDF admission_slip_combined.pdf written to %s" % path
414        self.certificate.study_mode = 'pd_ft'
415        self.browser.open(self.student_path)
416        self.browser.getLink("Download admission letter").click()
[15507]417        path = os.path.join(samples_dir(), 'admission_slip.pdf')
418        open(path, 'wb').write(self.browser.contents)
419        print "Sample PDF admission_slip.pdf written to %s" % path
[13030]420        return
421
422    def test_payment_disabled(self):
423        self.certificate.study_mode = 'nce_ft'
424        IWorkflowState(self.student).setState('cleared')
425        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
426        self.browser.open(self.payments_path)
427        self.browser.getLink("Add current session payment ticket").click()
428        self.browser.getControl(name="form.p_category").value = ['schoolfee']
429        self.browser.getControl("Create ticket").click()
430        self.assertMatches('...ticket created...',
431                           self.browser.contents)
432        self.app['configuration']['2004'].payment_disabled = ['sf_nce1']
[16365]433        self.browser.open(self.payments_path)
[13030]434        self.browser.getLink("Add current session payment ticket").click()
435        self.browser.getControl(name="form.p_category").value = ['schoolfee']
436        self.browser.getControl("Create ticket").click()
[13800]437        self.assertMatches('...This category of payments has been disabled...',
[13030]438                           self.browser.contents)
439        self.certificate.study_mode = 'ug_ft'
440        self.browser.open(self.payments_path)
441        self.browser.getLink("Add current session payment ticket").click()
442        self.browser.getControl(name="form.p_category").value = ['schoolfee']
443        self.browser.getControl("Create ticket").click()
444        self.assertMatches('...ticket created...',
445                           self.browser.contents)
[14591]446        return
447
448    def test_student_course_registration(self):
449        IWorkflowState(self.student).setState('school fee paid')
450        self.browser.open(self.login_path)
451        self.browser.getControl(name="form.login").value = self.student_id
452        self.browser.getControl(name="form.password").value = 'spwd'
453        self.browser.getControl("Login").click()
454        # Now students can add the current study level
455        self.browser.getLink("Study Course").click()
456        self.browser.getLink("Add course list").click()
457        self.assertMatches('...Add current level 100 (Year 1)...',
458                           self.browser.contents)
459        self.browser.getControl("Create course list now").click()
[15977]460        # Students can open the customized pdf course registration slip
[14591]461        self.browser.open(
462            self.student_path + '/studycourse/100/course_registration_slip.pdf')
463        self.assertEqual(self.browser.headers['Status'], '200 Ok')
464        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
465        path = os.path.join(samples_dir(), 'course_registration_slip.pdf')
466        open(path, 'wb').write(self.browser.contents)
[15977]467        print "Sample PDF course_registration_slip.pdf written to %s" % path
468
469        # Students can open the examination clearance slip if they are
470        # in state courses validated
471        IWorkflowState(self.student).setState('courses validated')
472        self.browser.open(self.student_path + '/studycourse/100')
[15989]473        self.browser.getLink("Download 1st semester examination clearance slip").click()
[15977]474        self.assertEqual(self.browser.headers['Status'], '200 Ok')
475        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
[15989]476        path = os.path.join(samples_dir(), 'examination_clearance_slip_1.pdf')
[15977]477        open(path, 'wb').write(self.browser.contents)
[15989]478        print "Sample PDF examination_clearance_slip_1.pdf written to %s" % path
479        self.browser.open(self.student_path + '/studycourse/100')
480        self.browser.getLink("Download 2nd semester examination clearance slip").click()
481        self.assertEqual(self.browser.headers['Status'], '200 Ok')
482        self.assertEqual(self.browser.headers['Content-Type'], 'application/pdf')
483        path = os.path.join(samples_dir(), 'examination_clearance_slip_2.pdf')
484        open(path, 'wb').write(self.browser.contents)
485        print "Sample PDF examination_clearance_slip_2.pdf written to %s" % path
Note: See TracBrowser for help on using the repository browser.