source: main/waeup.kofa/trunk/src/waeup/kofa/doctests/accesscode.txt @ 12935

Last change on this file since 12935 was 12935, checked in by Henrik Bettermann, 9 years ago

Move doctest.

File size: 18.8 KB
RevLine 
[12920]1Access Codes (aka PINs)
2***********************
[5068]3
[7811]4.. module:: waeup.kofa.accesscodes.accesscode
[5068]5
[5080]6Components that represent access codes and related.
[5068]7
[6417]8.. :NOdoctest:
[7819]9.. :NOlayer: waeup.kofa.testing.KofaUnitTestLayer
[5068]10
[12920]11About Access Codes
[5118]12==================
[5068]13
[5118]14Access codes are ids used to grant first-time access to the system for
15students.
[5080]16
[5118]17They are normally not generated by third-party components but always
18part of batches.
[5068]19
[5118]20An access-code consists of three parts::
[5087]21
[5118]22    APP-12-0123456789
23    ^^^ ^^ ^^^^^^^^^^
24     A  B  C
[5087]25
[5118]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.
[5068]29
[7811]30For the generation of the random number :mod:`waeup.kofa` requires a
[5118]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.
[5068]34
35
[12920]36Access Code
37===========
[5068]38
[5118]39.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
[5068]40
[12920]41   You normally shouldn't create standalone access codes. Use
[5118]42   instances of :class:`AccessCodeBatch` instead as they generate them
43   (in masses) and care for them.
[5068]44
[5118]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.
[5080]48
[12920]49   Access codes can have three states: unused, used, and
50   disabled. While still unused but enabled access codes are reflected
[5150]51   by an empty ``invalidation_date`` (set to ``None``), an already
52   used (invalidated) code provides an invalidation date.
53
[12920]54   In case of misuse or similar cases access codes can also be
[5150]55   completely disabled by setting the ``disabled`` attribute to
56   ``True``.
57
[5118]58   The class implements
[7811]59   :mod:`waeup.kofa.accesscodes.interfaces.IAccessCode`:
[5068]60
[7811]61    >>> from waeup.kofa.accesscodes.interfaces import IAccessCode
62    >>> from waeup.kofa.accesscodes.accesscodes import AccessCode
[5068]63    >>> from zope.interface.verify import verifyClass
64    >>> verifyClass(IAccessCode, AccessCode)
65    True
66
[5118]67   .. attribute:: representation
[5109]68
[5118]69      The 'full' id of an access-code as described above. Something
70      like ``'APP-12-0123456789'``.
[5109]71
[5118]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
[5150]110      If an access-code is disabled, this attribute contains the
111      datetime of disabling.
112
[5118]113      Read-only attribute. Only batches are supposed to set this value.
114
[5150]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
[5118]122   Access codes that are not part of a batch, will give strange
123   representations:
124
[5128]125    >>> ac = AccessCode(None, '9999999999')
[5109]126    >>> ac.representation
127    '--<10-DIGITS>'
128
[5128]129   Also the ``cost`` will not be set:
[5118]130
[5128]131    >>> ac.cost is None
132    True
133
134
[12920]135Access Code Batch
136=================
[5118]137
138.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
139
[12920]140   Create a batch of access codes.
[5118]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
[7811]152   :class:`waeup.kofa.accesscodes.interfaces.IAccessCodeBatch`.
[5118]153
[12920]154   When creating a batch, all entries (access codes) are generated as
[5118]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
[12920]172      Number of entries (access codes) inside the batch.
[5118]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
[5150]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
[5122]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
[5118]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
[5150]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
[5118]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
[5150]276   .. method:: search(searchterm, searchtype)
[5118]277
[5150]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
[12920]300      Returns a list of access codes found or empty list.
[5150]301
302
[5118]303Examples
304--------
305
306:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
307
[7811]308    >>> from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch
309    >>> from waeup.kofa.accesscodes.accesscodes import AccessCodeBatch
[5118]310    >>> from zope.interface.verify import verifyClass
311    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
312    True
313
[12920]314Creating a batch of three access codes, with a cost of ``12.12`` per
[5118]315code, the batch prefix ``APP``, batch number ``10``, creator ID
316``Fred`` and some arbitrary creation date:
317
318    >>> import datetime
[7811]319    >>> from waeup.kofa.accesscodes.accesscodes import AccessCodeBatch
[5118]320    >>> batch = AccessCodeBatch(
321    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
322
[12920]323Getting all access codes from a batch:
[5118]324
325    >>> ac_codes = batch.entries()
326    >>> ac_codes
[6334]327    <generator object...at 0x...>
[5118]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()]
[7811]333    [<waeup.kofa...AccessCode object at 0x...>, ...]
[5118]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
[7811]340    <waeup.kofa...AccessCode object at 0x...>
[5118]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
[6413]357#
358#    >>> str(ac.invalidation_date), str(ac.student_id)
359#    ('None', 'None')
360#
[5118]361
362    >>> batch.invalidate(ac_id)
363    >>> batch.invalidated_num
364    1
365
[6413]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')
[7811]376#    <waeup.kofa...AccessCode object at 0x...>
[6413]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:
[5118]387
[6413]388#    >>> batch.enable(ac_id)
389#    >>> ac.student_id
390#    'some_user_id'
[5118]391
[5150]392Disabling an entry:
393
[6413]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
[5150]409
410Already disabled entries will not be disabled again:
411
[6413]412#    >>> batch.disable(ac_id, 'other userid')
413#    >>> ac.student_id
414#    'some userid'
[5150]415
416Reenabling an entry:
417
[6413]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
[5150]427
[5122]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:
[5118]431
[5122]432    >>> ac.cost
433    12.119999999999999
434
435    >>> print "%0.2f" % ac.cost
436    12.12
437
[5150]438Searching for serials:
[5122]439
[5150]440    >>> result = batch.search(0, 'serial')
441    >>> result
[7811]442    [<waeup.kofa...AccessCode object at 0x...>]
[5150]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
[6413]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'
[5150]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
[6413]468#    >>> batch.search('not-a-student-id', 'stud_id')
469#    []
[5150]470
[6413]471#    >>> batch.search('blah', 'not-a-valid-searchtype')
472#    []
[5150]473
[12920]474Access Code Batch Container
475===========================
[5122]476
477.. class:: AccessCodeBatchContainer()
478
479   A container for access code batches.
480
481   .. method:: addBatch(batch)
482
[5128]483      Add a batch in this container. You should make sure, that
484      entries in the given batch are set correctly.
[5122]485
[5128]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
[5122]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
[5132]506   .. method:: getImportFiles()
[5122]507
[5132]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
[5150]519   .. method:: search(search_term, search_type)
[5132]520
[5150]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
[12920]542Examples
543--------
[5150]544
545Creating a batch container:
546
[7811]547    >>> from waeup.kofa.accesscodes.accesscodes import (
[5150]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')
[7811]563    [<waeup.kofa...AccessCode object at 0x...>,
564     <waeup.kofa...AccessCode object at 0x...>]
[5150]565
566    >>> container.search('not-a-number', 'serial')
567    []
568
569    >>> result = container.search('1', 'serial')
570    >>> result
[7811]571    [<waeup.kofa...AccessCode object at 0x...>,
572     <waeup.kofa...AccessCode object at 0x...>]
[5150]573
574Searching for ACs:
575
576    >>> ac = result[0]
577    >>> container.search(ac.representation, 'pin')
[7811]578    [<waeup.kofa...AccessCode object at 0x...>]
[5150]579
580Searching for student IDs:
581
[6413]582#    >>> ac.__parent__.invalidate(
583#    ...   ac.representation, 'some_user')
584#    >>> container.search('some_user', 'stud_id')
[7811]585#    [<waeup.kofa...AccessCode object at 0x...>]
[5150]586
587
[12920]588Access Code Plugin
[5109]589==================
590
591.. class:: AccessCodePlugin
592
[7819]593  A `waeup.kofa` plugin that updates existing Kofa university
[12920]594  instances so that they provide support for access codes.
[5138]595
[7819]596  .. attribute:: grok.implements(IKofaPluggable)
[5109]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
[7819]610  IKofaPluggable interface named ``accesscodes``.
[5109]611
[5138]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.
[5109]615
616    >>> from zope.component import getUtility
[7819]617    >>> from waeup.kofa.interfaces import IKofaPluggable
618    >>> plugin = getUtility(IKofaPluggable, name='accesscodes')
[5109]619    >>> plugin
[7811]620    <waeup.kofa.accesscodes.accesscodes.AccessCodePlugin object at 0x...>
[5109]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.