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

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

Remove entries() method of AccessCodeBatch?. Use values() instead.

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