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

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

Remove the invalidated_num counter (replaced by used_num).

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