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

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

Shorten code lookup.

File size: 13.9 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
[5068]8from random import SystemRandom as random
[5073]9from waeup.sirp.interfaces import IWAeUPSIRPPluggable
[5079]10from waeup.sirp.accesscodes.interfaces import (
11    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
12    )
[5068]13
[5102]14class ManageACBatches(grok.Permission):
15    grok.name('waeup.manageACBatches')
16
[5118]17class AccessCode(grok.Context):
[5068]18    grok.implements(IAccessCode)
19
[5118]20    def __init__(self, batch_serial, random_num, invalidation_date=None,
[5149]21                 student_id=None, disabled=False):
[5068]22        self.batch_serial = batch_serial
23        self.random_num = random_num
[5118]24        self._invalidation_date = invalidation_date
[5068]25        self.student_id = student_id
[5149]26        self._disabled = disabled
[5068]27
28    @property
29    def representation(self):
30        return '%s-%s-%s' % (
31            self.batch_prefix, self.batch_num, self.random_num)
32
[5079]33    @property
34    def batch(self):
35        return getattr(self, '__parent__', None)
[5086]36
[5079]37    @property
38    def batch_prefix(self):
39        if self.batch is None:
40            return ''
41        return self.batch.prefix
[5086]42
[5079]43    @property
44    def batch_num(self):
45        if self.batch is None:
46            return ''
47        return self.batch.num
[5068]48
[5118]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
[5149]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
[5118]67class AccessCodeBatch(grok.Model):
[5068]68    """A batch of access codes.
69    """
70    grok.implements(IAccessCodeBatch)
71
[5086]72    def __init__(self, creation_date, creator, batch_prefix, cost,
73                 entry_num, num=None):
[5079]74        super(AccessCodeBatch, self).__init__()
[5068]75        self.creation_date = creation_date
76        self.creator = creator
[5116]77        self.prefix = batch_prefix.upper()
[5068]78        self.cost = cost
79        self.entry_num = entry_num
[5086]80        self.num = num
[5118]81        self.invalidated_num = 0
[5149]82        self.disabled_num = 0
[5118]83        self._entries = list()
84        self._acids = OIBTree()
[5149]85        self._studids = OIBTree()
[5118]86        self._createEntries()
[5079]87
[5118]88    def _createEntries(self):
[5079]89        """Create the entries for this batch.
90        """
[5118]91        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
[5121]92            self.addAccessCode(num, pin)
93        self._p_changed = True # XXX: most probably not needed.
[5112]94        return
[5118]95
96    def _getNewRandomNum(self, num=1):
[5112]97        """Create a set of ``num`` random numbers of 10 digits each.
[5086]98
[5068]99        The number is returned as string.
100        """
[5118]101        curr = 1
102        while curr <= num:
[5112]103            pin = ''
[5068]104            for x in range(10):
[5112]105                pin += str(random().randint(0, 9))
[5127]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
[5073]110
[5118]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
[5149]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
[5121]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
[5118]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
[5149]153        if student_id is not None:
154            self._studids.update({student_id: num})
[5118]155        self.invalidated_num += 1
156
[5149]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
[5110]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')
[5118]201        ac_storage = self._getStoragePath()
[5110]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])
[5118]209
210        for value in self._entries:
[5110]211            writer.writerow(
212                [str(value.batch_serial), str(value.representation)]
213                )
[5118]214        site = grok.getSite()
[5112]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)
[5110]220        return os.path.basename(csv_path)
221
[5118]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,
[5123]243                    value.student_id, date
[5118]244                    ])
245        return os.path.basename(csv_path)
246
[5149]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
[5079]267class AccessCodeBatchContainer(grok.Container):
268    grok.implements(IAccessCodeBatchContainer)
[5073]269
[5132]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
[5079]281    def addBatch(self, batch):
[5086]282        """Add a batch.
283        """
284        batch.num = self.getNum(batch.prefix)
285        key = "%s-%s" % (batch.prefix, batch.num)
[5079]286        self[key] = batch
[5086]287        self._p_changed = True
[5079]288
[5127]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       
[5086]299    def getNum(self, prefix):
300        """Get next unused num for given prefix.
301        """
302        num = 1
[5116]303        while self.get('%s-%s' % (prefix, num), None) is not None:
[5086]304            num += 1
305        return num
[5095]306
[5132]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
[5149]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
[5153]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   
[5073]392class AccessCodePlugin(grok.GlobalUtility):
393    grok.name('accesscodes')
394    grok.implements(IWAeUPSIRPPluggable)
395
396    def setup(self, site, name, logger):
[5079]397        site['accesscodes'] = AccessCodeBatchContainer()
398        logger.info('Installed container for access code batches.')
399        return
[5073]400
401    def update(self, site, name, logger):
[5107]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
[5447]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
[5467]429    batch = site['accesscodes'].get(batch_id, None)
430    if batch is None:
[5447]431        return None
[5467]432   
[5447]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.