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

Last change on this file since 6388 was 6387, checked in by uli, 13 years ago

* empty log message *

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