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

Last change on this file since 5141 was 5132, checked in by uli, 14 years ago

Support reimport of AC batches.

File size: 9.5 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            self.addAccessCode(num, pin)
84        self._p_changed = True # XXX: most probably not needed.
85        return
86
87    def _getNewRandomNum(self, num=1):
88        """Create a set of ``num`` random numbers of 10 digits each.
89
90        The number is returned as string.
91        """
92        curr = 1
93        while curr <= num:
94            pin = ''
95            for x in range(10):
96                pin += str(random().randint(0, 9))
97            if not '%s-%s-%s' % (self.prefix, self.num, pin) in self._acids:
98                curr += 1
99                yield pin
100            # PIN already in use
101
102    def _getStoragePath(self):
103        """Get the directory, where we store all batch-related CSV files.
104        """
105        site = grok.getSite()
106        storagepath = site['datacenter'].storage
107        ac_storage = os.path.join(storagepath, 'accesscodes')
108        if not os.path.exists(ac_storage):
109            os.mkdir(ac_storage)
110        return ac_storage
111
112    def entries(self):
113        """Get all entries of this batch as generator.
114        """
115        for x in self._entries:
116            yield x
117           
118    def getAccessCode(self, ac_id):
119        """Get the AccessCode with ID ``ac_id`` or ``KeyError``.
120        """
121        return self._entries[self._acids[ac_id]]
122
123    def addAccessCode(self, num, pin):
124        """Add an access-code.
125        """
126        ac = AccessCode(num, pin)
127        ac.__parent__ = self
128        self._entries.append(ac)
129        self._acids.update({ac.representation: num})
130        return
131
132    def invalidate(self, ac_id, student_id=None):
133        """Invalidate the AC with ID ``ac_id``.
134        """
135        num = self._acids[ac_id]
136        ac = self.getAccessCode(ac_id)
137        ac._invalidation_date = datetime.now()
138        ac.student_id = student_id
139        self.invalidated_num += 1
140
141    def createCSVLogFile(self):
142        """Create a CSV file with data in batch.
143
144        Data will not contain invalidation date nor student ids.  File
145        will be created in ``accesscodes`` subdir of data center
146        storage path.
147
148        Returns name of created file.
149        """
150        date = self.creation_date.strftime('%Y_%m_%d_%H_%M_%S')
151        ac_storage = self._getStoragePath()
152        csv_path = os.path.join(
153            ac_storage, '%s-%s-%s-%s.csv' % (
154                self.prefix, self.num, date, self.creator)
155            )
156        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
157        writer.writerow(['serial', 'ac', 'cost'])
158        writer.writerow([self.prefix, str(self.num), "%0.2f" % self.cost])
159
160        for value in self._entries:
161            writer.writerow(
162                [str(value.batch_serial), str(value.representation)]
163                )
164        site = grok.getSite()
165        logger = site.logger
166        logger.info(
167            "Created batch %s-%s" % (self.prefix, self.num))
168        logger.info(
169            "Written batch CSV to %s" % csv_path)
170        return os.path.basename(csv_path)
171
172    def archive(self):
173        """Create a CSV file for archive.
174        """
175        ac_storage = self._getStoragePath()
176        now = datetime.now()
177        timestamp = now.strftime('%Y_%m_%d_%H_%M_%S')
178        csv_path = os.path.join(
179            ac_storage, '%s-%s_archive-%s-%s.csv' % (
180                self.prefix, self.num, timestamp, self.creator)
181            )
182        writer = csv.writer(open(csv_path, 'w'), quoting=csv.QUOTE_ALL)
183        writer.writerow(['prefix', 'serial', 'ac', 'student', 'date'])
184        writer.writerow([self.prefix, '%0.2f' % self.cost, str(self.num),
185                         str(self.entry_num)])
186        for value in self._entries:
187            date = ''
188            if value.invalidation_date is not None:
189                date = value.invalidation_date.strftime(
190                    '%Y-%m-%d-%H-%M-%S')
191            writer.writerow([
192                    self.prefix, value.batch_serial, value.representation,
193                    value.student_id, date
194                    ])
195        return os.path.basename(csv_path)
196
197class AccessCodeBatchContainer(grok.Container):
198    grok.implements(IAccessCodeBatchContainer)
199
200    def _getStoragePath(self):
201        """Get the directory, where batch import files are stored.
202        """
203        site = grok.getSite()
204        storagepath = site['datacenter'].storage
205        ac_storage = os.path.join(storagepath, 'accesscodes')
206        import_path = os.path.join(ac_storage, 'imports')
207        if not os.path.exists(import_path):
208            os.mkdir(import_path)
209        return import_path
210
211    def addBatch(self, batch):
212        """Add a batch.
213        """
214        batch.num = self.getNum(batch.prefix)
215        key = "%s-%s" % (batch.prefix, batch.num)
216        self[key] = batch
217        self._p_changed = True
218
219    def createBatch(self, creation_date, creator, batch_prefix, cost,
220                    entry_num):
221        """Create and add a batch.
222        """
223        batch_num = self.getNum(batch_prefix)
224        batch = AccessCodeBatch(creation_date, creator, batch_prefix,
225                                cost, entry_num, num=batch_num)
226        self.addBatch(batch)
227        return batch
228       
229    def getNum(self, prefix):
230        """Get next unused num for given prefix.
231        """
232        num = 1
233        while self.get('%s-%s' % (prefix, num), None) is not None:
234            num += 1
235        return num
236
237    def getImportFiles(self):
238        """Return a generator with basenames of available import files.
239        """
240        path = self._getStoragePath()
241        for filename in sorted(os.listdir(path)):
242            yield filename
243   
244    def reimport(self, filename, creator=u'UNKNOWN'):
245        """Reimport a batch given in CSV file.
246
247        CSV file must be of format as generated by createCSVLogFile
248        method.
249        """
250        path = os.path.join(self._getStoragePath(), filename)
251        reader = csv.DictReader(open(path, 'rb'), quoting=csv.QUOTE_ALL)
252        entry = reader.next()
253        cost = float(entry['cost'])
254        num = int(entry['ac'])
255        batch_name = '%s-%s' % (entry['serial'], num)
256        if batch_name in self.keys():
257            raise KeyError('Batch already exists: %s' % batch_name)
258        batch = AccessCodeBatch(
259            datetime.now(), creator, entry['serial'], cost, 0, num=num)
260        num_entries = 0
261        for row in reader:
262            pin = row['ac']
263            serial = int(row['serial'])
264            rand_num = pin.rsplit('-', 1)[-1]
265            batch.addAccessCode(serial, rand_num)
266            num_entries += 1
267        batch.entry_num = num_entries
268        self[batch_name] = batch
269        batch.createCSVLogFile()
270        return
271   
272class AccessCodePlugin(grok.GlobalUtility):
273    grok.name('accesscodes')
274    grok.implements(IWAeUPSIRPPluggable)
275
276    def setup(self, site, name, logger):
277        site['accesscodes'] = AccessCodeBatchContainer()
278        logger.info('Installed container for access code batches.')
279        return
280
281    def update(self, site, name, logger):
282        if not 'accesscodes' in site.keys():
283            logger.info('Updating site at %s. Installing access codes.' % (
284                    site,))
285            self.setup(site, name, logger)
286        else:
287            logger.info(
288                'AccessCodePlugin: Updating site at %s: Nothing to do.' % (
289                    site, ))
290        return
Note: See TracBrowser for help on using the repository browser.