source: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt @ 5141

Last change on this file since 5141 was 5138, checked in by uli, 14 years ago
  • Explain better, what the plugin is used for.
  • Use upcoming unit-test layer.
File size: 12.1 KB
Line 
1:mod:`waeup.sirp.accesscodes.accesscodes` -- access codes (aka PINs)
2********************************************************************
3
4.. module:: waeup.sirp.accesscodes.accesscodes
5
6Components that represent access codes and related.
7
8.. :doctest:
9.. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
10
11About access-codes
12==================
13
14Access codes are ids used to grant first-time access to the system for
15students.
16
17They are normally not generated by third-party components but always
18part of batches.
19
20An access-code consists of three parts::
21
22    APP-12-0123456789
23    ^^^ ^^ ^^^^^^^^^^
24     A  B  C
25
26where ``A`` tells about the purpose of the code, ``B`` gives the
27number of batch the code belongs to (1 to 3 digits), and ``C`` is a
28unique random number of 10 digits.
29
30For the generation of the random number :mod:`waeup.sirp` requires a
31'urandom' entropy provider which is available with most standard
32Unix/Linux systems. This makes the generated numbers relatively
33secure, especially when compared with recent PHP-based applications.
34
35
36AccessCode
37==========
38
39.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
40
41   You normally shouldn't create standalone access-codes. Use
42   instances of :class:`AccessCodeBatch` instead as they generate them
43   (in masses) and care for them.
44
45   Note, that :class:`AccessCode` instances are not persistent on
46   themselves. They have to be stored inside a persistent object (like
47   :class:`AccessCodeBatch`) to be kept.
48
49   The class implements
50   :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`:
51
52    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
53    >>> from waeup.sirp.accesscodes.accesscodes import AccessCode
54    >>> from zope.interface.verify import verifyClass
55    >>> verifyClass(IAccessCode, AccessCode)
56    True
57
58   .. attribute:: representation
59
60      The 'full' id of an access-code as described above. Something
61      like ``'APP-12-0123456789'``.
62
63      Read-only attribute.
64
65   .. attribute:: batch_serial
66
67      Serial number of this access-code inside the batch.
68
69      .. note:: XXX: Do we really need this?
70
71         Subject to be dropped.
72
73   .. attribute:: student_id
74
75      A string or ``None``. Set when an access-code is
76      invalidated. ``None`` by default.
77
78   .. attribute:: batch_prefix
79
80      The prefix of the batch the access-code belongs to.
81
82      Read-only attribute.
83
84   .. attribute:: batch_num
85
86      The number of the batch the access-code belongs to.
87
88      Read-only attribute.
89
90   .. attribute:: cost
91
92      What the access-code costs. A float.
93
94      Read-only attribute.
95
96   .. attribute:: invalidation_date
97
98      Python datetime when the access code was invalidated, or
99      ``None``. ``None`` by default.
100
101      Read-only attribute. Only batches are supposed to set this value.
102
103   Access codes that are not part of a batch, will give strange
104   representations:
105
106    >>> ac = AccessCode(None, '9999999999')
107    >>> ac.representation
108    '--<10-DIGITS>'
109
110   Also the ``cost`` will not be set:
111
112    >>> ac.cost is None
113    True
114
115
116AccessCodeBatch
117===============
118
119.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
120
121   Create a batch of access-codes.
122
123   :param creation_date: python datetime
124   :param creator: creators user id
125   :type creator: string
126   :param batch_prefix: prefix of this batch
127   :param cost: cost per access code
128   :type cost: float
129   :param entry_num: number of access codes to create
130   :param num: number of this batch
131
132   A persistent :class:`grok.Model`. It implements
133   :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`.
134
135   When creating a batch, all entries (access-codes) are generated as
136   well.
137
138   .. attribute:: creation_date
139
140      The datetime when the batch was created.
141
142   .. attribute:: creator
143
144      String with user id of the user that generated the batch.
145
146   .. attribute:: cost
147
148      Float representing the costs for a single access-code. All
149      entries inside the batch share the same cost.
150
151   .. attribute:: entry_num
152
153      Number of entries (access-codes) inside the batch.
154
155   .. attribute:: invalidated_num
156
157      Number of entries that were already invalidated.
158
159   .. attribute:: prefix
160
161      Prefix of the batch. This tells about the purpose of this batch.
162
163   .. attribute:: num
164
165      Number of this batch. For a certain prefix there can exist
166      several batches, which are numbered in increasing order. The
167      number is normally computed by the
168      :class:`AccessCodeBatchContainer` in which batches are
169      stored.
170
171      .. seealso:: :class:`AccessCodeBatchContainer`
172
173   .. method:: entries()
174
175      Get all accesscodes stored in the batch.
176
177      Returns a generator over all stored entries.
178
179   .. method:: getAccessCode(acesscode_id)
180
181      Get the :class:`AccessCode` object for the given
182      ``accesscode_id``.
183
184      Certain single access codes can be accessed inside a batch by
185      their representation (i.e. something like ``'APP-12-0123456789'``.
186
187      When a code cannot be found :exc:`KeyError` is raised.
188
189   .. method:: addAccessCode(serial_num, random_num)
190
191      Add an access code to the batch.
192
193      ``serial_num`` denotes the serial number of the new access-code
194      inside the batch. ``random_num`` is a string of 10 digits unique
195      in this batch.
196
197   .. method:: invalidate(ac_id[, student_id=None])
198
199      Invalidate the access-code with ID ``ac_id``.
200
201      Sets also the ``student_id`` attribute of the respective
202      :class:`AccessCode` entry.
203
204   .. method:: createCSVLogFile()
205
206      Create a CSV file with data in batch.
207
208      Data will not contain invalidation date nor student ids.  File
209      will be created in ``accesscodes`` subdir of data center storage
210      path.
211
212      Returns name of created file.
213
214   .. method:: archive()
215
216      Create a CSV file for archive. Archive files contain also
217      ``student_id`` and ``invalidation_date``.
218
219      Returns name of created file.
220
221
222Examples
223--------
224
225:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
226
227    >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch
228    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
229    >>> from zope.interface.verify import verifyClass
230    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
231    True
232
233Creating a batch of three access-codes, with a cost of ``12.12`` per
234code, the batch prefix ``APP``, batch number ``10``, creator ID
235``Fred`` and some arbitrary creation date:
236
237    >>> import datetime
238    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
239    >>> batch = AccessCodeBatch(
240    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
241
242Getting all access-codes from a batch:
243
244    >>> ac_codes = batch.entries()
245    >>> ac_codes
246    <generator object at 0x...>
247
248    >>> [x.representation for x in ac_codes]
249    ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>']
250
251    >>> [x for x in batch.entries()]
252    [<waeup.sirp...AccessCode object at 0x...>, ...]
253
254Getting a single entry from the batch:
255
256    >>> ac_id = list(batch.entries())[0].representation
257    >>> ac = batch.getAccessCode(ac_id)
258    >>> ac
259    <waeup.sirp...AccessCode object at 0x...>
260
261    >>> ac is list(batch.entries())[0]
262    True
263
264Trying to get a single not-existent entry from a batch:
265
266    >>> batch.getAccessCode('blah')
267    Traceback (most recent call last):
268    ...
269    KeyError: 'blah'
270
271Invalidating an entry:
272
273    >>> batch.invalidated_num
274    0
275
276    >>> str(ac.invalidation_date), str(ac.student_id)
277    ('None', 'None')
278
279    >>> batch.invalidate(ac_id)
280    >>> batch.invalidated_num
281    1
282
283    >>> ac.invalidation_date , str(ac.student_id)
284    (datetime.datetime(...), 'None')
285
286    >>> batch.invalidate(ac_id, 'some_user_id')
287    >>> ac.student_id
288    'some_user_id'
289
290Access codes get their ``cost`` from the batch they belong to. Note,
291that it is advisable to print costs always using a format, as Python
292floats are often represented by irritating values:
293
294    >>> ac.cost
295    12.119999999999999
296
297    >>> print "%0.2f" % ac.cost
298    12.12
299
300
301AccessCodeBatchContainer
302========================
303
304.. class:: AccessCodeBatchContainer()
305
306   A container for access code batches.
307
308   .. method:: addBatch(batch)
309
310      Add a batch in this container. You should make sure, that
311      entries in the given batch are set correctly.
312
313   .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num)
314
315      Create a batch inside this container. Returns the batch created.
316
317      :param creation_date: python datetime
318      :param creator: creators user id
319      :type creator: string
320      :param batch_prefix: prefix of this batch
321      :param cost: cost per access code
322      :type cost: float
323      :param entry_num: number of access codes to create
324
325   .. method:: getNum(prefix)
326
327      Get next unused num for a new batch and a given prefix.
328
329      Batches for a given prefix are numerated. Whenever a new batch
330      is created and other batches inside the container already have
331      the same prefix, the new one will get the lowest unused number.
332
333   .. method:: getImportFiles()
334
335      Get a list of basenames of available import files, suitable for
336      feeding :meth:`reimport`.
337
338   .. method:: reimport(filename, creator=u'UNKNOWN')
339
340      Reimport a CSV log file of previously created AC batch.
341
342      ``filename`` is the name (basename) of the file residing in the
343      accesscode storage's ``import`` directory.``creator`` is the
344      user ID of the current user.
345
346
347Access code plugin
348==================
349
350.. class:: AccessCodePlugin
351
352  A `waeup.sirp` plugin that updates existing WAeUP SIRP university
353  instances so that they provide support for access-codes.
354
355  .. attribute:: grok.implements(IWAeUPSIRPPluggable)
356  .. attribute:: grok.name('accesscodes')
357
358  .. method:: setup(site, name, logger)
359
360     Create an accesscodebatch container in ``site``. Any events are
361     logged to ``logger``.
362
363  .. method:: update(site, name, logger)
364
365     Check if ``site`` contains an accesscodebatch container and add
366     it if missing. Any events are logged to ``logger``.
367
368  The AccessCodePlugin is available as a global named utility for the
369  IWAeUPSIRPPluggable interface named ``accesscodes``.
370
371  It is looked up by a university instance when created, so that this
372  instance has nothing to know about accesscodes at all and can
373  although provide support for them.
374
375    >>> from zope.component import getUtility
376    >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
377    >>> plugin = getUtility(IWAeUPSIRPPluggable, name='accesscodes')
378    >>> plugin
379    <waeup.sirp.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
380
381  It provides a `setup()` and an `update()` method. Both have to be
382  fed with a site object (the `IUniversity` instance to modify), a
383  logger and a name.
384
385  We create a faked site and a logger:
386
387    >>> import logging
388    >>> faked_site = dict()
389    >>> logger = logging.getLogger('ac_test')
390    >>> logger.setLevel(logging.DEBUG)
391    >>> ch = logging.FileHandler('ac_tests.log', 'w')
392    >>> ch.setLevel(logging.DEBUG)
393    >>> logger.addHandler(ch)
394
395  Now we can install our stuff in the faked site:
396
397    >>> plugin.setup(faked_site, 'blah', logger)
398
399  The faked site now has an access code container:
400
401    >>> faked_site.keys()
402    ['accesscodes']
403
404  The action is described in the log:
405
406    >>> print open('ac_tests.log', 'r').read()
407    Installed container for access code batches.
408
409  We can also update an existing site, by calling `update()`:
410
411    >>> plugin.update(faked_site, 'blah', logger)
412
413  There was nothing to do for the updater:
414
415    >>> print open('ac_tests.log', 'r').read()
416    Installed container for access code batches.
417    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
418
419  But if we remove the created batch container and call the updater, it
420  will create a new one:
421
422    >>> del faked_site['accesscodes']
423    >>> plugin.update(faked_site, 'blah', logger)
424    >>> print open('ac_tests.log', 'r').read()
425    Installed container for access code batches.
426    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
427    Updating site at {}. Installing access codes.
428    Installed container for access code batches.
429
430  Clean up:
431
432    >>> import os
433    >>> os.unlink('ac_tests.log')
Note: See TracBrowser for help on using the repository browser.