source: main/waeup.sirp/branches/accesscodes-with-workflow/src/waeup/sirp/accesscodes/accesscodes.py @ 6411

Last change on this file since 6411 was 6406, checked in by Henrik Bettermann, 14 years ago

Add access code attributes 'state' and 'history'.

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