source: main/waeup.sirp/trunk/src/waeup/sirp/accesscodes/accesscodes.py @ 6418

Last change on this file since 6418 was 6418, checked in by uli, 14 years ago

Oops. Too much copy'n'paste.

File size: 15.4 KB
RevLine 
[5068]1"""Components to handle access codes.
2"""
[5110]3import csv
[5068]4import grok
[5110]5import os
[5118]6from BTrees.OIBTree import OIBTree
7from datetime import datetime
[6413]8from hurry.workflow.interfaces import (
9    IWorkflowInfo, InvalidTransitionError, IWorkflowState)
[5068]10from random import SystemRandom as random
[5073]11from waeup.sirp.interfaces import IWAeUPSIRPPluggable
[5079]12from waeup.sirp.accesscodes.interfaces import (
13    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
14    )
[6413]15from waeup.sirp.accesscodes.workflow import DISABLED, USED
[5068]16
[5102]17class ManageACBatches(grok.Permission):
18    grok.name('waeup.manageACBatches')
19
[6359]20class AccessCode(grok.Model):
[5068]21    grok.implements(IAccessCode)
22
[6386]23    def __init__(self, batch_serial, random_num):
[5068]24        self.batch_serial = batch_serial
25        self.random_num = random_num
[6359]26        IWorkflowInfo(self).fireTransition('init')
[5068]27
28    @property
29    def representation(self):
30        return '%s-%s-%s' % (
31            self.batch_prefix, self.batch_num, self.random_num)
32
[5079]33    @property
34    def batch(self):
35        return getattr(self, '__parent__', None)
[5086]36
[5079]37    @property
38    def batch_prefix(self):
39        if self.batch is None:
40            return ''
41        return self.batch.prefix
[5086]42
[5079]43    @property
44    def batch_num(self):
45        if self.batch is None:
46            return ''
47        return self.batch.num
[5068]48
[5118]49    @property
50    def cost(self):
51        if self.batch is None:
52            return None
53        return self.batch.cost
54
[6413]55    @property
56    def disabled(self):
57        return IWorkflowState(self).getState() == DISABLED
58
59    @property
60    def used(self):
61        return IWorkflowState(self).getState() == USED
62
[6417]63class AccessCodeBatch(grok.Container):
[5068]64    """A batch of access codes.
65    """
66    grok.implements(IAccessCodeBatch)
67
[5086]68    def __init__(self, creation_date, creator, batch_prefix, cost,
69                 entry_num, num=None):
[5079]70        super(AccessCodeBatch, self).__init__()
[5068]71        self.creation_date = creation_date
72        self.creator = creator
[5116]73        self.prefix = batch_prefix.upper()
[5068]74        self.cost = cost
75        self.entry_num = entry_num
[5086]76        self.num = num
[5118]77        self.invalidated_num = 0
[5149]78        self.disabled_num = 0
[5079]79
[5118]80    def _createEntries(self):
[5079]81        """Create the entries for this batch.
82        """
[5118]83        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
[5121]84            self.addAccessCode(num, pin)
[5112]85        return
[5118]86
87    def _getNewRandomNum(self, num=1):
[5112]88        """Create a set of ``num`` random numbers of 10 digits each.
[5086]89
[5068]90        The number is returned as string.
91        """
[5118]92        curr = 1
93        while curr <= num:
[5112]94            pin = ''
[5068]95            for x in range(10):
[5112]96                pin += str(random().randint(0, 9))
[6417]97            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
[5127]98                curr += 1
99                yield pin
100            # PIN already in use
[5073]101
[5118]102    def _getStoragePath(self):
103        """Get the directory, where we store all batch-related CSV files.
104        """
105        site = grok.getSite()
106        storagepath = site['datacenter'].storage
107        ac_storage = os.path.join(storagepath, 'accesscodes')
108        if not os.path.exists(ac_storage):
109            os.mkdir(ac_storage)
110        return ac_storage
111
112    def entries(self):
113        """Get all entries of this batch as generator.
114        """
[6417]115        for x in self.values():
[5118]116            yield x
[6124]117
[5118]118    def getAccessCode(self, ac_id):
119        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
120        """
[6417]121        return self[ac_id]
[5118]122
[5121]123    def addAccessCode(self, num, pin):
124        """Add an access-code.
125        """
126        ac = AccessCode(num, pin)
127        ac.__parent__ = self
[6417]128        self[ac.representation] = ac
[5121]129        return
130
[5118]131    def invalidate(self, ac_id, student_id=None):
132        """Invalidate the AC with ID ``ac_id``.
133        """
134        self.invalidated_num += 1
135
[6359]136    def disable(self, ac_id, user_id=None):
[5149]137        """Disable the AC with ID ``ac_id``.
138
139        ``user_id`` is the user ID of the user triggering the
140        process. Already disabled ACs are left untouched.
141        """
142        num = self._acids[ac_id]
143        ac = self.getAccessCode(ac_id)
144        if ac._disabled == True:
145            return
146        self.disabled_num += 1
[6124]147
[5149]148    def enable(self, ac_id):
149        """(Re-)enable the AC with ID ``ac_id``.
150
151        This leaves the given AC in state ``unused``. Already enabled
152        ACs are left untouched.
153        """
154
[5110]155    def createCSVLogFile(self):
156        """Create a CSV file with data in batch.
157
158        Data will not contain invalidation date nor student ids.  File
159        will be created in ``accesscodes`` subdir of data center
160        storage path.
161
162        Returns name of created file.
163        """
164        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
[5118]165        ac_storage = self._getStoragePath()
[5110]166        csv_path = os.path.join(
167            ac_storage, '%s-%s-%s-%s.csv' % (
168                self.prefix, self.num, date, self.creator)
169            )
170        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
171        writer.writerow(['serial', 'ac', 'cost'])
172        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
[5118]173
[6417]174        for value in self.values():
[5110]175            writer.writerow(
176                [str(value.batch_serial), str(value.representation)]
177                )
[5118]178        site = grok.getSite()
[5112]179        logger = site.logger
180        logger.info(
181            "Created batch %s-%s" % (self.prefix, self.num))
182        logger.info(
183            "Written batch CSV to %s" % csv_path)
[5110]184        return os.path.basename(csv_path)
185
[5118]186    def archive(self):
187        """Create a CSV file for archive.
188        """
189        ac_storage = self._getStoragePath()
190        now = datetime.now()
191        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
192        csv_path = os.path.join(
193            ac_storage, '%s-%s_archive-%s-%s.csv' % (
194                self.prefix, self.num, timestamp, self.creator)
195            )
196        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
[6386]197        writer.writerow(['prefix', 'serial', 'ac', 'date'])
[5118]198        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
199                         str(self.entry_num)])
[6417]200        for value in self.values():
[5118]201            writer.writerow([
202                    self.prefix, value.batch_serial, value.representation,
203                    ])
204        return os.path.basename(csv_path)
205
[5149]206    def search(self, searchterm, searchtype):
207        if searchtype == 'serial':
208            if len(self._entries) < searchterm + 1:
209                return []
210            return [self._entries[searchterm]]
211        if searchtype == 'pin':
212            try:
213                entry = self.getAccessCode(searchterm)
214                return [entry]
215            except KeyError:
216                return []
217
[6417]218@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
[6418]219def handle_batch_added(batch, event):
[6417]220    # A (maybe dirty?) workaround to make batch containers work
221    # without self-maintained acids: as batches should contain their
222    # set of data immediately after creation, but we cannot add
223    # subobjects as long as the batch was not added already to the
224    # ZODB, we trigger the item creation for the time after the batch
225    # was added to the ZODB.
226    batch._createEntries()
227    return
228
229
[5079]230class AccessCodeBatchContainer(grok.Container):
231    grok.implements(IAccessCodeBatchContainer)
[5073]232
[5132]233    def _getStoragePath(self):
234        """Get the directory, where batch import files are stored.
235        """
236        site = grok.getSite()
237        storagepath = site['datacenter'].storage
238        ac_storage = os.path.join(storagepath, 'accesscodes')
239        import_path = os.path.join(ac_storage, 'imports')
240        if not os.path.exists(import_path):
241            os.mkdir(import_path)
242        return import_path
243
[5079]244    def addBatch(self, batch):
[5086]245        """Add a batch.
246        """
247        batch.num = self.getNum(batch.prefix)
248        key = "%s-%s" % (batch.prefix, batch.num)
[5079]249        self[key] = batch
[5086]250        self._p_changed = True
[5079]251
[6417]252    def createBatch(self, creation_date, creator, prefix, cost,
[5127]253                    entry_num):
254        """Create and add a batch.
255        """
[6417]256        batch_num = self.getNum(prefix)
257        batch = AccessCodeBatch(creation_date, creator, prefix,
[5127]258                                cost, entry_num, num=batch_num)
259        self.addBatch(batch)
260        return batch
[6124]261
[5086]262    def getNum(self, prefix):
263        """Get next unused num for given prefix.
264        """
265        num = 1
[5116]266        while self.get('%s-%s' % (prefix, num), None) is not None:
[5086]267            num += 1
268        return num
[5095]269
[5132]270    def getImportFiles(self):
271        """Return a generator with basenames of available import files.
272        """
273        path = self._getStoragePath()
274        for filename in sorted(os.listdir(path)):
275            yield filename
[6124]276
[5132]277    def reimport(self, filename, creator=u'UNKNOWN'):
278        """Reimport a batch given in CSV file.
279
280        CSV file must be of format as generated by createCSVLogFile
281        method.
282        """
283        path = os.path.join(self._getStoragePath(), filename)
284        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
285        entry = reader.next()
286        cost = float(entry['cost'])
287        num = int(entry['ac'])
288        batch_name = '%s-%s' % (entry['serial'], num)
289        if batch_name in self.keys():
290            raise KeyError('Batch already exists: %s' % batch_name)
[6417]291        #batch = AccessCodeBatch(
292        #    datetime.now(), creator, entry['serial'], cost, 0, num=num)
[5132]293        batch = AccessCodeBatch(
[6417]294            datetime.now(), creator, entry['serial'], cost, 0, num=0)
[5132]295        num_entries = 0
[6417]296        self[batch_name] = batch
[5132]297        for row in reader:
298            pin = row['ac']
299            serial = int(row['serial'])
300            rand_num = pin.rsplit('-', 1)[-1]
301            batch.addAccessCode(serial, rand_num)
302            num_entries += 1
303        batch.entry_num = num_entries
[6417]304
[5132]305        batch.createCSVLogFile()
306        return
[5149]307
308    def search(self, searchterm, searchtype, ):
309        """Look for access-codes that comply with the given params.
310        """
311        results = []
312        if searchtype == 'serial':
313            try:
314                searchterm = int(searchterm)
315            except:
316                return []
317        for batchname in self.keys():
318            part_result = self[batchname].search(searchterm, searchtype)
319            results.extend(part_result)
320        return results
321
[5153]322    def getAccessCode(self, ac_id):
323        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
324        """
325        for batchname in self.keys():
326            batch = self[batchname]
327            try:
328                return batch.getAccessCode(ac_id)
329            except KeyError:
330                continue
331        return None
[6124]332
[5153]333    def disable(self, ac_id, user_id):
334        """Disable the AC with ID ``ac_id``.
335
336        ``user_id`` is the user ID of the user triggering the
337        process. Already disabled ACs are left untouched.
338        """
339        ac = self.getAccessCode(ac_id)
340        if ac is None:
341            return
342        ac.__parent__.disable(ac_id, user_id)
343        return
344
345    def enable(self, ac_id):
346        """(Re-)enable the AC with ID ``ac_id``.
347
348        This leaves the given AC in state ``unused``. Already enabled
349        ACs are left untouched.
350        """
351        ac = self.getAccessCode(ac_id)
352        if ac is None:
353            return
354        ac.__parent__.enable(ac_id)
355        return
356
[6359]357    def invalidate(self, ac_id):
358        """Invalidate the AC with ID ``ac_id``.
359        """
360        ac = self.getAccessCode(ac_id)
361        if ac is None:
362            return
363        ac.__parent__.invalidate(ac_id)
364        return
[6124]365
[6359]366
[5073]367class AccessCodePlugin(grok.GlobalUtility):
368    grok.name('accesscodes')
369    grok.implements(IWAeUPSIRPPluggable)
370
371    def setup(self, site, name, logger):
[5079]372        site['accesscodes'] = AccessCodeBatchContainer()
373        logger.info('Installed container for access code batches.')
374        return
[5073]375
376    def update(self, site, name, logger):
[5107]377        if not 'accesscodes' in site.keys():
378            logger.info('Updating site at %s. Installing access codes.' % (
379                    site,))
380            self.setup(site, name, logger)
381        else:
382            logger.info(
383                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
384                    site, ))
385        return
[5447]386
387def get_access_code(access_code):
388    """Get an access code instance.
389
390    An access code here is a string like ``PUDE-1-1234567890``.
[6124]391
[5447]392    Returns ``None`` if the given code cannot be found.
393
394    This is a convenicence function that is faster than looking up a
395    batch container for the approriate access code.
396    """
397    site = grok.getSite()
398    if not isinstance(access_code, basestring):
399        return None
400    try:
401        batch_id, ac_id = access_code.rsplit('-', 1)
402    except:
403        return None
[5467]404    batch = site['accesscodes'].get(batch_id, None)
405    if batch is None:
[5447]406        return None
407    try:
408        code = batch.getAccessCode(access_code)
409    except KeyError:
410        return None
411    return code
[6359]412
[6408]413def fire_transition(access_code, arg, toward=False):
414    """Fire workflow transition for access code.
415
416    The access code instance is looked up via `access_code` (a string
417    like ``APP-1-12345678``).
418
419    `arg` tells what kind of transition to trigger. This will be a
420    transition id like ``'use'`` or ``'init'``, or some transition
421    target like :var:`waeup.sirp.accesscodes.workflow.INITIALIZED`.
422
423    If `toward` is ``False`` (the default) you have to pass a
424    transition id as `arg`, otherwise you must give a transition
425    target.
426
427    :func:`fire_transition` might raise exceptions depending on the
428    reason why the requested transition cannot be performed.
429
430    The following exceptions can occur during processing:
431
432    :exc:`KeyError`:
433      signals not existent access code, batch or site.
434
435    :exc:`ValueError`:
436      signals illegal format of `access_code` string. The regular format is
437      ``APP-N-XXXXXXXX``.
438
439    :exc:`hurry.workflow.interfaces.InvalidTransitionError`:
440      the transition requested cannot be performed because the workflow
441      rules forbid it.
442
443    :exc:`Unauthorized`:
444      the current user is not allowed to perform the requested transition.
445
446    """
[6374]447    try:
[6408]448        batch_id, ac_id = access_code.rsplit('-', 1)
449    except ValueError:
450        raise ValueError(
451            'Invalid access code format: %s (use: APP-N-XXXXXXXX)' % (
452                access_code,))
453    try:
454        ac = grok.getSite()['accesscodes'][batch_id].getAccessCode(access_code)
455    except TypeError:
456        raise KeyError(
457            'No site available for looking up accesscodes')
458    info = IWorkflowInfo(ac)
459    if toward:
460        info.fireTransitionToward(arg)
461    else:
462        info.fireTransition(arg)
[6374]463    return True
464
[6359]465def invalidate_accesscode(access_code):
[6374]466    """Invalidate AccessCode denoted by string ``access_code``.
467
468    Fires an appropriate transition to perform the task.
469
[6408]470    See :func:`fire_transition` for possible exceptions and their
471    meanings.
[6374]472    """
[6413]473    return fire_transition(access_code, 'use')
[6359]474
475def disable_accesscode(access_code):
[6374]476    """Disable AccessCode denoted by string ``access_code``.
477
478    Fires an appropriate transition to perform the task.
479
[6408]480    See :func:`fire_transition` for possible exceptions and their
481    meanings.
[6374]482    """
[6408]483    return fire_transition(access_code, DISABLED, toward=True)
[6359]484
485def reenable_accesscode(access_code):
[6374]486    """Reenable AccessCode denoted by string ``access_code``.
487
488    Fires an appropriate transition to perform the task.
489
[6408]490    See :func:`fire_transition` for possible exceptions and their
491    meanings.
[6374]492    """
[6408]493    return fire_transition(access_code, 'reenable')
Note: See TracBrowser for help on using the repository browser.