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

Last change on this file since 10222 was 7321, checked in by Henrik Bettermann, 13 years ago

Replace the term 'WAeUP' by SIRP which is a WAeUP product.

File size: 18.8 KB
Line 
1:mod:`waeup.sirp.accesscodes.accesscode` -- access codes (aka PINs)
2*******************************************************************
3
4.. module:: waeup.sirp.accesscodes.accesscode
5
6Components that represent access codes and related.
7
8.. :NOdoctest:
9.. :NOlayer: waeup.sirp.testing.SIRPUnitTestLayer
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#
358#    >>> str(ac.invalidation_date), str(ac.student_id)
359#    ('None', 'None')
360#
361
362    >>> batch.invalidate(ac_id)
363    >>> batch.invalidated_num
364    1
365
366#    >>> ac.invalidation_date , str(ac.student_id)
367#    (datetime.datetime(...), 'None')
368#
369#    >>> batch.invalidate(ac_id, 'some_user_id')
370#    >>> ac.student_id
371#    'some_user_id'
372#
373#Getting a single entry by student_id:
374#
375#    >>> batch.getAccessCodeForStudentId('some_user_id')
376#    <waeup.sirp...AccessCode object at 0x...>
377#
378#Non-existent values will cause a :exc:`KeyError`:
379#
380#    >>> batch.getAccessCodeForStudentId('non-existing')
381#    Traceback (most recent call last):
382#    ...
383#    KeyError: 'non-existing'
384#
385#Already enabled entries will be left untouched when trying to renable
386#them:
387
388#    >>> batch.enable(ac_id)
389#    >>> ac.student_id
390#    'some_user_id'
391
392Disabling an entry:
393
394#    >>> batch.disabled_num
395#    0
396#
397#    >>> ac.disabled
398#    False
399#
400#    >>> batch.disable(ac_id, 'some userid')
401#    >>> ac.disabled
402#    True
403#
404#    >>> ac.student_id
405#    'some userid'
406#
407#    >>> batch.disabled_num
408#    1
409
410Already disabled entries will not be disabled again:
411
412#    >>> batch.disable(ac_id, 'other userid')
413#    >>> ac.student_id
414#    'some userid'
415
416Reenabling an entry:
417
418#    >>> batch.enable(ac_id)
419#    >>> ac.disabled
420#    False
421#
422#    >>> ac.student_id is None
423#    True
424#
425#    >>> ac.invalidation_date is None
426#    True
427
428Access codes get their ``cost`` from the batch they belong to. Note,
429that it is advisable to print costs always using a format, as Python
430floats are often represented by irritating values:
431
432    >>> ac.cost
433    12.119999999999999
434
435    >>> print "%0.2f" % ac.cost
436    12.12
437
438Searching for serials:
439
440    >>> result = batch.search(0, 'serial')
441    >>> result
442    [<waeup.sirp...AccessCode object at 0x...>]
443
444    >>> result[0].batch_serial
445    0
446
447Searching for AC-IDs:
448
449    >>> result = batch.search(ac.representation, 'pin')
450    >>> result[0].representation
451    'APP-...-...'
452
453Searching for student IDs:
454
455#    >>> batch.invalidate(ac_id, 'some_new_user_id')
456#    >>> result = batch.search('some_new_user_id', 'stud_id')
457#    >>> result[0].student_id
458#    'some_new_user_id'
459
460Searching for not existing entries will return empty lists:
461
462    >>> batch.search(12, 'serial')
463    []
464
465    >>> batch.search('not-a-valid-pin', 'pin')
466    []
467
468#    >>> batch.search('not-a-student-id', 'stud_id')
469#    []
470
471#    >>> batch.search('blah', 'not-a-valid-searchtype')
472#    []
473
474AccessCodeBatchContainer
475========================
476
477.. class:: AccessCodeBatchContainer()
478
479   A container for access code batches.
480
481   .. method:: addBatch(batch)
482
483      Add a batch in this container. You should make sure, that
484      entries in the given batch are set correctly.
485
486   .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num)
487
488      Create a batch inside this container. Returns the batch created.
489
490      :param creation_date: python datetime
491      :param creator: creators user id
492      :type creator: string
493      :param batch_prefix: prefix of this batch
494      :param cost: cost per access code
495      :type cost: float
496      :param entry_num: number of access codes to create
497
498   .. method:: getNum(prefix)
499
500      Get next unused num for a new batch and a given prefix.
501
502      Batches for a given prefix are numerated. Whenever a new batch
503      is created and other batches inside the container already have
504      the same prefix, the new one will get the lowest unused number.
505
506   .. method:: getImportFiles()
507
508      Get a list of basenames of available import files, suitable for
509      feeding :meth:`reimport`.
510
511   .. method:: reimport(filename, creator=u'UNKNOWN')
512
513      Reimport a CSV log file of previously created AC batch.
514
515      ``filename`` is the name (basename) of the file residing in the
516      accesscode storage's ``import`` directory.``creator`` is the
517      user ID of the current user.
518
519   .. method:: search(search_term, search_type)
520
521      Look in all contained batches for access codes that comply with
522      the given parameters.
523
524      ``search_type`` must be one of ``serial``, ``pin``, or
525      ``stud_id`` and specifies, what kind of data is contained in the
526      ``search_term``.
527
528      - ``serial`` looks for the AC with the ``serial_batch`` set to
529        the given (integer) number in ``search_term``. Here
530        ``search_term`` can be an integer or a string. If it is a
531        string it will be converted to an int.
532
533      - ``pin`` looks for the AC with the ``representation`` set to
534        the given string in ``search_term``. Here ``search_term`` must
535        be a string containing the full AC-ID like
536        ``APP-1-0123456789``.
537
538      - ``stud_id`` looks for the AC with the ``student_id`` set to
539        the given string in ``search_term``. Here ``search_term`` must
540        be a string containing the full student ID.
541
542Examples:
543---------
544
545Creating a batch container:
546
547    >>> from waeup.sirp.accesscodes.accesscodes import (
548    ...   AccessCodeBatchContainer)
549    >>> container = AccessCodeBatchContainer()
550
551Creating batches inside the container:
552
553    >>> from datetime import datetime
554    >>> batch1 = container.createBatch(
555    ...   datetime.now(), 'some userid', 'FOO', 10.12, 5)
556
557    >>> batch2 = container.createBatch(
558    ...   datetime.now(), 'other userid', 'BAR', 1.95, 5)
559
560Searching the container for batch serials:
561
562    >>> container.search(1, 'serial')
563    [<waeup.sirp...AccessCode object at 0x...>,
564     <waeup.sirp...AccessCode object at 0x...>]
565
566    >>> container.search('not-a-number', 'serial')
567    []
568
569    >>> result = container.search('1', 'serial')
570    >>> result
571    [<waeup.sirp...AccessCode object at 0x...>,
572     <waeup.sirp...AccessCode object at 0x...>]
573
574Searching for ACs:
575
576    >>> ac = result[0]
577    >>> container.search(ac.representation, 'pin')
578    [<waeup.sirp...AccessCode object at 0x...>]
579
580Searching for student IDs:
581
582#    >>> ac.__parent__.invalidate(
583#    ...   ac.representation, 'some_user')
584#    >>> container.search('some_user', 'stud_id')
585#    [<waeup.sirp...AccessCode object at 0x...>]
586
587
588Access code plugin
589==================
590
591.. class:: AccessCodePlugin
592
593  A `waeup.sirp` plugin that updates existing SIRP university
594  instances so that they provide support for access-codes.
595
596  .. attribute:: grok.implements(ISIRPPluggable)
597  .. attribute:: grok.name('accesscodes')
598
599  .. method:: setup(site, name, logger)
600
601     Create an accesscodebatch container in ``site``. Any events are
602     logged to ``logger``.
603
604  .. method:: update(site, name, logger)
605
606     Check if ``site`` contains an accesscodebatch container and add
607     it if missing. Any events are logged to ``logger``.
608
609  The AccessCodePlugin is available as a global named utility for the
610  ISIRPPluggable interface named ``accesscodes``.
611
612  It is looked up by a university instance when created, so that this
613  instance has nothing to know about accesscodes at all and can
614  although provide support for them.
615
616    >>> from zope.component import getUtility
617    >>> from waeup.sirp.interfaces import ISIRPPluggable
618    >>> plugin = getUtility(ISIRPPluggable, name='accesscodes')
619    >>> plugin
620    <waeup.sirp.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
621
622  It provides a `setup()` and an `update()` method. Both have to be
623  fed with a site object (the `IUniversity` instance to modify), a
624  logger and a name.
625
626  We create a faked site and a logger:
627
628    >>> import logging
629    >>> faked_site = dict()
630    >>> logger = logging.getLogger('ac_test')
631    >>> logger.setLevel(logging.DEBUG)
632    >>> ch = logging.FileHandler('ac_tests.log', 'w')
633    >>> ch.setLevel(logging.DEBUG)
634    >>> logger.addHandler(ch)
635
636  Now we can install our stuff in the faked site:
637
638    >>> plugin.setup(faked_site, 'blah', logger)
639
640  The faked site now has an access code container:
641
642    >>> faked_site.keys()
643    ['accesscodes']
644
645  The action is described in the log:
646
647    >>> print open('ac_tests.log', 'r').read()
648    Installed container for access code batches.
649
650  We can also update an existing site, by calling `update()`:
651
652    >>> plugin.update(faked_site, 'blah', logger)
653
654  There was nothing to do for the updater:
655
656    >>> print open('ac_tests.log', 'r').read()
657    Installed container for access code batches.
658    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
659
660  But if we remove the created batch container and call the updater, it
661  will create a new one:
662
663    >>> del faked_site['accesscodes']
664    >>> plugin.update(faked_site, 'blah', logger)
665    >>> print open('ac_tests.log', 'r').read()
666    Installed container for access code batches.
667    AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do.
668    Updating site at {}. Installing access codes.
669    Installed container for access code batches.
670
671  Clean up:
672
673    >>> import os
674    >>> os.unlink('ac_tests.log')
Note: See TracBrowser for help on using the repository browser.