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

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

Support comments when doing accesscode transitions. These comments will appear in the respective access codes history.

File size: 16.1 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
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
63class AccessCodeBatch(grok.Container):
64    """A batch of access codes.
65    """
66    grok.implements(IAccessCodeBatch)
67
68    def __init__(self, creation_date, creator, batch_prefix, cost,
69                 entry_num, num=None):
70        super(AccessCodeBatch, self).__init__()
71        self.creation_date = creation_date
72        self.creator = creator
73        self.prefix = batch_prefix.upper()
74        self.cost = cost
75        self.entry_num = entry_num
76        self.num = num
77        self.invalidated_num = 0
78        self.disabled_num = 0
79
80    def _createEntries(self):
81        """Create the entries for this batch.
82        """
83        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
84            self.addAccessCode(num, pin)
85        return
86
87    def _getNewRandomNum(self, num=1):
88        """Create a set of ``num`` random numbers of 10 digits each.
89
90        The number is returned as string.
91        """
92        curr = 1
93        while curr <= num:
94            pin = ''
95            for x in range(10):
96                pin += str(random().randint(0, 9))
97            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self.keys():
98                curr += 1
99                yield pin
100            # PIN already in use
101
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        """
115        for x in self.values():
116            yield x
117
118    def getAccessCode(self, ac_id):
119        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
120        """
121        return self[ac_id]
122
123    def addAccessCode(self, num, pin):
124        """Add an access-code.
125        """
126        ac = AccessCode(num, pin)
127        ac.__parent__ = self
128        self[ac.representation] = ac
129        return
130
131    def invalidate(self, ac_id, student_id=None):
132        """Invalidate the AC with ID ``ac_id``.
133        """
134        self.invalidated_num += 1
135
136    def disable(self, ac_id, user_id=None):
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
147
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
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')
165        ac_storage = self._getStoragePath()
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])
173
174        for value in self.values():
175            writer.writerow(
176                [str(value.batch_serial), str(value.representation)]
177                )
178        site = grok.getSite()
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)
184        return os.path.basename(csv_path)
185
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)
197        writer.writerow(['prefix', 'serial', 'ac', 'date'])
198        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
199                         str(self.entry_num)])
200        for value in self.values():
201            writer.writerow([
202                    self.prefix, value.batch_serial, value.representation,
203                    ])
204        return os.path.basename(csv_path)
205
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
218@grok.subscribe(IAccessCodeBatch, grok.IObjectAddedEvent)
219def handle_batch_added(batch, event):
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
230class AccessCodeBatchContainer(grok.Container):
231    grok.implements(IAccessCodeBatchContainer)
232
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
244    def addBatch(self, batch):
245        """Add a batch.
246        """
247        batch.num = self.getNum(batch.prefix)
248        key = "%s-%s" % (batch.prefix, batch.num)
249        self[key] = batch
250        self._p_changed = True
251
252    def createBatch(self, creation_date, creator, prefix, cost,
253                    entry_num):
254        """Create and add a batch.
255        """
256        batch_num = self.getNum(prefix)
257        batch = AccessCodeBatch(creation_date, creator, prefix,
258                                cost, entry_num, num=batch_num)
259        self.addBatch(batch)
260        return batch
261
262    def getNum(self, prefix):
263        """Get next unused num for given prefix.
264        """
265        num = 1
266        while self.get('%s-%s' % (prefix, num), None) is not None:
267            num += 1
268        return num
269
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
276
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)
291        #batch = AccessCodeBatch(
292        #    datetime.now(), creator, entry['serial'], cost, 0, num=num)
293        batch = AccessCodeBatch(
294            datetime.now(), creator, entry['serial'], cost, 0, num=0)
295        num_entries = 0
296        self[batch_name] = batch
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
304
305        batch.createCSVLogFile()
306        return
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
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
332
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
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
365
366
367class AccessCodePlugin(grok.GlobalUtility):
368    grok.name('accesscodes')
369    grok.implements(IWAeUPSIRPPluggable)
370
371    def setup(self, site, name, logger):
372        site['accesscodes'] = AccessCodeBatchContainer()
373        logger.info('Installed container for access code batches.')
374        return
375
376    def update(self, site, name, logger):
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
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``.
391
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
404    batch = site['accesscodes'].get(batch_id, None)
405    if batch is None:
406        return None
407    try:
408        code = batch.getAccessCode(access_code)
409    except KeyError:
410        return None
411    return code
412
413def fire_transition(access_code, arg, toward=False, comment=None):
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    If `comment` is specified (default is ``None``) the given string
428    will be passed along as transition comment. It will appear in the
429    history of the changed access code. You can use this to place
430    remarks like for which object the access code was used or similar.
431
432    :func:`fire_transition` might raise exceptions depending on the
433    reason why the requested transition cannot be performed.
434
435    The following exceptions can occur during processing:
436
437    :exc:`KeyError`:
438      signals not existent access code, batch or site.
439
440    :exc:`ValueError`:
441      signals illegal format of `access_code` string. The regular format is
442      ``APP-N-XXXXXXXX``.
443
444    :exc:`hurry.workflow.interfaces.InvalidTransitionError`:
445      the transition requested cannot be performed because the workflow
446      rules forbid it.
447
448    :exc:`Unauthorized`:
449      the current user is not allowed to perform the requested transition.
450
451    """
452    try:
453        batch_id, ac_id = access_code.rsplit('-', 1)
454    except ValueError:
455        raise ValueError(
456            'Invalid access code format: %s (use: APP-N-XXXXXXXX)' % (
457                access_code,))
458    try:
459        ac = grok.getSite()['accesscodes'][batch_id].getAccessCode(access_code)
460    except TypeError:
461        raise KeyError(
462            'No site available for looking up accesscodes')
463    info = IWorkflowInfo(ac)
464    if toward:
465        info.fireTransitionToward(arg, comment=comment)
466    else:
467        info.fireTransition(arg, comment=comment)
468    return True
469
470def invalidate_accesscode(access_code, comment=None):
471    """Invalidate 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(access_code, 'use', comment=comment)
482
483def disable_accesscode(access_code, comment=None):
484    """Disable AccessCode denoted by string ``access_code``.
485
486    Fires an appropriate transition to perform the task.
487
488    `comment` is a string that will appear in the access code
489    history.
490
491    See :func:`fire_transition` for possible exceptions and their
492    meanings.
493    """
494    return fire_transition(
495        access_code, DISABLED, toward=True, comment=comment)
496
497def reenable_accesscode(access_code, comment=None):
498    """Reenable AccessCode denoted by string ``access_code``.
499
500    Fires an appropriate transition to perform the task.
501
502    `comment` is a string that will appear in the access code
503    history.
504
505    See :func:`fire_transition` for possible exceptions and their
506    meanings.
507    """
508    return fire_transition(access_code, 'reenable', comment=comment)
Note: See TracBrowser for help on using the repository browser.