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

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

PIN stuff.

File size: 14.7 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
[6359]8from hurry.workflow.interfaces import IWorkflowInfo
[5068]9from random import SystemRandom as random
[5073]10from waeup.sirp.interfaces import IWAeUPSIRPPluggable
[5079]11from waeup.sirp.accesscodes.interfaces import (
12    IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer
13    )
[6359]14from waeup.sirp.accesscodes.workflow import DISABLED
[5068]15
[5102]16class ManageACBatches(grok.Permission):
17    grok.name('waeup.manageACBatches')
18
[6359]19class AccessCode(grok.Model):
[5068]20    grok.implements(IAccessCode)
21
[5118]22    def __init__(self, batch_serial, random_num, invalidation_date=None,
[5149]23                 student_id=None, disabled=False):
[5068]24        self.batch_serial = batch_serial
25        self.random_num = random_num
[5118]26        self._invalidation_date = invalidation_date
[5068]27        self.student_id = student_id
[5149]28        self._disabled = disabled
[6359]29        IWorkflowInfo(self).fireTransition('init')
[5068]30
31    @property
32    def representation(self):
33        return '%s-%s-%s' % (
34            self.batch_prefix, self.batch_num, self.random_num)
35
[5079]36    @property
37    def batch(self):
38        return getattr(self, '__parent__', None)
[5086]39
[5079]40    @property
41    def batch_prefix(self):
42        if self.batch is None:
43            return ''
44        return self.batch.prefix
[5086]45
[5079]46    @property
47    def batch_num(self):
48        if self.batch is None:
49            return ''
50        return self.batch.num
[5068]51
[5118]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
[5149]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
[5118]70class AccessCodeBatch(grok.Model):
[5068]71    """A batch of access codes.
72    """
73    grok.implements(IAccessCodeBatch)
74
[5086]75    def __init__(self, creation_date, creator, batch_prefix, cost,
76                 entry_num, num=None):
[5079]77        super(AccessCodeBatch, self).__init__()
[5068]78        self.creation_date = creation_date
79        self.creator = creator
[5116]80        self.prefix = batch_prefix.upper()
[5068]81        self.cost = cost
82        self.entry_num = entry_num
[5086]83        self.num = num
[5118]84        self.invalidated_num = 0
[5149]85        self.disabled_num = 0
[5118]86        self._entries = list()
87        self._acids = OIBTree()
[5149]88        self._studids = OIBTree()
[5118]89        self._createEntries()
[5079]90
[5118]91    def _createEntries(self):
[5079]92        """Create the entries for this batch.
93        """
[5118]94        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
[5121]95            self.addAccessCode(num, pin)
96        self._p_changed = True # XXX: most probably not needed.
[5112]97        return
[5118]98
99    def _getNewRandomNum(self, num=1):
[5112]100        """Create a set of ``num`` random numbers of 10 digits each.
[5086]101
[5068]102        The number is returned as string.
103        """
[5118]104        curr = 1
105        while curr <= num:
[5112]106            pin = ''
[5068]107            for x in range(10):
[5112]108                pin += str(random().randint(0, 9))
[5127]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
[5073]113
[5118]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
[6124]129
[5118]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
[5149]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
[5121]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
[5118]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
[5149]156        if student_id is not None:
157            self._studids.update({student_id: num})
[5118]158        self.invalidated_num += 1
159
[6359]160    def disable(self, ac_id, user_id=None):
[5149]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
[6124]178
[5149]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
[5110]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')
[5118]204        ac_storage = self._getStoragePath()
[5110]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])
[5118]212
213        for value in self._entries:
[5110]214            writer.writerow(
215                [str(value.batch_serial), str(value.representation)]
216                )
[5118]217        site = grok.getSite()
[5112]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)
[5110]223        return os.path.basename(csv_path)
224
[5118]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,
[5123]246                    value.student_id, date
[5118]247                    ])
248        return os.path.basename(csv_path)
249
[5149]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
[5079]270class AccessCodeBatchContainer(grok.Container):
271    grok.implements(IAccessCodeBatchContainer)
[5073]272
[5132]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
[5079]284    def addBatch(self, batch):
[5086]285        """Add a batch.
286        """
287        batch.num = self.getNum(batch.prefix)
288        key = "%s-%s" % (batch.prefix, batch.num)
[5079]289        self[key] = batch
[5086]290        self._p_changed = True
[5079]291
[5127]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
[6124]301
[5086]302    def getNum(self, prefix):
303        """Get next unused num for given prefix.
304        """
305        num = 1
[5116]306        while self.get('%s-%s' % (prefix, num), None) is not None:
[5086]307            num += 1
308        return num
[5095]309
[5132]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
[6124]316
[5132]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
[5149]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
[5153]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
[6124]369
[5153]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
[6359]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
[6124]402
[6359]403
[5073]404class AccessCodePlugin(grok.GlobalUtility):
405    grok.name('accesscodes')
406    grok.implements(IWAeUPSIRPPluggable)
407
408    def setup(self, site, name, logger):
[5079]409        site['accesscodes'] = AccessCodeBatchContainer()
410        logger.info('Installed container for access code batches.')
411        return
[5073]412
413    def update(self, site, name, logger):
[5107]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
[5447]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``.
[6124]428
[5447]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
[5467]441    batch = site['accesscodes'].get(batch_id, None)
442    if batch is None:
[5447]443        return None
444    try:
445        code = batch.getAccessCode(access_code)
446    except KeyError:
447        return None
448    return code
[6359]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.