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
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):
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    """
447    try:
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)
463    return True
464
465def invalidate_accesscode(access_code):
466    """Invalidate AccessCode denoted by string ``access_code``.
467
468    Fires an appropriate transition to perform the task.
469
470    See :func:`fire_transition` for possible exceptions and their
471    meanings.
472    """
473    return fire_transition(access_code, 'use')
474
475def disable_accesscode(access_code):
476    """Disable AccessCode denoted by string ``access_code``.
477
478    Fires an appropriate transition to perform the task.
479
480    See :func:`fire_transition` for possible exceptions and their
481    meanings.
482    """
483    return fire_transition(access_code, DISABLED, toward=True)
484
485def reenable_accesscode(access_code):
486    """Reenable AccessCode denoted by string ``access_code``.
487
488    Fires an appropriate transition to perform the task.
489
490    See :func:`fire_transition` for possible exceptions and their
491    meanings.
492    """
493    return fire_transition(access_code, 'reenable')
Note: See TracBrowser for help on using the repository browser.