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

Last change on this file since 6069 was 5150, checked in by uli, 15 years ago

Update tests.

File size: 18.8 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   Access-codes can have three states: unused, used, and
50   disabled. While still unused but enabled access-codes are reflected
51   by an empty ``invalidation_date`` (set to ``None``), an already
52   used (invalidated) code provides an invalidation date.
53
54   In case of misuse or similar cases access-codes can also be
55   completely disabled by setting the ``disabled`` attribute to
56   ``True``.
57
58   The class implements
59   :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`:
60
61    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
62    >>> from waeup.sirp.accesscodes.accesscodes import AccessCode
63    >>> from zope.interface.verify import verifyClass
64    >>> verifyClass(IAccessCode, AccessCode)
65    True
66
67   .. attribute:: representation
68
69      The 'full' id of an access-code as described above. Something
70      like ``'APP-12-0123456789'``.
71
72      Read-only attribute.
73
74   .. attribute:: batch_serial
75
76      Serial number of this access-code inside the batch.
77
78      .. note:: XXX: Do we really need this?
79
80         Subject to be dropped.
81
82   .. attribute:: student_id
83
84      A string or ``None``. Set when an access-code is
85      invalidated. ``None`` by default.
86
87   .. attribute:: batch_prefix
88
89      The prefix of the batch the access-code belongs to.
90
91      Read-only attribute.
92
93   .. attribute:: batch_num
94
95      The number of the batch the access-code belongs to.
96
97      Read-only attribute.
98
99   .. attribute:: cost
100
101      What the access-code costs. A float.
102
103      Read-only attribute.
104
105   .. attribute:: invalidation_date
106
107      Python datetime when the access code was invalidated, or
108      ``None``. ``None`` by default.
109
110      If an access-code is disabled, this attribute contains the
111      datetime of disabling.
112
113      Read-only attribute. Only batches are supposed to set this value.
114
115   .. attribute:: disabled
116
117      Boolean, ``False`` by default. When set to ``True``, this
118      access-code should not be used any more.
119
120      Read-only attribute. Only batches are supposed to set this value.
121
122   Access codes that are not part of a batch, will give strange
123   representations:
124
125    >>> ac = AccessCode(None, '9999999999')
126    >>> ac.representation
127    '--<10-DIGITS>'
128
129   Also the ``cost`` will not be set:
130
131    >>> ac.cost is None
132    True
133
134
135AccessCodeBatch
136===============
137
138.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
139
140   Create a batch of access-codes.
141
142   :param creation_date: python datetime
143   :param creator: creators user id
144   :type creator: string
145   :param batch_prefix: prefix of this batch
146   :param cost: cost per access code
147   :type cost: float
148   :param entry_num: number of access codes to create
149   :param num: number of this batch
150
151   A persistent :class:`grok.Model`. It implements
152   :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`.
153
154   When creating a batch, all entries (access-codes) are generated as
155   well.
156
157   .. attribute:: creation_date
158
159      The datetime when the batch was created.
160
161   .. attribute:: creator
162
163      String with user id of the user that generated the batch.
164
165   .. attribute:: cost
166
167      Float representing the costs for a single access-code. All
168      entries inside the batch share the same cost.
169
170   .. attribute:: entry_num
171
172      Number of entries (access-codes) inside the batch.
173
174   .. attribute:: invalidated_num
175
176      Number of entries that were already invalidated.
177
178   .. attribute:: prefix
179
180      Prefix of the batch. This tells about the purpose of this batch.
181
182   .. attribute:: num
183
184      Number of this batch. For a certain prefix there can exist
185      several batches, which are numbered in increasing order. The
186      number is normally computed by the
187      :class:`AccessCodeBatchContainer` in which batches are
188      stored.
189
190      .. seealso:: :class:`AccessCodeBatchContainer`
191
192   .. method:: entries()
193
194      Get all accesscodes stored in the batch.
195
196      Returns a generator over all stored entries.
197
198   .. method:: getAccessCode(acesscode_id)
199
200      Get the :class:`AccessCode` object for the given
201      ``accesscode_id``.
202
203      Certain single access codes can be accessed inside a batch by
204      their representation (i.e. something like ``'APP-12-0123456789'``.
205
206      When a code cannot be found :exc:`KeyError` is raised.
207
208   .. method:: getAccessCodeForStudentId(student_id)
209
210      Get the :class:`AccessCode` object for the given
211      ``student_id``.
212
213      Certain single access codes can be accessed inside a batch by
214      their student_id. The student_id must be some string; ``None``
215      is not a valid value.
216
217      When a code cannot be found :exc:`KeyError` is raised.
218
219   .. method:: addAccessCode(serial_num, random_num)
220
221      Add an access code to the batch.
222
223      ``serial_num`` denotes the serial number of the new access-code
224      inside the batch. ``random_num`` is a string of 10 digits unique
225      in this batch.
226
227   .. method:: invalidate(ac_id[, student_id=None])
228
229      Invalidate the access-code with ID ``ac_id``.
230
231      Sets also the ``student_id`` attribute of the respective
232      :class:`AccessCode` entry.
233
234   .. method:: disable(ac_id, user_id)
235
236      Disable the access-code with ID ``ac_id``.
237
238     ``user_id`` is the user ID of the user triggering the
239      process. Already disabled ACs are left untouched.
240
241      Sets also the ``student_id`` and ``invalidation_date``
242      attributes of the respective :class:`AccessCode` entry. While
243      ``student_id`` is set to the given ``user_id``,
244      ``invalidation_date`` is set to current datetime.
245
246      Disabled access codes are supposed not to be used any more at
247      all.
248
249   .. method:: enable(ac_id)
250
251      (Re-)enable the access-code with ID ``ac_id``.
252
253      This leaves the given AC in state ``unused``. Already enabled
254      ACs are left untouched.
255
256      Sets ``student_id`` and ``invalidation_date`` values of the
257      respective :class:`AccessCode` entry to ``None``.
258
259   .. method:: createCSVLogFile()
260
261      Create a CSV file with data in batch.
262
263      Data will not contain invalidation date nor student ids.  File
264      will be created in ``accesscodes`` subdir of data center storage
265      path.
266
267      Returns name of created file.
268
269   .. method:: archive()
270
271      Create a CSV file for archive. Archive files contain also
272      ``student_id`` and ``invalidation_date``.
273
274      Returns name of created file.
275
276   .. method:: search(searchterm, searchtype)
277
278      Search the batch for entries that comply with ``searchterm`` and
279      ``searchtype``.
280
281      ``searchtype`` must be one of ``serial``, ``pin``, or
282      ``stud_id`` and specifies, what kind of data is contained in the
283      ``searchterm``.
284
285      - ``serial`` looks for the AC with the ``serial_batch`` set to
286        the given (integer) number in ``searchterm``. Here
287        ``searchterm`` must be an int number.
288
289      - ``pin`` looks for the AC with the ``representation`` set to
290        the given string in ``searchterm``. Here ``searchterm`` must
291        be a string containing the full AC-ID like
292        ``APP-1-0123456789``.
293
294      - ``stud_id`` looks for the AC with the ``student_id`` set to
295        the given string in ``searchterm``. Here ``searchterm`` must
296        be a string containing the full student ID.
297
298      Lookup is done via local BTrees and therefore pretty fast.
299
300      Returns a list of access-codes found or empty list.
301
302
303Examples
304--------
305
306:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
307
308    >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch
309    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
310    >>> from zope.interface.verify import verifyClass
311    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
312    True
313
314Creating a batch of three access-codes, with a cost of ``12.12`` per
315code, the batch prefix ``APP``, batch number ``10``, creator ID
316``Fred`` and some arbitrary creation date:
317
318    >>> import datetime
319    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
320    >>> batch = AccessCodeBatch(
321    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
322
323Getting all access-codes from a batch:
324
325    >>> ac_codes = batch.entries()
326    >>> ac_codes
327    <generator object at 0x...>
328
329    >>> [x.representation for x in ac_codes]
330    ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>']
331
332    >>> [x for x in batch.entries()]
333    [<waeup.sirp...AccessCode object at 0x...>, ...]
334
335Getting a single entry from the batch:
336
337    >>> ac_id = list(batch.entries())[0].representation
338    >>> ac = batch.getAccessCode(ac_id)
339    >>> ac
340    <waeup.sirp...AccessCode object at 0x...>
341
342    >>> ac is list(batch.entries())[0]
343    True
344
345Trying to get a single not-existent entry from a batch:
346
347    >>> batch.getAccessCode('blah')
348    Traceback (most recent call last):
349    ...
350    KeyError: 'blah'
351
352Invalidating an entry:
353
354    >>> batch.invalidated_num
355    0
356
357    >>> str(ac.invalidation_date), str(ac.student_id)
358    ('None', 'None')
359
360    >>> batch.invalidate(ac_id)
361    >>> batch.invalidated_num
362    1
363
364    >>> ac.invalidation_date , str(ac.student_id)
365    (datetime.datetime(...), 'None')
366
367    >>> batch.invalidate(ac_id, 'some_user_id')
368    >>> ac.student_id
369    'some_user_id'
370
371Getting a single entry by student_id:
372
373    >>> batch.getAccessCodeForStudentId('some_user_id')
374    <waeup.sirp...AccessCode object at 0x...>
375
376Non-existent values will cause a :exc:`KeyError`:
377
378    >>> batch.getAccessCodeForStudentId('non-existing')
379    Traceback (most recent call last):
380    ...
381    KeyError: 'non-existing'
382
383Already enabled entries will be left untouched when trying to renable
384them:
385
386    >>> batch.enable(ac_id)
387    >>> ac.student_id
388    'some_user_id'
389
390Disabling an entry:
391
392    >>> batch.disabled_num
393    0
394
395    >>> ac.disabled
396    False
397
398    >>> batch.disable(ac_id, 'some userid')
399    >>> ac.disabled
400    True
401
402    >>> ac.student_id
403    'some userid'
404
405    >>> batch.disabled_num
406    1
407
408Already disabled entries will not be disabled again:
409
410    >>> batch.disable(ac_id, 'other userid')
411    >>> ac.student_id
412    'some userid'
413
414Reenabling an entry:
415
416    >>> batch.enable(ac_id)
417    >>> ac.disabled
418    False
419
420    >>> ac.student_id is None
421    True
422
423    >>> ac.invalidation_date is None
424    True
425
426Access codes get their ``cost`` from the batch they belong to. Note,
427that it is advisable to print costs always using a format, as Python
428floats are often represented by irritating values:
429
430    >>> ac.cost
431    12.119999999999999
432
433    >>> print "%0.2f" % ac.cost
434    12.12
435
436Searching for serials:
437
438    >>> result = batch.search(0, 'serial')
439    >>> result
440    [<waeup.sirp...AccessCode object at 0x...>]
441
442    >>> result[0].batch_serial
443    0
444
445Searching for AC-IDs:
446
447    >>> result = batch.search(ac.representation, 'pin')
448    >>> result[0].representation
449    'APP-...-...'
450
451Searching for student IDs:
452
453    >>> batch.invalidate(ac_id, 'some_new_user_id')
454    >>> result = batch.search('some_new_user_id', 'stud_id')
455    >>> result[0].student_id
456    'some_new_user_id'
457
458Searching for not existing entries will return empty lists:
459
460    >>> batch.search(12, 'serial')
461    []
462
463    >>> batch.search('not-a-valid-pin', 'pin')
464    []
465
466    >>> batch.search('not-a-student-id', 'stud_id')
467    []
468
469    >>> batch.search('blah', 'not-a-valid-searchtype')
470    []
471
472AccessCodeBatchContainer
473========================
474
475.. class:: AccessCodeBatchContainer()
476
477   A container for access code batches.
478
479   .. method:: addBatch(batch)
480
481      Add a batch in this container. You should make sure, that
482      entries in the given batch are set correctly.
483
484   .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num)
485
486      Create a batch inside this container. Returns the batch created.
487
488      :param creation_date: python datetime
489      :param creator: creators user id
490      :type creator: string
491      :param batch_prefix: prefix of this batch
492      :param cost: cost per access code
493      :type cost: float
494      :param entry_num: number of access codes to create
495
496   .. method:: getNum(prefix)
497
498      Get next unused num for a new batch and a given prefix.
499
500      Batches for a given prefix are numerated. Whenever a new batch
501      is created and other batches inside the container already have
502      the same prefix, the new one will get the lowest unused number.
503
504   .. method:: getImportFiles()
505
506      Get a list of basenames of available import files, suitable for
507      feeding :meth:`reimport`.
508
509   .. method:: reimport(filename, creator=u'UNKNOWN')
510
511      Reimport a CSV log file of previously created AC batch.
512
513      ``filename`` is the name (basename) of the file residing in the
514      accesscode storage's ``import`` directory.``creator`` is the
515      user ID of the current user.
516
517   .. method:: search(search_term, search_type)
518
519      Look in all contained batches for access codes that comply with
520      the given parameters.
521
522      ``search_type`` must be one of ``serial``, ``pin``, or
523      ``stud_id`` and specifies, what kind of data is contained in the
524      ``search_term``.
525
526      - ``serial`` looks for the AC with the ``serial_batch`` set to
527        the given (integer) number in ``search_term``. Here
528        ``search_term`` can be an integer or a string. If it is a
529        string it will be converted to an int.
530
531      - ``pin`` looks for the AC with the ``representation`` set to
532        the given string in ``search_term``. Here ``search_term`` must
533        be a string containing the full AC-ID like
534        ``APP-1-0123456789``.
535
536      - ``stud_id`` looks for the AC with the ``student_id`` set to
537        the given string in ``search_term``. Here ``search_term`` must
538        be a string containing the full student ID.
539
540Examples:
541---------
542
543Creating a batch container:
544
545    >>> from waeup.sirp.accesscodes.accesscodes import (
546    ...   AccessCodeBatchContainer)
547    >>> container = AccessCodeBatchContainer()
548
549Creating batches inside the container:
550
551    >>> from datetime import datetime
552    >>> batch1 = container.createBatch(
553    ...   datetime.now(), 'some userid', 'FOO', 10.12, 5)
554
555    >>> batch2 = container.createBatch(
556    ...   datetime.now(), 'other userid', 'BAR', 1.95, 5)
557
558Searching the container for batch serials:
559
560    >>> container.search(1, 'serial')
561    [<waeup.sirp...AccessCode object at 0x...>,
562     <waeup.sirp...AccessCode object at 0x...>]
563
564    >>> container.search('not-a-number', 'serial')
565    []
566
567    >>> result = container.search('1', 'serial')
568    >>> result
569    [<waeup.sirp...AccessCode object at 0x...>,
570     <waeup.sirp...AccessCode object at 0x...>]
571
572Searching for ACs:
573
574    >>> ac = result[0]
575    >>> container.search(ac.representation, 'pin')
576    [<waeup.sirp...AccessCode object at 0x...>]
577
578Searching for student IDs:
579
580    >>> ac.__parent__.invalidate(
581    ...   ac.representation, 'some_user')
582    >>> container.search('some_user', 'stud_id')
583    [<waeup.sirp...AccessCode object at 0x...>]
584
585
586Access code plugin
587==================
588
589.. class:: AccessCodePlugin
590
591  A `waeup.sirp` plugin that updates existing WAeUP SIRP university
592  instances so that they provide support for access-codes.
593
594  .. attribute:: grok.implements(IWAeUPSIRPPluggable)
595  .. attribute:: grok.name('accesscodes')
596
597  .. method:: setup(site, name, logger)
598
599     Create an accesscodebatch container in ``site``. Any events are
600     logged to ``logger``.
601
602  .. method:: update(site, name, logger)
603
604     Check if ``site`` contains an accesscodebatch container and add
605     it if missing. Any events are logged to ``logger``.
606
607  The AccessCodePlugin is available as a global named utility for the
608  IWAeUPSIRPPluggable interface named ``accesscodes``.
609
610  It is looked up by a university instance when created, so that this
611  instance has nothing to know about accesscodes at all and can
612  although provide support for them.
613
614    >>> from zope.component import getUtility
615    >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
616    >>> plugin = getUtility(IWAeUPSIRPPluggable, name='accesscodes')
617    >>> plugin
618    <waeup.sirp.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
619
620  It provides a `setup()` and an `update()` method. Both have to be
621  fed with a site object (the `IUniversity` instance to modify), a
622  logger and a name.
623
624  We create a faked site and a logger:
625
626    >>> import logging
627    >>> faked_site = dict()
628    >>> logger = logging.getLogger('ac_test')
629    >>> logger.setLevel(logging.DEBUG)
630    >>> ch = logging.FileHandler('ac_tests.log', 'w')
631    >>> ch.setLevel(logging.DEBUG)
632    >>> logger.addHandler(ch)
633
634  Now we can install our stuff in the faked site:
635
636    >>> plugin.setup(faked_site, 'blah', logger)
637
638  The faked site now has an access code container:
639
640    >>> faked_site.keys()
641    ['accesscodes']
642
643  The action is described in the log:
644
645    >>> print open('ac_tests.log', 'r').read()
646    Installed container for access code batches.
647
648  We can also update an existing site, by calling `update()`:
649
650    >>> plugin.update(faked_site, 'blah', logger)
651
652  There was nothing to do for the updater:
653
654    >>> print open('ac_tests.log', 'r').read()
655    Installed container for access code batches.
656    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
657
658  But if we remove the created batch container and call the updater, it
659  will create a new one:
660
661    >>> del faked_site['accesscodes']
662    >>> plugin.update(faked_site, 'blah', logger)
663    >>> print open('ac_tests.log', 'r').read()
664    Installed container for access code batches.
665    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
666    Updating site at {}. Installing access codes.
667    Installed container for access code batches.
668
669  Clean up:
670
671    >>> import os
672    >>> os.unlink('ac_tests.log')
Note: See TracBrowser for help on using the repository browser.