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
Line 
1"""Components to handle access codes.
2"""
3import csv
4import grok
5import os
6from BTrees.OIBTree import OIBTree
7from datetime import datetime
8from hurry.workflow.interfaces import (
9    IWorkflowInfo, InvalidTransitionError, IWorkflowState)
10from random import SystemRandom as random
11from waeup.sirp.interfaces import IWAeUPSIRPPluggable, IObjectHistory
12from waeup.sirp.accesscodes.interfaces import (
13    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
14    )
15from waeup.sirp.accesscodes.workflow import DISABLED, USED
16
17class ManageACBatches(grok.Permission):
18    grok.name('waeup.manageACBatches')
19
20class AccessCode(grok.Model):
21    grok.implements(IAccessCode)
22
23    def __init__(self, batch_serial, random_num):
24        self.batch_serial = batch_serial
25        self.random_num = random_num
26        IWorkflowInfo(self).fireTransition('init')
27
28    @property
29    def representation(self):
30        return '%s-%s-%s' % (
31            self.batch_prefix, self.batch_num, self.random_num)
32
33    @property
34    def batch(self):
35        return getattr(self, '__parent__', None)
36
37    @property
38    def batch_prefix(self):
39        if self.batch is None:
40            return ''
41        return self.batch.prefix
42
43    @property
44    def batch_num(self):
45        if self.batch is None:
46            return ''
47        return self.batch.num
48
49    @property
50    def cost(self):
51        if self.batch is None:
52            return None
53        return self.batch.cost
54
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
63    @property
64    def history(self):
65        history = IObjectHistory(self)
66        return '\n'.join(history.messages)
67
68class AccessCodeBatch(grok.Container):
69    """A batch of access codes.
70    """
71    grok.implements(IAccessCodeBatch)
72
73    def __init__(self, creation_date, creator, batch_prefix, cost,
74                 entry_num, num=None):
75        super(AccessCodeBatch, self).__init__()
76        self.creation_date = creation_date
77        self.creator = creator
78        self.prefix = batch_prefix.upper()
79        self.cost = cost
80        self.entry_num = entry_num
81        self.num = num
82        self.invalidated_num = 0
83        self.used_num = 0
84        self.disabled_num = 0
85
86    def _createEntries(self):
87        """Create the entries for this batch.
88        """
89        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
90            self.addAccessCode(num, pin)
91        return
92
93    def _getNewRandomNum(self, num=1):
94        """Create a set of ``num`` random numbers of 10 digits each.
95
96        The number is returned as string.
97        """
98        curr = 1
99        while curr <= num:
100            pin = ''
101            for x in range(10):
102                pin += str(random().randint(0, 9))
103            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
104                curr += 1
105                yield pin
106            # PIN already in use
107
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        """
121        for x in self.values():
122            yield x
123
124    def getAccessCode(self, ac_id):
125        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
126        """
127        return self[ac_id]
128
129    def addAccessCode(self, num, pin):
130        """Add an access-code.
131        """
132        ac = AccessCode(num, pin)
133        ac.__parent__ = self
134        self[ac.representation] = ac
135        return
136
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')
147        ac_storage = self._getStoragePath()
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])
155
156        for value in sorted(self.values(),
157                            cmp=lambda x, y: cmp(
158                x.batch_serial, y.batch_serial)):
159            writer.writerow(
160                [str(value.batch_serial), str(value.representation)]
161                )
162        site = grok.getSite()
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)
168        return os.path.basename(csv_path)
169
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)
181        writer.writerow(['prefix', 'serial', 'ac', 'date'])
182        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
183                         str(self.entry_num)])
184        for value in sorted(
185            self.values(),
186            cmp = lambda x, y: cmp(x.batch_serial, y.batch_serial)
187            ):
188            writer.writerow([
189                    self.prefix, value.batch_serial, value.representation,
190                    ])
191        return os.path.basename(csv_path)
192
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
205@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
206def handle_batch_added(batch, event):
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
217class AccessCodeBatchContainer(grok.Container):
218    grok.implements(IAccessCodeBatchContainer)
219
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
231    def addBatch(self, batch):
232        """Add a batch.
233        """
234        batch.num = self.getNum(batch.prefix)
235        key = "%s-%s" % (batch.prefix, batch.num)
236        self[key] = batch
237        self._p_changed = True
238
239    def createBatch(self, creation_date, creator, prefix, cost,
240                    entry_num):
241        """Create and add a batch.
242        """
243        batch_num = self.getNum(prefix)
244        batch = AccessCodeBatch(creation_date, creator, prefix,
245                                cost, entry_num, num=batch_num)
246        self.addBatch(batch)
247        return batch
248
249    def getNum(self, prefix):
250        """Get next unused num for given prefix.
251        """
252        num = 1
253        while self.get('%s-%s' % (prefix, num), None) is not None:
254            num += 1
255        return num
256
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
263
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)
278        #batch = AccessCodeBatch(
279        #    datetime.now(), creator, entry['serial'], cost, 0, num=num)
280        batch = AccessCodeBatch(
281            datetime.now(), creator, entry['serial'], cost, 0, num=0)
282        num_entries = 0
283        self[batch_name] = batch
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
291
292        batch.createCSVLogFile()
293        return
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
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
319
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
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
352
353
354class AccessCodePlugin(grok.GlobalUtility):
355    grok.name('accesscodes')
356    grok.implements(IWAeUPSIRPPluggable)
357
358    def setup(self, site, name, logger):
359        site['accesscodes'] = AccessCodeBatchContainer()
360        logger.info('Installed container for access code batches.')
361        return
362
363    def update(self, site, name, logger):
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
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``.
378
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
391    batch = site['accesscodes'].get(batch_id, None)
392    if batch is None:
393        return None
394    try:
395        code = batch.getAccessCode(access_code)
396    except KeyError:
397        return None
398    return code
399
400def fire_transition(access_code, arg, toward=False, comment=None):
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
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
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    """
439    try:
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:
452        info.fireTransitionToward(arg, comment=comment)
453    else:
454        info.fireTransition(arg, comment=comment)
455    return True
456
457def invalidate_accesscode(access_code, comment=None):
458    """Invalidate AccessCode denoted by string ``access_code``.
459
460    Fires an appropriate transition to perform the task.
461
462    `comment` is a string that will appear in the access code
463    history.
464
465    See :func:`fire_transition` for possible exceptions and their
466    meanings.
467    """
468    return fire_transition(access_code, 'use', comment=comment)
469
470def disable_accesscode(access_code, comment=None):
471    """Disable AccessCode denoted by string ``access_code``.
472
473    Fires an appropriate transition to perform the task.
474
475    `comment` is a string that will appear in the access code
476    history.
477
478    See :func:`fire_transition` for possible exceptions and their
479    meanings.
480    """
481    return fire_transition(
482        access_code, DISABLED, toward=True, comment=comment)
483
484def reenable_accesscode(access_code, comment=None):
485    """Reenable AccessCode denoted by string ``access_code``.
486
487    Fires an appropriate transition to perform the task.
488
489    `comment` is a string that will appear in the access code
490    history.
491
492    See :func:`fire_transition` for possible exceptions and their
493    meanings.
494    """
495    return fire_transition(access_code, 'reenable', comment=comment)
Note: See TracBrowser for help on using the repository browser.