[5068] | 1 | """Components to handle access codes. |
---|
| 2 | """ |
---|
[5110] | 3 | import csv |
---|
[5068] | 4 | import grok |
---|
[5110] | 5 | import os |
---|
[5118] | 6 | from BTrees.OIBTree import OIBTree |
---|
| 7 | from datetime import datetime |
---|
[5068] | 8 | from random import SystemRandom as random |
---|
[5073] | 9 | from waeup.sirp.interfaces import IWAeUPSIRPPluggable |
---|
[5079] | 10 | from waeup.sirp.accesscodes.interfaces import ( |
---|
| 11 | IAccessCode, IAccessCodeBatch, IAccessCodeBatchContainer |
---|
| 12 | ) |
---|
[5068] | 13 | |
---|
[5102] | 14 | class ManageACBatches(grok.Permission): |
---|
| 15 | grok.name('waeup.manageACBatches') |
---|
| 16 | |
---|
[5118] | 17 | class AccessCode(grok.Context): |
---|
[5068] | 18 | grok.implements(IAccessCode) |
---|
| 19 | |
---|
[5118] | 20 | def __init__(self, batch_serial, random_num, invalidation_date=None, |
---|
| 21 | student_id=None): |
---|
[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 |
---|
| 26 | |
---|
| 27 | @property |
---|
| 28 | def representation(self): |
---|
| 29 | return '%s-%s-%s' % ( |
---|
| 30 | self.batch_prefix, self.batch_num, self.random_num) |
---|
| 31 | |
---|
[5079] | 32 | @property |
---|
| 33 | def batch(self): |
---|
| 34 | return getattr(self, '__parent__', None) |
---|
[5086] | 35 | |
---|
[5079] | 36 | @property |
---|
| 37 | def batch_prefix(self): |
---|
| 38 | if self.batch is None: |
---|
| 39 | return '' |
---|
| 40 | return self.batch.prefix |
---|
[5086] | 41 | |
---|
[5079] | 42 | @property |
---|
| 43 | def batch_num(self): |
---|
| 44 | if self.batch is None: |
---|
| 45 | return '' |
---|
| 46 | return self.batch.num |
---|
[5068] | 47 | |
---|
[5118] | 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 | |
---|
| 60 | class AccessCodeBatch(grok.Model): |
---|
[5068] | 61 | """A batch of access codes. |
---|
| 62 | """ |
---|
| 63 | grok.implements(IAccessCodeBatch) |
---|
| 64 | |
---|
[5086] | 65 | def __init__(self, creation_date, creator, batch_prefix, cost, |
---|
| 66 | entry_num, num=None): |
---|
[5079] | 67 | super(AccessCodeBatch, self).__init__() |
---|
[5068] | 68 | self.creation_date = creation_date |
---|
| 69 | self.creator = creator |
---|
[5116] | 70 | self.prefix = batch_prefix.upper() |
---|
[5068] | 71 | self.cost = cost |
---|
| 72 | self.entry_num = entry_num |
---|
[5086] | 73 | self.num = num |
---|
[5118] | 74 | self.invalidated_num = 0 |
---|
| 75 | self._entries = list() |
---|
| 76 | self._acids = OIBTree() |
---|
| 77 | self._createEntries() |
---|
[5079] | 78 | |
---|
[5118] | 79 | def _createEntries(self): |
---|
[5079] | 80 | """Create the entries for this batch. |
---|
| 81 | """ |
---|
[5118] | 82 | for num, pin in enumerate(self._getNewRandomNum(num=self.entry_num)): |
---|
[5121] | 83 | self.addAccessCode(num, pin) |
---|
| 84 | self._p_changed = True # XXX: most probably not needed. |
---|
[5112] | 85 | return |
---|
[5118] | 86 | |
---|
| 87 | def _getNewRandomNum(self, num=1): |
---|
[5112] | 88 | """Create a set of ``num`` random numbers of 10 digits each. |
---|
[5086] | 89 | |
---|
[5068] | 90 | The number is returned as string. |
---|
| 91 | """ |
---|
[5118] | 92 | curr = 1 |
---|
| 93 | while curr <= num: |
---|
[5112] | 94 | pin = '' |
---|
[5068] | 95 | for x in range(10): |
---|
[5112] | 96 | pin += str(random().randint(0, 9)) |
---|
[5127] | 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 |
---|
[5073] | 101 | |
---|
[5118] | 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 | |
---|
[5121] | 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 | |
---|
[5118] | 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 | |
---|
[5110] | 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') |
---|
[5118] | 151 | ac_storage = self._getStoragePath() |
---|
[5110] | 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]) |
---|
[5118] | 159 | |
---|
| 160 | for value in self._entries: |
---|
[5110] | 161 | writer.writerow( |
---|
| 162 | [str(value.batch_serial), str(value.representation)] |
---|
| 163 | ) |
---|
[5118] | 164 | site = grok.getSite() |
---|
[5112] | 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) |
---|
[5110] | 170 | return os.path.basename(csv_path) |
---|
| 171 | |
---|
[5118] | 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, |
---|
[5123] | 193 | value.student_id, date |
---|
[5118] | 194 | ]) |
---|
| 195 | return os.path.basename(csv_path) |
---|
| 196 | |
---|
[5079] | 197 | class AccessCodeBatchContainer(grok.Container): |
---|
| 198 | grok.implements(IAccessCodeBatchContainer) |
---|
[5073] | 199 | |
---|
[5079] | 200 | def addBatch(self, batch): |
---|
[5086] | 201 | """Add a batch. |
---|
| 202 | """ |
---|
| 203 | batch.num = self.getNum(batch.prefix) |
---|
| 204 | key = "%s-%s" % (batch.prefix, batch.num) |
---|
[5079] | 205 | self[key] = batch |
---|
[5086] | 206 | self._p_changed = True |
---|
[5079] | 207 | |
---|
[5127] | 208 | def createBatch(self, creation_date, creator, batch_prefix, cost, |
---|
| 209 | entry_num): |
---|
| 210 | """Create and add a batch. |
---|
| 211 | """ |
---|
| 212 | batch_num = self.getNum(batch_prefix) |
---|
| 213 | batch = AccessCodeBatch(creation_date, creator, batch_prefix, |
---|
| 214 | cost, entry_num, num=batch_num) |
---|
| 215 | self.addBatch(batch) |
---|
| 216 | return batch |
---|
| 217 | |
---|
[5086] | 218 | def getNum(self, prefix): |
---|
| 219 | """Get next unused num for given prefix. |
---|
| 220 | """ |
---|
| 221 | num = 1 |
---|
[5116] | 222 | while self.get('%s-%s' % (prefix, num), None) is not None: |
---|
[5086] | 223 | num += 1 |
---|
| 224 | return num |
---|
[5095] | 225 | |
---|
[5073] | 226 | class AccessCodePlugin(grok.GlobalUtility): |
---|
| 227 | grok.name('accesscodes') |
---|
| 228 | grok.implements(IWAeUPSIRPPluggable) |
---|
| 229 | |
---|
| 230 | def setup(self, site, name, logger): |
---|
[5079] | 231 | site['accesscodes'] = AccessCodeBatchContainer() |
---|
| 232 | logger.info('Installed container for access code batches.') |
---|
| 233 | return |
---|
[5073] | 234 | |
---|
| 235 | def update(self, site, name, logger): |
---|
[5107] | 236 | if not 'accesscodes' in site.keys(): |
---|
| 237 | logger.info('Updating site at %s. Installing access codes.' % ( |
---|
| 238 | site,)) |
---|
| 239 | self.setup(site, name, logger) |
---|
| 240 | else: |
---|
| 241 | logger.info( |
---|
| 242 | 'AccessCodePlugin: Updating site at %s: Nothing to do.' % ( |
---|
| 243 | site, )) |
---|
| 244 | return |
---|