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

Last change on this file since 5235 was 5153, checked in by uli, 15 years ago

Provide wrappers for getAccessCode(), enable(), and disable() also on batch
container level.

File size: 13.2 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
Note: See TracBrowser for help on using the repository browser.