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

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

Rename applicants.py, accesscodes.py and students.py modules. Their names should be singular like in the university package.

File size: 18.9 KB
RevLine 
[6632]1:mod:`waeup.sirp.accesscodes.accesscode` -- access codes (aka PINs)
2*******************************************************************
[5068]3
[6632]4.. module:: waeup.sirp.accesscodes.accesscode
[5068]5
[5080]6Components that represent access codes and related.
[5068]7
[6417]8.. :NOdoctest:
9.. :NOlayer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer
[5068]10
[5118]11About access-codes
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
[5118]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.
[5068]34
35
[5118]36AccessCode
37==========
[5068]38
[5118]39.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
[5068]40
[5118]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.
[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
[5150]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
[5118]58   The class implements
59   :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`:
[5068]60
[5118]61    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
62    >>> from waeup.sirp.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
[5118]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
[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
300      Returns a list of access-codes found or empty list.
301
302
[5118]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
[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()]
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
[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')
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:
[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
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
[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
[5122]474AccessCodeBatchContainer
475========================
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
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
[6413]582#    >>> ac.__parent__.invalidate(
583#    ...   ac.representation, 'some_user')
584#    >>> container.search('some_user', 'stud_id')
585#    [<waeup.sirp...AccessCode object at 0x...>]
[5150]586
587
[5109]588Access code plugin
589==================
590
591.. class:: AccessCodePlugin
592
[5138]593  A `waeup.sirp` plugin that updates existing WAeUP SIRP university
594  instances so that they provide support for access-codes.
595
[5109]596  .. attribute:: grok.implements(IWAeUPSIRPPluggable)
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  IWAeUPSIRPPluggable interface named ``accesscodes``.
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
617    >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable
618    >>> plugin = getUtility(IWAeUPSIRPPluggable, 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.