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

Last change on this file since 5119 was 5118, checked in by uli, 15 years ago

Implement completely different design for access-codes and their batches. We drop using zope.catalogs here and care
for the limited set of operations we need on access codes and batches ourselves. This speeds up performance for batch
creation by factor 2 and uses only half the memory in ZODB than what was needed with old PIN implemenentation.

File size: 7.4 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):
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
27    @property
28    def representation(self):
29        return '%s-%s-%s' % (
30            self.batch_prefix, self.batch_num, self.random_num)
31
32    @property
33    def batch(self):
34        return getattr(self, '__parent__', None)
35
36    @property
37    def batch_prefix(self):
38        if self.batch is None:
39            return ''
40        return self.batch.prefix
41
42    @property
43    def batch_num(self):
44        if self.batch is None:
45            return ''
46        return self.batch.num
47
48    @property
49    def cost(self):
50        if self.batch is None:
51            return None
52        return self.batch.cost
53
54    @property
55    def invalidation_date(self):
56        # We define this as a property to make it unwritable.
57        # This attribute should be set by the surrounding batch only.
58        return self._invalidation_date
59   
60class AccessCodeBatch(grok.Model):
61    """A batch of access codes.
62    """
63    grok.implements(IAccessCodeBatch)
64
65    def __init__(self, creation_date, creator, batch_prefix, cost,
66                 entry_num, num=None):
67        super(AccessCodeBatch, self).__init__()
68        self.creation_date = creation_date
69        self.creator = creator
70        self.prefix = batch_prefix.upper()
71        self.cost = cost
72        self.entry_num = entry_num
73        self.num = num
74        self.invalidated_num = 0
75        self._entries = list()
76        self._acids = OIBTree()
77        self._createEntries()
78
79    def _createEntries(self):
80        """Create the entries for this batch.
81        """
82        for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)):
83            ac = AccessCode(num, pin)
84            ac.__parent__ = self
85            self._entries.append(ac)
86            self._acids.update({ac.representation: num})
87            self._p_changed = True # XXX: most probably not needed.
88        return
89
90    def _getNewRandomNum(self, num=1):
91        """Create a set of ``num`` random numbers of 10 digits each.
92
93        The number is returned as string.
94        """
95        curr = 1
96        while curr <= num:
97            pin = ''
98            for x in range(10):
99                pin += str(random().randint(0, 9))
100            if '%s-%s-%s' % (self.prefix, self.num, pin) in self._acids:
101                # PIN already in use
102                continue
103            curr += 1
104            yield pin
105
106    def _getStoragePath(self):
107        """Get the directory, where we store all batch-related CSV files.
108        """
109        site = grok.getSite()
110        storagepath = site['datacenter'].storage
111        ac_storage = os.path.join(storagepath, 'accesscodes')
112        if not os.path.exists(ac_storage):
113            os.mkdir(ac_storage)
114        return ac_storage
115
116    def entries(self):
117        """Get all entries of this batch as generator.
118        """
119        for x in self._entries:
120            yield x
121           
122    def getAccessCode(self, ac_id):
123        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
124        """
125        return self._entries[self._acids[ac_id]]
126
127    def invalidate(self, ac_id, student_id=None):
128        """Invalidate the AC with ID ``ac_id``.
129        """
130        num = self._acids[ac_id]
131        ac = self.getAccessCode(ac_id)
132        ac._invalidation_date = datetime.now()
133        ac.student_id = student_id
134        self.invalidated_num += 1
135
136    def createCSVLogFile(self):
137        """Create a CSV file with data in batch.
138
139        Data will not contain invalidation date nor student ids.  File
140        will be created in ``accesscodes`` subdir of data center
141        storage path.
142
143        Returns name of created file.
144        """
145        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
146        ac_storage = self._getStoragePath()
147        csv_path = os.path.join(
148            ac_storage, '%s-%s-%s-%s.csv' % (
149                self.prefix, self.num, date, self.creator)
150            )
151        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
152        writer.writerow(['serial', 'ac', 'cost'])
153        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
154
155        for value in self._entries:
156            writer.writerow(
157                [str(value.batch_serial), str(value.representation)]
158                )
159        site = grok.getSite()
160        logger = site.logger
161        logger.info(
162            "Created batch %s-%s" % (self.prefix, self.num))
163        logger.info(
164            "Written batch CSV to %s" % csv_path)
165        return os.path.basename(csv_path)
166
167    def archive(self):
168        """Create a CSV file for archive.
169        """
170        ac_storage = self._getStoragePath()
171        now = datetime.now()
172        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
173        csv_path = os.path.join(
174            ac_storage, '%s-%s_archive-%s-%s.csv' % (
175                self.prefix, self.num, timestamp, self.creator)
176            )
177        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
178        writer.writerow(['prefix', 'serial', 'ac', 'student', 'date'])
179        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
180                         str(self.entry_num)])
181        for value in self._entries:
182            date = ''
183            stud_id = ''
184            if value.invalidation_date is not None:
185                date = value.invalidation_date.strftime(
186                    '%Y-%m-%d-%H-%M-%S')
187            if stud_id is not None:
188                stud_id = value.student_id
189            writer.writerow([
190                    self.prefix, value.batch_serial, value.representation,
191                    stud_id, date
192                    ])
193        return os.path.basename(csv_path)
194
195class AccessCodeBatchContainer(grok.Container):
196    grok.implements(IAccessCodeBatchContainer)
197
198    def addBatch(self, batch):
199        """Add a batch.
200        """
201        batch.num = self.getNum(batch.prefix)
202        key = "%s-%s" % (batch.prefix, batch.num)
203        self[key] = batch
204        self._p_changed = True
205
206    def getNum(self, prefix):
207        """Get next unused num for given prefix.
208        """
209        num = 1
210        while self.get('%s-%s' % (prefix, num), None) is not None:
211            num += 1
212        return num
213
214class AccessCodePlugin(grok.GlobalUtility):
215    grok.name('accesscodes')
216    grok.implements(IWAeUPSIRPPluggable)
217
218    def setup(self, site, name, logger):
219        site['accesscodes'] = AccessCodeBatchContainer()
220        logger.info('Installed container for access code batches.')
221        return
222
223    def update(self, site, name, logger):
224        if not 'accesscodes' in site.keys():
225            logger.info('Updating site at %s. Installing access codes.' % (
226                    site,))
227            self.setup(site, name, logger)
228        else:
229            logger.info(
230                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
231                    site, ))
232        return
Note: See TracBrowser for help on using the repository browser.