:mod:`waeup.sirp.accesscodes.accesscodes` -- access codes (aka PINs) ******************************************************************** .. module:: waeup.sirp.accesscodes.accesscodes Components that represent access codes and related. .. :doctest: About access-codes ================== Access codes are ids used to grant first-time access to the system for students. They are normally not generated by third-party components but always part of batches. An access-code consists of three parts:: APP-12-0123456789 ^^^ ^^ ^^^^^^^^^^ A B C where ``A`` tells about the purpose of the code, ``B`` gives the number of batch the code belongs to (1 to 3 digits), and ``C`` is a unique random number of 10 digits. For the generation of the random number :mod:`waeup.sirp` requires a 'urandom' entropy provider which is available with most standard Unix/Linux systems. This makes the generated numbers relatively secure, especially when compared with recent PHP-based applications. AccessCode ========== .. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]]) You normally shouldn't create standalone access-codes. Use instances of :class:`AccessCodeBatch` instead as they generate them (in masses) and care for them. Note, that :class:`AccessCode` instances are not persistent on themselves. They have to be stored inside a persistent object (like :class:`AccessCodeBatch`) to be kept. The class implements :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`: >>> from waeup.sirp.accesscodes.interfaces import IAccessCode >>> from waeup.sirp.accesscodes.accesscodes import AccessCode >>> from zope.interface.verify import verifyClass >>> verifyClass(IAccessCode, AccessCode) True .. attribute:: representation The 'full' id of an access-code as described above. Something like ``'APP-12-0123456789'``. Read-only attribute. .. attribute:: batch_serial Serial number of this access-code inside the batch. .. note:: XXX: Do we really need this? Subject to be dropped. .. attribute:: student_id A string or ``None``. Set when an access-code is invalidated. ``None`` by default. .. attribute:: batch_prefix The prefix of the batch the access-code belongs to. Read-only attribute. .. attribute:: batch_num The number of the batch the access-code belongs to. Read-only attribute. .. attribute:: cost What the access-code costs. A float. Read-only attribute. .. attribute:: invalidation_date Python datetime when the access code was invalidated, or ``None``. ``None`` by default. Read-only attribute. Only batches are supposed to set this value. Access codes that are not part of a batch, will give strange representations: >>> ac = AccessCode(None, '9999999999', 12.12) >>> ac.representation '--<10-DIGITS>' AccessCodeBatch =============== .. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num) Create a batch of access-codes. :param creation_date: python datetime :param creator: creators user id :type creator: string :param batch_prefix: prefix of this batch :param cost: cost per access code :type cost: float :param entry_num: number of access codes to create :param num: number of this batch A persistent :class:`grok.Model`. It implements :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`. When creating a batch, all entries (access-codes) are generated as well. .. attribute:: creation_date The datetime when the batch was created. .. attribute:: creator String with user id of the user that generated the batch. .. attribute:: cost Float representing the costs for a single access-code. All entries inside the batch share the same cost. .. attribute:: entry_num Number of entries (access-codes) inside the batch. .. attribute:: invalidated_num Number of entries that were already invalidated. .. attribute:: prefix Prefix of the batch. This tells about the purpose of this batch. .. attribute:: num Number of this batch. For a certain prefix there can exist several batches, which are numbered in increasing order. The number is normally computed by the :class:`AccessCodeBatchContainer` in which batches are stored. .. seealso:: :class:`AccessCodeBatchContainer` .. method:: entries() Get all accesscodes stored in the batch. Returns a generator over all stored entries. .. method:: getAccessCode(acesscode_id) Get the :class:`AccessCode` object for the given ``accesscode_id``. Certain single access codes can be accessed inside a batch by their representation (i.e. something like ``'APP-12-0123456789'``. When a code cannot be found :exc:`KeyError` is raised. .. method:: addAccessCode(serial_num, random_num) Add an access code to the batch. ``serial_num`` denotes the serial number of the new access-code inside the batch. ``random_num`` is a string of 10 digits unique in this batch. .. method:: invalidate(ac_id[, student_id=None]) Invalidate the access-code with ID ``ac_id``. Sets also the ``student_id`` attribute of the respective :class:`AccessCode` entry. .. method:: createCSVLogFile() Create a CSV file with data in batch. Data will not contain invalidation date nor student ids. File will be created in ``accesscodes`` subdir of data center storage path. Returns name of created file. .. method:: archive() Create a CSV file for archive. Archive files contain also ``student_id`` and ``invalidation_date``. Returns name of created file. Examples -------- :class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`: >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch >>> from zope.interface.verify import verifyClass >>> verifyClass(IAccessCodeBatch, AccessCodeBatch) True Creating a batch of three access-codes, with a cost of ``12.12`` per code, the batch prefix ``APP``, batch number ``10``, creator ID ``Fred`` and some arbitrary creation date: >>> import datetime >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch >>> batch = AccessCodeBatch( ... datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10) Getting all access-codes from a batch: >>> ac_codes = batch.entries() >>> ac_codes >>> [x.representation for x in ac_codes] ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>'] >>> [x for x in batch.entries()] [, ...] Getting a single entry from the batch: >>> ac_id = list(batch.entries())[0].representation >>> ac = batch.getAccessCode(ac_id) >>> ac >>> ac is list(batch.entries())[0] True Trying to get a single not-existent entry from a batch: >>> batch.getAccessCode('blah') Traceback (most recent call last): ... KeyError: 'blah' Invalidating an entry: >>> batch.invalidated_num 0 >>> str(ac.invalidation_date), str(ac.student_id) ('None', 'None') >>> batch.invalidate(ac_id) >>> batch.invalidated_num 1 >>> ac.invalidation_date , str(ac.student_id) (datetime.datetime(...), 'None') >>> batch.invalidate(ac_id, 'some_user_id') >>> ac.student_id 'some_user_id' Access codes get their ``cost`` from the batch they belong to. Note, that it is advisable to print costs always using a format, as Python floats are often represented by irritating values: >>> ac.cost 12.119999999999999 >>> print "%0.2f" % ac.cost 12.12 AccessCodeBatchContainer ======================== .. class:: AccessCodeBatchContainer() A container for access code batches. .. method:: addBatch(batch) Add a batch in this container. .. method:: getNum(prefix) Get next unused num for a new batch and a given prefix. Batches for a given prefix are numerated. Whenever a new batch is created and other batches inside the container already have the same prefix, the new one will get the lowest unused number. Access code plugin ================== .. class:: AccessCodePlugin .. attribute:: grok.implements(IWAeUPSIRPPluggable) .. attribute:: grok.name('accesscodes') .. method:: setup(site, name, logger) Create an accesscodebatch container in ``site``. Any events are logged to ``logger``. .. method:: update(site, name, logger) Check if ``site`` contains an accesscodebatch container and add it if missing. Any events are logged to ``logger``. The AccessCodePlugin is available as a global named utility for the IWAeUPSIRPPluggable interface named ``accesscodes``. It is looked up by a university instance when created. >>> from zope.component import getUtility >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable >>> plugin = getUtility(IWAeUPSIRPPluggable, name='accesscodes') >>> plugin It provides a `setup()` and an `update()` method. Both have to be fed with a site object (the `IUniversity` instance to modify), a logger and a name. We create a faked site and a logger: >>> import logging >>> faked_site = dict() >>> logger = logging.getLogger('ac_test') >>> logger.setLevel(logging.DEBUG) >>> ch = logging.FileHandler('ac_tests.log', 'w') >>> ch.setLevel(logging.DEBUG) >>> logger.addHandler(ch) Now we can install our stuff in the faked site: >>> plugin.setup(faked_site, 'blah', logger) The faked site now has an access code container: >>> faked_site.keys() ['accesscodes'] The action is described in the log: >>> print open('ac_tests.log', 'r').read() Installed container for access code batches. We can also update an existing site, by calling `update()`: >>> plugin.update(faked_site, 'blah', logger) There was nothing to do for the updater: >>> print open('ac_tests.log', 'r').read() Installed container for access code batches. AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do. But if we remove the created batch container and call the updater, it will create a new one: >>> del faked_site['accesscodes'] >>> plugin.update(faked_site, 'blah', logger) >>> print open('ac_tests.log', 'r').read() Installed container for access code batches. AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do. Updating site at {}. Installing access codes. Installed container for access code batches. Clean up: >>> import os >>> os.unlink('ac_tests.log')