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

Last change on this file since 6416 was 6413, checked in by uli, 14 years ago

Start to make the whole accesscode story work again.

File size: 15.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.Model):
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        self._entries = list()
80        self._acids = OIBTree()
81        self._createEntries()
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._acids:
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 entries(self):
116        """Get all entries of this batch as generator.
117        """
118        for x in self._entries:
119            yield x
120
121    def getAccessCode(self, ac_id):
122        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
123        """
124        return self._entries[self._acids[ac_id]]
125
126    def addAccessCode(self, num, pin):
127        """Add an access-code.
128        """
129        ac = AccessCode(num, pin)
130        ac.__parent__ = self
131        self._entries.append(ac)
132        self._acids.update({ac.representation: num})
133        return
134
135    def invalidate(self, ac_id, student_id=None):
136        """Invalidate the AC with ID ``ac_id``.
137        """
138        num = self._acids[ac_id]
139        self.invalidated_num += 1
140
141    def disable(self, ac_id, user_id=None):
142        """Disable the AC with ID ``ac_id``.
143
144        ``user_id`` is the user ID of the user triggering the
145        process. Already disabled ACs are left untouched.
146        """
147        num = self._acids[ac_id]
148        ac = self.getAccessCode(ac_id)
149        if ac._disabled == True:
150            return
151        self.disabled_num += 1
152
153    def enable(self, ac_id):
154        """(Re-)enable the AC with ID ``ac_id``.
155
156        This leaves the given AC in state ``unused``. Already enabled
157        ACs are left untouched.
158        """
159        num = self._acids[ac_id]
160        self.disabled_num -= 1
161
162    def createCSVLogFile(self):
163        """Create a CSV file with data in batch.
164
165        Data will not contain invalidation date nor student ids.  File
166        will be created in ``accesscodes`` subdir of data center
167        storage path.
168
169        Returns name of created file.
170        """
171        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
172        ac_storage = self._getStoragePath()
173        csv_path = os.path.join(
174            ac_storage, '%s-%s-%s-%s.csv' % (
175                self.prefix, self.num, date, self.creator)
176            )
177        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
178        writer.writerow(['serial', 'ac', 'cost'])
179        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
180
181        for value in self._entries:
182            writer.writerow(
183                [str(value.batch_serial), str(value.representation)]
184                )
185        site = grok.getSite()
186        logger = site.logger
187        logger.info(
188            "Created batch %s-%s" % (self.prefix, self.num))
189        logger.info(
190            "Written batch CSV to %s" % csv_path)
191        return os.path.basename(csv_path)
192
193    def archive(self):
194        """Create a CSV file for archive.
195        """
196        ac_storage = self._getStoragePath()
197        now = datetime.now()
198        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
199        csv_path = os.path.join(
200            ac_storage, '%s-%s_archive-%s-%s.csv' % (
201                self.prefix, self.num, timestamp, self.creator)
202            )
203        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
204        writer.writerow(['prefix', 'serial', 'ac', 'date'])
205        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
206                         str(self.entry_num)])
207        for value in self._entries:
208            writer.writerow([
209                    self.prefix, value.batch_serial, value.representation,
210                    ])
211        return os.path.basename(csv_path)
212
213    def search(self, searchterm, searchtype):
214        if searchtype == 'serial':
215            if len(self._entries) < searchterm + 1:
216                return []
217            return [self._entries[searchterm]]
218        if searchtype == 'pin':
219            try:
220                entry = self.getAccessCode(searchterm)
221                return [entry]
222            except KeyError:
223                return []
224
225class AccessCodeBatchContainer(grok.Container):
226    grok.implements(IAccessCodeBatchContainer)
227
228    def _getStoragePath(self):
229        """Get the directory, where batch import files are stored.
230        """
231        site = grok.getSite()
232        storagepath = site['datacenter'].storage
233        ac_storage = os.path.join(storagepath, 'accesscodes')
234        import_path = os.path.join(ac_storage, 'imports')
235        if not os.path.exists(import_path):
236            os.mkdir(import_path)
237        return import_path
238
239    def addBatch(self, batch):
240        """Add a batch.
241        """
242        batch.num = self.getNum(batch.prefix)
243        key = "%s-%s" % (batch.prefix, batch.num)
244        self[key] = batch
245        self._p_changed = True
246
247    def createBatch(self, creation_date, creator, batch_prefix, cost,
248                    entry_num):
249        """Create and add a batch.
250        """
251        batch_num = self.getNum(batch_prefix)
252        batch = AccessCodeBatch(creation_date, creator, batch_prefix,
253                                cost, entry_num, num=batch_num)
254        self.addBatch(batch)
255        return batch
256
257    def getNum(self, prefix):
258        """Get next unused num for given prefix.
259        """
260        num = 1
261        while self.get('%s-%s' % (prefix, num), None) is not None:
262            num += 1
263        return num
264
265    def getImportFiles(self):
266        """Return a generator with basenames of available import files.
267        """
268        path = self._getStoragePath()
269        for filename in sorted(os.listdir(path)):
270            yield filename
271
272    def reimport(self, filename, creator=u'UNKNOWN'):
273        """Reimport a batch given in CSV file.
274
275        CSV file must be of format as generated by createCSVLogFile
276        method.
277        """
278        path = os.path.join(self._getStoragePath(), filename)
279        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
280        entry = reader.next()
281        cost = float(entry['cost'])
282        num = int(entry['ac'])
283        batch_name = '%s-%s' % (entry['serial'], num)
284        if batch_name in self.keys():
285            raise KeyError('Batch already exists: %s' % batch_name)
286        batch = AccessCodeBatch(
287            datetime.now(), creator, entry['serial'], cost, 0, num=num)
288        num_entries = 0
289        for row in reader:
290            pin = row['ac']
291            serial = int(row['serial'])
292            rand_num = pin.rsplit('-', 1)[-1]
293            batch.addAccessCode(serial, rand_num)
294            num_entries += 1
295        batch.entry_num = num_entries
296        self[batch_name] = batch
297        batch.createCSVLogFile()
298        return
299
300    def search(self, searchterm, searchtype, ):
301        """Look for access-codes that comply with the given params.
302        """
303        results = []
304        if searchtype == 'serial':
305            try:
306                searchterm = int(searchterm)
307            except:
308                return []
309        for batchname in self.keys():
310            part_result = self[batchname].search(searchterm, searchtype)
311            results.extend(part_result)
312        return results
313
314    def getAccessCode(self, ac_id):
315        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
316        """
317        for batchname in self.keys():
318            batch = self[batchname]
319            try:
320                return batch.getAccessCode(ac_id)
321            except KeyError:
322                continue
323        return None
324
325    def disable(self, ac_id, user_id):
326        """Disable the AC with ID ``ac_id``.
327
328        ``user_id`` is the user ID of the user triggering the
329        process. Already disabled ACs are left untouched.
330        """
331        ac = self.getAccessCode(ac_id)
332        if ac is None:
333            return
334        ac.__parent__.disable(ac_id, user_id)
335        return
336
337    def enable(self, ac_id):
338        """(Re-)enable the AC with ID ``ac_id``.
339
340        This leaves the given AC in state ``unused``. Already enabled
341        ACs are left untouched.
342        """
343        ac = self.getAccessCode(ac_id)
344        if ac is None:
345            return
346        ac.__parent__.enable(ac_id)
347        return
348
349    def invalidate(self, ac_id):
350        """Invalidate the AC with ID ``ac_id``.
351        """
352        ac = self.getAccessCode(ac_id)
353        if ac is None:
354            return
355        ac.__parent__.invalidate(ac_id)
356        return
357
358
359class AccessCodePlugin(grok.GlobalUtility):
360    grok.name('accesscodes')
361    grok.implements(IWAeUPSIRPPluggable)
362
363    def setup(self, site, name, logger):
364        site['accesscodes'] = AccessCodeBatchContainer()
365        logger.info('Installed container for access code batches.')
366        return
367
368    def update(self, site, name, logger):
369        if not 'accesscodes' in site.keys():
370            logger.info('Updating site at %s. Installing access codes.' % (
371                    site,))
372            self.setup(site, name, logger)
373        else:
374            logger.info(
375                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
376                    site, ))
377        return
378
379def get_access_code(access_code):
380    """Get an access code instance.
381
382    An access code here is a string like ``PUDE-1-1234567890``.
383
384    Returns ``None`` if the given code cannot be found.
385
386    This is a convenicence function that is faster than looking up a
387    batch container for the approriate access code.
388    """
389    site = grok.getSite()
390    if not isinstance(access_code, basestring):
391        return None
392    try:
393        batch_id, ac_id = access_code.rsplit('-', 1)
394    except:
395        return None
396    batch = site['accesscodes'].get(batch_id, None)
397    if batch is None:
398        return None
399    try:
400        code = batch.getAccessCode(access_code)
401    except KeyError:
402        return None
403    return code
404
405def fire_transition(access_code, arg, toward=False):
406    """Fire workflow transition for access code.
407
408    The access code instance is looked up via `access_code` (a string
409    like ``APP-1-12345678``).
410
411    `arg` tells what kind of transition to trigger. This will be a
412    transition id like ``'use'`` or ``'init'``, or some transition
413    target like :var:`waeup.sirp.accesscodes.workflow.INITIALIZED`.
414
415    If `toward` is ``False`` (the default) you have to pass a
416    transition id as `arg`, otherwise you must give a transition
417    target.
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)
453    else:
454        info.fireTransition(arg)
455    return True
456
457def invalidate_accesscode(access_code):
458    """Invalidate AccessCode denoted by string ``access_code``.
459
460    Fires an appropriate transition to perform the task.
461
462    See :func:`fire_transition` for possible exceptions and their
463    meanings.
464    """
465    return fire_transition(access_code, 'use')
466
467def disable_accesscode(access_code):
468    """Disable AccessCode denoted by string ``access_code``.
469
470    Fires an appropriate transition to perform the task.
471
472    See :func:`fire_transition` for possible exceptions and their
473    meanings.
474    """
475    return fire_transition(access_code, DISABLED, toward=True)
476
477def reenable_accesscode(access_code):
478    """Reenable AccessCode denoted by string ``access_code``.
479
480    Fires an appropriate transition to perform the task.
481
482    See :func:`fire_transition` for possible exceptions and their
483    meanings.
484    """
485    return fire_transition(access_code, 'reenable')
Note: See TracBrowser for help on using the repository browser.