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

Last change on this file since 6064 was 5467, checked in by uli, 14 years ago

Shorten code lookup.

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