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

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

Remove the unneeded methods really.

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