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

Last change on this file since 6436 was 6432, checked in by uli, 13 years ago

Clean up imports.

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