Changeset 5118


Ignore:
Timestamp:
3 Apr 2010, 14:10:12 (15 years ago)
Author:
uli
Message:

Implement completely different design for access-codes and their batches. We drop using zope.catalogs here and care
for the limited set of operations we need on access codes and batches ourselves. This speeds up performance for batch
creation by factor 2 and uses only half the memory in ZODB than what was needed with old PIN implemenentation.

Location:
main/waeup.sirp/trunk/src/waeup/sirp/accesscodes
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py

    r5116 r5118  
    44import grok
    55import os
     6from BTrees.OIBTree import OIBTree
     7from datetime import datetime
    68from random import SystemRandom as random
    79from waeup.sirp.interfaces import IWAeUPSIRPPluggable
     
    1315    grok.name('waeup.manageACBatches')
    1416
    15 class AccessCode(grok.Model):
     17class AccessCode(grok.Context):
    1618    grok.implements(IAccessCode)
    1719
    18     def __init__(self, batch_serial, random_num, cost,
    19                  invalidation_date=None, student_id=None):
     20    def __init__(self, batch_serial, random_num, invalidation_date=None,
     21                 student_id=None):
    2022        self.batch_serial = batch_serial
    2123        self.random_num = random_num
    22         self.cost = cost
    23         self.invalidation_date = invalidation_date
     24        self._invalidation_date = invalidation_date
    2425        self.student_id = student_id
    2526
     
    4546        return self.batch.num
    4647
    47 class AccessCodeBatch(grok.Container):
     48    @property
     49    def cost(self):
     50        if self.batch is None:
     51            return None
     52        return self.batch.cost
     53
     54    @property
     55    def invalidation_date(self):
     56        # We define this as a property to make it unwritable.
     57        # This attribute should be set by the surrounding batch only.
     58        return self._invalidation_date
     59   
     60class AccessCodeBatch(grok.Model):
    4861    """A batch of access codes.
    4962    """
     
    5972        self.entry_num = entry_num
    6073        self.num = num
    61 
    62     def createEntries(self):
     74        self.invalidated_num = 0
     75        self._entries = list()
     76        self._acids = OIBTree()
     77        self._createEntries()
     78
     79    def _createEntries(self):
    6380        """Create the entries for this batch.
    6481        """
    65         rands = self.getNewRandomNum(num=self.entry_num)
    66         for num in range(self.entry_num):
    67             ac = AccessCode(num, rands[num], self.cost)
    68             self[str(num)] = ac
     82        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
     83            ac = AccessCode(num, pin)
     84            ac.__parent__ = self
     85            self._entries.append(ac)
     86            self._acids.update({ac.representation: num})
     87            self._p_changed = True # XXX: most probably not needed.
    6988        return
    70        
    71     def getNewRandomNum(self, num=1):
     89
     90    def _getNewRandomNum(self, num=1):
    7291        """Create a set of ``num`` random numbers of 10 digits each.
    7392
    7493        The number is returned as string.
    7594        """
    76         results = {}
    77         while len(results) < num:
     95        curr = 1
     96        while curr <= num:
    7897            pin = ''
    7998            for x in range(10):
    8099                pin += str(random().randint(0, 9))
    81             results[pin] = True
    82         return results.keys()
    83 
    84     def createCSVLogFile(self):
    85         """Create a CSV file with data in batch.
    86 
    87         Data will not contain invalidation date nor student ids.  File
    88         will be created in ``accesscodes`` subdir of data center
    89         storage path.
    90 
    91         Returns name of created file.
     100            if '%s-%s-%s' % (self.prefix, self.num, pin) in self._acids:
     101                # PIN already in use
     102                continue
     103            curr += 1
     104            yield pin
     105
     106    def _getStoragePath(self):
     107        """Get the directory, where we store all batch-related CSV files.
    92108        """
    93109        site = grok.getSite()
     
    96112        if not os.path.exists(ac_storage):
    97113            os.mkdir(ac_storage)
     114        return ac_storage
     115
     116    def entries(self):
     117        """Get all entries of this batch as generator.
     118        """
     119        for x in self._entries:
     120            yield x
     121           
     122    def getAccessCode(self, ac_id):
     123        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
     124        """
     125        return self._entries[self._acids[ac_id]]
     126
     127    def invalidate(self, ac_id, student_id=None):
     128        """Invalidate the AC with ID ``ac_id``.
     129        """
     130        num = self._acids[ac_id]
     131        ac = self.getAccessCode(ac_id)
     132        ac._invalidation_date = datetime.now()
     133        ac.student_id = student_id
     134        self.invalidated_num += 1
     135
     136    def createCSVLogFile(self):
     137        """Create a CSV file with data in batch.
     138
     139        Data will not contain invalidation date nor student ids.  File
     140        will be created in ``accesscodes`` subdir of data center
     141        storage path.
     142
     143        Returns name of created file.
     144        """
    98145        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
     146        ac_storage = self._getStoragePath()
    99147        csv_path = os.path.join(
    100148            ac_storage, '%s-%s-%s-%s.csv' % (
     
    104152        writer.writerow(['serial', 'ac', 'cost'])
    105153        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
    106         for key, value in self.items():
     154
     155        for value in self._entries:
    107156            writer.writerow(
    108157                [str(value.batch_serial), str(value.representation)]
    109158                )
     159        site = grok.getSite()
    110160        logger = site.logger
    111161        logger.info(
     
    115165        return os.path.basename(csv_path)
    116166
    117    
     167    def archive(self):
     168        """Create a CSV file for archive.
     169        """
     170        ac_storage = self._getStoragePath()
     171        now = datetime.now()
     172        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
     173        csv_path = os.path.join(
     174            ac_storage, '%s-%s_archive-%s-%s.csv' % (
     175                self.prefix, self.num, timestamp, self.creator)
     176            )
     177        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
     178        writer.writerow(['prefix', 'serial', 'ac', 'student', 'date'])
     179        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
     180                         str(self.entry_num)])
     181        for value in self._entries:
     182            date = ''
     183            stud_id = ''
     184            if value.invalidation_date is not None:
     185                date = value.invalidation_date.strftime(
     186                    '%Y-%m-%d-%H-%M-%S')
     187            if stud_id is not None:
     188                stud_id = value.student_id
     189            writer.writerow([
     190                    self.prefix, value.batch_serial, value.representation,
     191                    stud_id, date
     192                    ])
     193        return os.path.basename(csv_path)
     194
    118195class AccessCodeBatchContainer(grok.Container):
    119196    grok.implements(IAccessCodeBatchContainer)
     
    125202        key = "%s-%s" % (batch.prefix, batch.num)
    126203        self[key] = batch
    127         batch.createEntries()
    128204        self._p_changed = True
    129205
  • main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.txt

    r5109 r5118  
    88.. :doctest:
    99
    10 Access codes are created as parts of batches. We therefore have to
    11 create a batch first.
    12 
    13 Here we create a batch of three entries, with a cost of ``12.12`` per
     10About access-codes
     11==================
     12
     13Access codes are ids used to grant first-time access to the system for
     14students.
     15
     16They are normally not generated by third-party components but always
     17part of batches.
     18
     19An access-code consists of three parts::
     20
     21    APP-12-0123456789
     22    ^^^ ^^ ^^^^^^^^^^
     23     A  B  C
     24
     25where ``A`` tells about the purpose of the code, ``B`` gives the
     26number of batch the code belongs to (1 to 3 digits), and ``C`` is a
     27unique random number of 10 digits.
     28
     29For the generation of the random number :mod:`waeup.sirp` requires a
     30'urandom' entropy provider which is available with most standard
     31Unix/Linux systems. This makes the generated numbers relatively
     32secure, especially when compared with recent PHP-based applications.
     33
     34
     35AccessCode
     36==========
     37
     38.. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]])
     39
     40   You normally shouldn't create standalone access-codes. Use
     41   instances of :class:`AccessCodeBatch` instead as they generate them
     42   (in masses) and care for them.
     43
     44   Note, that :class:`AccessCode` instances are not persistent on
     45   themselves. They have to be stored inside a persistent object (like
     46   :class:`AccessCodeBatch`) to be kept.
     47
     48   The class implements
     49   :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`:
     50
     51    >>> from waeup.sirp.accesscodes.interfaces import IAccessCode
     52    >>> from waeup.sirp.accesscodes.accesscodes import AccessCode
     53    >>> from zope.interface.verify import verifyClass
     54    >>> verifyClass(IAccessCode, AccessCode)
     55    True
     56
     57   .. attribute:: representation
     58
     59      The 'full' id of an access-code as described above. Something
     60      like ``'APP-12-0123456789'``.
     61
     62      Read-only attribute.
     63
     64   .. attribute:: batch_serial
     65
     66      Serial number of this access-code inside the batch.
     67
     68      .. note:: XXX: Do we really need this?
     69
     70         Subject to be dropped.
     71
     72   .. attribute:: student_id
     73
     74      A string or ``None``. Set when an access-code is
     75      invalidated. ``None`` by default.
     76
     77   .. attribute:: batch_prefix
     78
     79      The prefix of the batch the access-code belongs to.
     80
     81      Read-only attribute.
     82
     83   .. attribute:: batch_num
     84
     85      The number of the batch the access-code belongs to.
     86
     87      Read-only attribute.
     88
     89   .. attribute:: cost
     90
     91      What the access-code costs. A float.
     92
     93      Read-only attribute.
     94
     95   .. attribute:: invalidation_date
     96
     97      Python datetime when the access code was invalidated, or
     98      ``None``. ``None`` by default.
     99
     100      Read-only attribute. Only batches are supposed to set this value.
     101
     102   Access codes that are not part of a batch, will give strange
     103   representations:
     104
     105    >>> ac = AccessCode(None, '9999999999', 12.12)
     106    >>> ac.representation
     107    '--<10-DIGITS>'
     108
     109
     110AccessCodeBatch
     111===============
     112
     113.. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num)
     114
     115   Create a batch of access-codes.
     116
     117   :param creation_date: python datetime
     118   :param creator: creators user id
     119   :type creator: string
     120   :param batch_prefix: prefix of this batch
     121   :param cost: cost per access code
     122   :type cost: float
     123   :param entry_num: number of access codes to create
     124   :param num: number of this batch
     125
     126   A persistent :class:`grok.Model`. It implements
     127   :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`.
     128
     129   When creating a batch, all entries (access-codes) are generated as
     130   well.
     131
     132   .. attribute:: creation_date
     133
     134      The datetime when the batch was created.
     135
     136   .. attribute:: creator
     137
     138      String with user id of the user that generated the batch.
     139
     140   .. attribute:: cost
     141
     142      Float representing the costs for a single access-code. All
     143      entries inside the batch share the same cost.
     144
     145   .. attribute:: entry_num
     146
     147      Number of entries (access-codes) inside the batch.
     148
     149   .. attribute:: invalidated_num
     150
     151      Number of entries that were already invalidated.
     152
     153   .. attribute:: prefix
     154
     155      Prefix of the batch. This tells about the purpose of this batch.
     156
     157   .. attribute:: num
     158
     159      Number of this batch. For a certain prefix there can exist
     160      several batches, which are numbered in increasing order. The
     161      number is normally computed by the
     162      :class:`AccessCodeBatchContainer` in which batches are
     163      stored.
     164
     165      .. seealso:: :class:`AccessCodeBatchContainer`
     166
     167   .. method:: entries()
     168
     169      Get all accesscodes stored in the batch.
     170
     171      Returns a generator over all stored entries.
     172
     173   .. method:: getAccessCode(acesscode_id)
     174
     175      Get the :class:`AccessCode` object for the given
     176      ``accesscode_id``.
     177
     178      Certain single access codes can be accessed inside a batch by
     179      their representation (i.e. something like ``'APP-12-0123456789'``.
     180
     181      When a code cannot be found :exc:`KeyError` is raised.
     182
     183   .. method:: invalidate(ac_id[, student_id=None])
     184
     185      Invalidate the access-code with ID ``ac_id``.
     186
     187      Sets also the ``student_id`` attribute of the respective
     188      :class:`AccessCode` entry.
     189
     190   .. method:: createCSVLogFile()
     191
     192      Create a CSV file with data in batch.
     193
     194      Data will not contain invalidation date nor student ids.  File
     195      will be created in ``accesscodes`` subdir of data center storage
     196      path.
     197
     198      Returns name of created file.
     199
     200   .. method:: archive()
     201
     202      Create a CSV file for archive. Archive files contain also
     203      ``student_id`` and ``invalidation_date``.
     204
     205      Returns name of created file.
     206
     207
     208Examples
     209--------
     210
     211:class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`:
     212
     213    >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch
     214    >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch
     215    >>> from zope.interface.verify import verifyClass
     216    >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
     217    True
     218
     219Creating a batch of three access-codes, with a cost of ``12.12`` per
    14220code, the batch prefix ``APP``, batch number ``10``, creator ID
    15221``Fred`` and some arbitrary creation date:
     
    20226    ...   datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10)
    21227
    22 The entries of a batch have to be created manually. This is, because
    23 we cannot add persistent objects in a still not persisted container:
    24 
    25     >>> batch.createEntries()
    26 
    27 Now we have three accesscodes stored in the batch:
    28 
    29     >>> sorted(list(batch.items()))
    30     [(u'0', <waeup.sirp...AccessCode object at 0x...), ...]
    31 
    32 As we can see, access codes entries can be found in a batch by their
    33 (stringified) serial number.
    34 
    35 Each accesscode has a representation:
    36 
    37     >>> ac = batch['0']
    38     >>> ac.representation
    39     'APP-10-<10-DIGITS>'
    40 
    41 The main point about a batch is that it can act as a dictionary for
    42 the generated access codes:
    43 
    44     >>> ac_codes = list(batch.values())
     228Getting all access-codes from a batch:
     229
     230    >>> ac_codes = batch.entries()
     231    >>> ac_codes
     232    <generator object at 0x...>
     233
    45234    >>> [x.representation for x in ac_codes]
    46     ['APP-10-...', 'APP-10-...', 'APP-10-...']
    47 
    48 `Accesscode` and `AccessCodeBatch` classes implement the respective
    49 interfaces:
    50 
    51     >>> from waeup.sirp.accesscodes.accesscodes import (
    52     ...   AccessCode, AccessCodeBatch)
    53     >>> from waeup.sirp.accesscodes.interfaces import(
    54     ...   IAccessCode, IAccessCodeBatch)
    55     >>> from zope.interface.verify import verifyClass
    56     >>> verifyClass(IAccessCode, AccessCode)
     235    ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>']
     236
     237    >>> [x for x in batch.entries()]
     238    [<waeup.sirp...AccessCode object at 0x...>, ...]
     239
     240Getting a single entry from the batch:
     241
     242    >>> ac_id = list(batch.entries())[0].representation
     243    >>> ac = batch.getAccessCode(ac_id)
     244    >>> ac
     245    <waeup.sirp...AccessCode object at 0x...>
     246
     247    >>> ac is list(batch.entries())[0]
    57248    True
    58249
    59     >>> verifyClass(IAccessCodeBatch, AccessCodeBatch)
    60     True
    61 
    62 Access codes without that are not part of a batch, will give strange
    63 representations:
    64 
    65     >>> ac = AccessCode(None, '9999999999', 12.12)
    66     >>> ac.representation
    67     '--<10-DIGITS>'
     250Trying to get a single not-existent entry from a batch:
     251
     252    >>> batch.getAccessCode('blah')
     253    Traceback (most recent call last):
     254    ...
     255    KeyError: 'blah'
     256
     257Invalidating an entry:
     258
     259    >>> batch.invalidated_num
     260    0
     261
     262    >>> str(ac.invalidation_date), str(ac.student_id)
     263    ('None', 'None')
     264
     265    >>> batch.invalidate(ac_id)
     266    >>> batch.invalidated_num
     267    1
     268
     269    >>> ac.invalidation_date , str(ac.student_id)
     270    (datetime.datetime(...), 'None')
     271
     272    >>> batch.invalidate(ac_id, 'some_user_id')
     273    >>> ac.student_id
     274    'some_user_id'
     275
    68276
    69277Access code plugin
  • main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/browser_templates/batchcontainer.pt

    r5103 r5118  
    44  The following batches are available:
    55</p>
    6 <table>
    7   <thead>
    8     <tr>
    9       <th>Prefix</th><th>Entries/(invalidated)</th><th>Cost</th>
    10       <th>Created</th><th>Creator</th>
    11     </tr>
    12   </thead>
    13   <tbody>
    14     <tr tal:repeat="batch context/values"
    15         tal:attributes="class python: repeat['batch'].odd() and 'even' or 'odd'">
    16       <td>
    17         <span tal:replace="batch/prefix">APP</span>
    18         -
    19         <span tal:replace="batch/num">10</span>
    20       </td>
    21       <td>
    22         <span tal:replace="batch/entry_num">1012</span>
    23         /
    24         <span tal:replace="python: view.invalidated(batch)">512</span>
    25       </td>
    26       <td tal:content="batch/cost">12.12</td>
    27       <td tal:content="python: batch.creation_date.ctime()">2009/12/24</td>
    28       <td tal:content="batch/creator">some user</td>
    29     </tr>
    30     <tr tal:condition="not: context/values">
    31       <td colspan="5"><b>No batches yet</b></td>
    32     </tr>
    33   </tbody>
    34 </table>
     6<form method="POST">
     7  <table>
     8    <thead>
     9      <tr>
     10        <th>&nbsp;</th>
     11        <th>Prefix</th><th>Entries/(invalidated)</th><th>Cost</th>
     12        <th>Created</th><th>Creator</th>
     13      </tr>
     14    </thead>
     15    <tbody>
     16      <tr tal:repeat="batch context/values"
     17          tal:attributes="class python: repeat['batch'].odd() and 'even' or 'odd'">
     18        <td>
     19          <input type="checkbox" name="batches" value="batch/prefix"
     20                 tal:attributes="value batch/__name__" />
     21        </td>
     22        <td>
     23          <span tal:replace="batch/prefix">APP</span>
     24          -
     25          <span tal:replace="batch/num">10</span>
     26        </td>
     27        <td>
     28          <span tal:replace="batch/entry_num">1012</span>
     29          /
     30          <span tal:replace="batch/invalidated_num">512</span>
     31        </td>
     32        <td tal:content="batch/cost">12.12</td>
     33        <td tal:content="python: batch.creation_date.ctime()">2009/12/24</td>
     34        <td tal:content="batch/creator">some user</td>
     35      </tr>
     36      <tr tal:condition="not: context/values">
     37        <td colspan="5"><b>No batches yet</b></td>
     38      </tr>
     39    </tbody>
     40  </table>
     41  <input type="submit" name="archive" value="Archive" />
     42  <input type="submit" name="delete" value="Archive and Delete" />
     43</form>
Note: See TracChangeset for help on using the changeset viewer.