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

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

PIN stuff.

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