Access Codes (aka PINs) *********************** .. module:: waeup.kofa.accesscodes.accesscode Components that represent access codes and related. .. :NOdoctest: .. :NOlayer: waeup.kofa.testing.KofaUnitTestLayer 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.kofa` 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. Access Code =========== .. 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. Access codes can have three states: unused, used, and disabled. While still unused but enabled access codes are reflected by an empty ``invalidation_date`` (set to ``None``), an already used (invalidated) code provides an invalidation date. In case of misuse or similar cases access codes can also be completely disabled by setting the ``disabled`` attribute to ``True``. The class implements :mod:`waeup.kofa.accesscodes.interfaces.IAccessCode`: >>> from waeup.kofa.accesscodes.interfaces import IAccessCode >>> from waeup.kofa.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. If an access-code is disabled, this attribute contains the datetime of disabling. Read-only attribute. Only batches are supposed to set this value. .. attribute:: disabled Boolean, ``False`` by default. When set to ``True``, this access-code should not be used any more. 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') >>> ac.representation '--<10-DIGITS>' Also the ``cost`` will not be set: >>> ac.cost is None True Access Code Batch ================= .. 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.kofa.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:: getAccessCodeForStudentId(student_id) Get the :class:`AccessCode` object for the given ``student_id``. Certain single access codes can be accessed inside a batch by their student_id. The student_id must be some string; ``None`` is not a valid value. 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:: disable(ac_id, user_id) Disable the access-code with ID ``ac_id``. ``user_id`` is the user ID of the user triggering the process. Already disabled ACs are left untouched. Sets also the ``student_id`` and ``invalidation_date`` attributes of the respective :class:`AccessCode` entry. While ``student_id`` is set to the given ``user_id``, ``invalidation_date`` is set to current datetime. Disabled access codes are supposed not to be used any more at all. .. method:: enable(ac_id) (Re-)enable the access-code with ID ``ac_id``. This leaves the given AC in state ``unused``. Already enabled ACs are left untouched. Sets ``student_id`` and ``invalidation_date`` values of the respective :class:`AccessCode` entry to ``None``. .. 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. .. method:: search(searchterm, searchtype) Search the batch for entries that comply with ``searchterm`` and ``searchtype``. ``searchtype`` must be one of ``serial``, ``pin``, or ``stud_id`` and specifies, what kind of data is contained in the ``searchterm``. - ``serial`` looks for the AC with the ``serial_batch`` set to the given (integer) number in ``searchterm``. Here ``searchterm`` must be an int number. - ``pin`` looks for the AC with the ``representation`` set to the given string in ``searchterm``. Here ``searchterm`` must be a string containing the full AC-ID like ``APP-1-0123456789``. - ``stud_id`` looks for the AC with the ``student_id`` set to the given string in ``searchterm``. Here ``searchterm`` must be a string containing the full student ID. Lookup is done via local BTrees and therefore pretty fast. Returns a list of access codes found or empty list. Examples -------- :class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`: >>> from waeup.kofa.accesscodes.interfaces import IAccessCodeBatch >>> from waeup.kofa.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.kofa.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' # #Getting a single entry by student_id: # # >>> batch.getAccessCodeForStudentId('some_user_id') # # #Non-existent values will cause a :exc:`KeyError`: # # >>> batch.getAccessCodeForStudentId('non-existing') # Traceback (most recent call last): # ... # KeyError: 'non-existing' # #Already enabled entries will be left untouched when trying to renable #them: # >>> batch.enable(ac_id) # >>> ac.student_id # 'some_user_id' Disabling an entry: # >>> batch.disabled_num # 0 # # >>> ac.disabled # False # # >>> batch.disable(ac_id, 'some userid') # >>> ac.disabled # True # # >>> ac.student_id # 'some userid' # # >>> batch.disabled_num # 1 Already disabled entries will not be disabled again: # >>> batch.disable(ac_id, 'other userid') # >>> ac.student_id # 'some userid' Reenabling an entry: # >>> batch.enable(ac_id) # >>> ac.disabled # False # # >>> ac.student_id is None # True # # >>> ac.invalidation_date is None # True 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 Searching for serials: >>> result = batch.search(0, 'serial') >>> result [] >>> result[0].batch_serial 0 Searching for AC-IDs: >>> result = batch.search(ac.representation, 'pin') >>> result[0].representation 'APP-...-...' Searching for student IDs: # >>> batch.invalidate(ac_id, 'some_new_user_id') # >>> result = batch.search('some_new_user_id', 'stud_id') # >>> result[0].student_id # 'some_new_user_id' Searching for not existing entries will return empty lists: >>> batch.search(12, 'serial') [] >>> batch.search('not-a-valid-pin', 'pin') [] # >>> batch.search('not-a-student-id', 'stud_id') # [] # >>> batch.search('blah', 'not-a-valid-searchtype') # [] Access Code Batch Container =========================== .. class:: AccessCodeBatchContainer() A container for access code batches. .. method:: addBatch(batch) Add a batch in this container. You should make sure, that entries in the given batch are set correctly. .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num) Create a batch inside this container. Returns the batch created. :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 .. 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. .. method:: getImportFiles() Get a list of basenames of available import files, suitable for feeding :meth:`reimport`. .. method:: reimport(filename, creator=u'UNKNOWN') Reimport a CSV log file of previously created AC batch. ``filename`` is the name (basename) of the file residing in the accesscode storage's ``import`` directory.``creator`` is the user ID of the current user. .. method:: search(search_term, search_type) Look in all contained batches for access codes that comply with the given parameters. ``search_type`` must be one of ``serial``, ``pin``, or ``stud_id`` and specifies, what kind of data is contained in the ``search_term``. - ``serial`` looks for the AC with the ``serial_batch`` set to the given (integer) number in ``search_term``. Here ``search_term`` can be an integer or a string. If it is a string it will be converted to an int. - ``pin`` looks for the AC with the ``representation`` set to the given string in ``search_term``. Here ``search_term`` must be a string containing the full AC-ID like ``APP-1-0123456789``. - ``stud_id`` looks for the AC with the ``student_id`` set to the given string in ``search_term``. Here ``search_term`` must be a string containing the full student ID. Examples -------- Creating a batch container: >>> from waeup.kofa.accesscodes.accesscodes import ( ... AccessCodeBatchContainer) >>> container = AccessCodeBatchContainer() Creating batches inside the container: >>> from datetime import datetime >>> batch1 = container.createBatch( ... datetime.now(), 'some userid', 'FOO', 10.12, 5) >>> batch2 = container.createBatch( ... datetime.now(), 'other userid', 'BAR', 1.95, 5) Searching the container for batch serials: >>> container.search(1, 'serial') [, ] >>> container.search('not-a-number', 'serial') [] >>> result = container.search('1', 'serial') >>> result [, ] Searching for ACs: >>> ac = result[0] >>> container.search(ac.representation, 'pin') [] Searching for student IDs: # >>> ac.__parent__.invalidate( # ... ac.representation, 'some_user') # >>> container.search('some_user', 'stud_id') # [] Access Code Plugin ================== .. class:: AccessCodePlugin A `waeup.kofa` plugin that updates existing Kofa university instances so that they provide support for access codes. .. attribute:: grok.implements(IKofaPluggable) .. 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 IKofaPluggable interface named ``accesscodes``. It is looked up by a university instance when created, so that this instance has nothing to know about accesscodes at all and can although provide support for them. >>> from zope.component import getUtility >>> from waeup.kofa.interfaces import IKofaPluggable >>> plugin = getUtility(IKofaPluggable, 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')