[5080] | 1 | :mod:`waeup.sirp.accesscodes.accesscodes` -- access codes (aka PINs) |
---|
| 2 | ******************************************************************** |
---|
[5068] | 3 | |
---|
[5080] | 4 | .. module:: waeup.sirp.accesscodes.accesscodes |
---|
[5068] | 5 | |
---|
[5080] | 6 | Components that represent access codes and related. |
---|
[5068] | 7 | |
---|
[5080] | 8 | .. :doctest: |
---|
[5138] | 9 | .. :layer: waeup.sirp.testing.WAeUPSIRPUnitTestLayer |
---|
[5068] | 10 | |
---|
[5118] | 11 | About access-codes |
---|
| 12 | ================== |
---|
[5068] | 13 | |
---|
[5118] | 14 | Access codes are ids used to grant first-time access to the system for |
---|
| 15 | students. |
---|
[5080] | 16 | |
---|
[5118] | 17 | They are normally not generated by third-party components but always |
---|
| 18 | part of batches. |
---|
[5068] | 19 | |
---|
[5118] | 20 | An access-code consists of three parts:: |
---|
[5087] | 21 | |
---|
[5118] | 22 | APP-12-0123456789 |
---|
| 23 | ^^^ ^^ ^^^^^^^^^^ |
---|
| 24 | A B C |
---|
[5087] | 25 | |
---|
[5118] | 26 | where ``A`` tells about the purpose of the code, ``B`` gives the |
---|
| 27 | number of batch the code belongs to (1 to 3 digits), and ``C`` is a |
---|
| 28 | unique random number of 10 digits. |
---|
[5068] | 29 | |
---|
[5118] | 30 | For the generation of the random number :mod:`waeup.sirp` requires a |
---|
| 31 | 'urandom' entropy provider which is available with most standard |
---|
| 32 | Unix/Linux systems. This makes the generated numbers relatively |
---|
| 33 | secure, especially when compared with recent PHP-based applications. |
---|
[5068] | 34 | |
---|
| 35 | |
---|
[5118] | 36 | AccessCode |
---|
| 37 | ========== |
---|
[5068] | 38 | |
---|
[5118] | 39 | .. class:: AccessCode(batch_serial, random_num[,invalidation_date=None[, student_id=None]]) |
---|
[5068] | 40 | |
---|
[5118] | 41 | You normally shouldn't create standalone access-codes. Use |
---|
| 42 | instances of :class:`AccessCodeBatch` instead as they generate them |
---|
| 43 | (in masses) and care for them. |
---|
[5068] | 44 | |
---|
[5118] | 45 | Note, that :class:`AccessCode` instances are not persistent on |
---|
| 46 | themselves. They have to be stored inside a persistent object (like |
---|
| 47 | :class:`AccessCodeBatch`) to be kept. |
---|
[5080] | 48 | |
---|
[5118] | 49 | The class implements |
---|
| 50 | :mod:`waeup.sirp.accesscodes.interfaces.IAccessCode`: |
---|
[5068] | 51 | |
---|
[5118] | 52 | >>> from waeup.sirp.accesscodes.interfaces import IAccessCode |
---|
| 53 | >>> from waeup.sirp.accesscodes.accesscodes import AccessCode |
---|
[5068] | 54 | >>> from zope.interface.verify import verifyClass |
---|
| 55 | >>> verifyClass(IAccessCode, AccessCode) |
---|
| 56 | True |
---|
| 57 | |
---|
[5118] | 58 | .. attribute:: representation |
---|
[5109] | 59 | |
---|
[5118] | 60 | The 'full' id of an access-code as described above. Something |
---|
| 61 | like ``'APP-12-0123456789'``. |
---|
[5109] | 62 | |
---|
[5118] | 63 | Read-only attribute. |
---|
| 64 | |
---|
| 65 | .. attribute:: batch_serial |
---|
| 66 | |
---|
| 67 | Serial number of this access-code inside the batch. |
---|
| 68 | |
---|
| 69 | .. note:: XXX: Do we really need this? |
---|
| 70 | |
---|
| 71 | Subject to be dropped. |
---|
| 72 | |
---|
| 73 | .. attribute:: student_id |
---|
| 74 | |
---|
| 75 | A string or ``None``. Set when an access-code is |
---|
| 76 | invalidated. ``None`` by default. |
---|
| 77 | |
---|
| 78 | .. attribute:: batch_prefix |
---|
| 79 | |
---|
| 80 | The prefix of the batch the access-code belongs to. |
---|
| 81 | |
---|
| 82 | Read-only attribute. |
---|
| 83 | |
---|
| 84 | .. attribute:: batch_num |
---|
| 85 | |
---|
| 86 | The number of the batch the access-code belongs to. |
---|
| 87 | |
---|
| 88 | Read-only attribute. |
---|
| 89 | |
---|
| 90 | .. attribute:: cost |
---|
| 91 | |
---|
| 92 | What the access-code costs. A float. |
---|
| 93 | |
---|
| 94 | Read-only attribute. |
---|
| 95 | |
---|
| 96 | .. attribute:: invalidation_date |
---|
| 97 | |
---|
| 98 | Python datetime when the access code was invalidated, or |
---|
| 99 | ``None``. ``None`` by default. |
---|
| 100 | |
---|
| 101 | Read-only attribute. Only batches are supposed to set this value. |
---|
| 102 | |
---|
| 103 | Access codes that are not part of a batch, will give strange |
---|
| 104 | representations: |
---|
| 105 | |
---|
[5128] | 106 | >>> ac = AccessCode(None, '9999999999') |
---|
[5109] | 107 | >>> ac.representation |
---|
| 108 | '--<10-DIGITS>' |
---|
| 109 | |
---|
[5128] | 110 | Also the ``cost`` will not be set: |
---|
[5118] | 111 | |
---|
[5128] | 112 | >>> ac.cost is None |
---|
| 113 | True |
---|
| 114 | |
---|
| 115 | |
---|
[5118] | 116 | AccessCodeBatch |
---|
| 117 | =============== |
---|
| 118 | |
---|
| 119 | .. class:: AccessCodeBatch(creation_date, creator, batch_prefix, cost, entry_num, num) |
---|
| 120 | |
---|
| 121 | Create a batch of access-codes. |
---|
| 122 | |
---|
| 123 | :param creation_date: python datetime |
---|
| 124 | :param creator: creators user id |
---|
| 125 | :type creator: string |
---|
| 126 | :param batch_prefix: prefix of this batch |
---|
| 127 | :param cost: cost per access code |
---|
| 128 | :type cost: float |
---|
| 129 | :param entry_num: number of access codes to create |
---|
| 130 | :param num: number of this batch |
---|
| 131 | |
---|
| 132 | A persistent :class:`grok.Model`. It implements |
---|
| 133 | :class:`waeup.sirp.accesscodes.interfaces.IAccessCodeBatch`. |
---|
| 134 | |
---|
| 135 | When creating a batch, all entries (access-codes) are generated as |
---|
| 136 | well. |
---|
| 137 | |
---|
| 138 | .. attribute:: creation_date |
---|
| 139 | |
---|
| 140 | The datetime when the batch was created. |
---|
| 141 | |
---|
| 142 | .. attribute:: creator |
---|
| 143 | |
---|
| 144 | String with user id of the user that generated the batch. |
---|
| 145 | |
---|
| 146 | .. attribute:: cost |
---|
| 147 | |
---|
| 148 | Float representing the costs for a single access-code. All |
---|
| 149 | entries inside the batch share the same cost. |
---|
| 150 | |
---|
| 151 | .. attribute:: entry_num |
---|
| 152 | |
---|
| 153 | Number of entries (access-codes) inside the batch. |
---|
| 154 | |
---|
| 155 | .. attribute:: invalidated_num |
---|
| 156 | |
---|
| 157 | Number of entries that were already invalidated. |
---|
| 158 | |
---|
| 159 | .. attribute:: prefix |
---|
| 160 | |
---|
| 161 | Prefix of the batch. This tells about the purpose of this batch. |
---|
| 162 | |
---|
| 163 | .. attribute:: num |
---|
| 164 | |
---|
| 165 | Number of this batch. For a certain prefix there can exist |
---|
| 166 | several batches, which are numbered in increasing order. The |
---|
| 167 | number is normally computed by the |
---|
| 168 | :class:`AccessCodeBatchContainer` in which batches are |
---|
| 169 | stored. |
---|
| 170 | |
---|
| 171 | .. seealso:: :class:`AccessCodeBatchContainer` |
---|
| 172 | |
---|
| 173 | .. method:: entries() |
---|
| 174 | |
---|
| 175 | Get all accesscodes stored in the batch. |
---|
| 176 | |
---|
| 177 | Returns a generator over all stored entries. |
---|
| 178 | |
---|
| 179 | .. method:: getAccessCode(acesscode_id) |
---|
| 180 | |
---|
| 181 | Get the :class:`AccessCode` object for the given |
---|
| 182 | ``accesscode_id``. |
---|
| 183 | |
---|
| 184 | Certain single access codes can be accessed inside a batch by |
---|
| 185 | their representation (i.e. something like ``'APP-12-0123456789'``. |
---|
| 186 | |
---|
| 187 | When a code cannot be found :exc:`KeyError` is raised. |
---|
| 188 | |
---|
[5122] | 189 | .. method:: addAccessCode(serial_num, random_num) |
---|
| 190 | |
---|
| 191 | Add an access code to the batch. |
---|
| 192 | |
---|
| 193 | ``serial_num`` denotes the serial number of the new access-code |
---|
| 194 | inside the batch. ``random_num`` is a string of 10 digits unique |
---|
| 195 | in this batch. |
---|
| 196 | |
---|
[5118] | 197 | .. method:: invalidate(ac_id[, student_id=None]) |
---|
| 198 | |
---|
| 199 | Invalidate the access-code with ID ``ac_id``. |
---|
| 200 | |
---|
| 201 | Sets also the ``student_id`` attribute of the respective |
---|
| 202 | :class:`AccessCode` entry. |
---|
| 203 | |
---|
| 204 | .. method:: createCSVLogFile() |
---|
| 205 | |
---|
| 206 | Create a CSV file with data in batch. |
---|
| 207 | |
---|
| 208 | Data will not contain invalidation date nor student ids. File |
---|
| 209 | will be created in ``accesscodes`` subdir of data center storage |
---|
| 210 | path. |
---|
| 211 | |
---|
| 212 | Returns name of created file. |
---|
| 213 | |
---|
| 214 | .. method:: archive() |
---|
| 215 | |
---|
| 216 | Create a CSV file for archive. Archive files contain also |
---|
| 217 | ``student_id`` and ``invalidation_date``. |
---|
| 218 | |
---|
| 219 | Returns name of created file. |
---|
| 220 | |
---|
| 221 | |
---|
| 222 | Examples |
---|
| 223 | -------- |
---|
| 224 | |
---|
| 225 | :class:`AccessCodeBatch` implements :class:`IAccessCodeBatch`: |
---|
| 226 | |
---|
| 227 | >>> from waeup.sirp.accesscodes.interfaces import IAccessCodeBatch |
---|
| 228 | >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch |
---|
| 229 | >>> from zope.interface.verify import verifyClass |
---|
| 230 | >>> verifyClass(IAccessCodeBatch, AccessCodeBatch) |
---|
| 231 | True |
---|
| 232 | |
---|
| 233 | Creating a batch of three access-codes, with a cost of ``12.12`` per |
---|
| 234 | code, the batch prefix ``APP``, batch number ``10``, creator ID |
---|
| 235 | ``Fred`` and some arbitrary creation date: |
---|
| 236 | |
---|
| 237 | >>> import datetime |
---|
| 238 | >>> from waeup.sirp.accesscodes.accesscodes import AccessCodeBatch |
---|
| 239 | >>> batch = AccessCodeBatch( |
---|
| 240 | ... datetime.datetime(2009, 12, 23), 'Fred','APP', 12.12, 3, num=10) |
---|
| 241 | |
---|
| 242 | Getting all access-codes from a batch: |
---|
| 243 | |
---|
| 244 | >>> ac_codes = batch.entries() |
---|
| 245 | >>> ac_codes |
---|
| 246 | <generator object at 0x...> |
---|
| 247 | |
---|
| 248 | >>> [x.representation for x in ac_codes] |
---|
| 249 | ['APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>', 'APP-10-<10-DIGITS>'] |
---|
| 250 | |
---|
| 251 | >>> [x for x in batch.entries()] |
---|
| 252 | [<waeup.sirp...AccessCode object at 0x...>, ...] |
---|
| 253 | |
---|
| 254 | Getting a single entry from the batch: |
---|
| 255 | |
---|
| 256 | >>> ac_id = list(batch.entries())[0].representation |
---|
| 257 | >>> ac = batch.getAccessCode(ac_id) |
---|
| 258 | >>> ac |
---|
| 259 | <waeup.sirp...AccessCode object at 0x...> |
---|
| 260 | |
---|
| 261 | >>> ac is list(batch.entries())[0] |
---|
| 262 | True |
---|
| 263 | |
---|
| 264 | Trying to get a single not-existent entry from a batch: |
---|
| 265 | |
---|
| 266 | >>> batch.getAccessCode('blah') |
---|
| 267 | Traceback (most recent call last): |
---|
| 268 | ... |
---|
| 269 | KeyError: 'blah' |
---|
| 270 | |
---|
| 271 | Invalidating an entry: |
---|
| 272 | |
---|
| 273 | >>> batch.invalidated_num |
---|
| 274 | 0 |
---|
| 275 | |
---|
| 276 | >>> str(ac.invalidation_date), str(ac.student_id) |
---|
| 277 | ('None', 'None') |
---|
| 278 | |
---|
| 279 | >>> batch.invalidate(ac_id) |
---|
| 280 | >>> batch.invalidated_num |
---|
| 281 | 1 |
---|
| 282 | |
---|
| 283 | >>> ac.invalidation_date , str(ac.student_id) |
---|
| 284 | (datetime.datetime(...), 'None') |
---|
| 285 | |
---|
| 286 | >>> batch.invalidate(ac_id, 'some_user_id') |
---|
| 287 | >>> ac.student_id |
---|
| 288 | 'some_user_id' |
---|
| 289 | |
---|
[5122] | 290 | Access codes get their ``cost`` from the batch they belong to. Note, |
---|
| 291 | that it is advisable to print costs always using a format, as Python |
---|
| 292 | floats are often represented by irritating values: |
---|
[5118] | 293 | |
---|
[5122] | 294 | >>> ac.cost |
---|
| 295 | 12.119999999999999 |
---|
| 296 | |
---|
| 297 | >>> print "%0.2f" % ac.cost |
---|
| 298 | 12.12 |
---|
| 299 | |
---|
| 300 | |
---|
| 301 | AccessCodeBatchContainer |
---|
| 302 | ======================== |
---|
| 303 | |
---|
| 304 | .. class:: AccessCodeBatchContainer() |
---|
| 305 | |
---|
| 306 | A container for access code batches. |
---|
| 307 | |
---|
| 308 | .. method:: addBatch(batch) |
---|
| 309 | |
---|
[5128] | 310 | Add a batch in this container. You should make sure, that |
---|
| 311 | entries in the given batch are set correctly. |
---|
[5122] | 312 | |
---|
[5128] | 313 | .. method:: createBatch(creation_date, creator, batch_prefix, cost, entry_num) |
---|
| 314 | |
---|
| 315 | Create a batch inside this container. Returns the batch created. |
---|
| 316 | |
---|
| 317 | :param creation_date: python datetime |
---|
| 318 | :param creator: creators user id |
---|
| 319 | :type creator: string |
---|
| 320 | :param batch_prefix: prefix of this batch |
---|
| 321 | :param cost: cost per access code |
---|
| 322 | :type cost: float |
---|
| 323 | :param entry_num: number of access codes to create |
---|
| 324 | |
---|
[5122] | 325 | .. method:: getNum(prefix) |
---|
| 326 | |
---|
| 327 | Get next unused num for a new batch and a given prefix. |
---|
| 328 | |
---|
| 329 | Batches for a given prefix are numerated. Whenever a new batch |
---|
| 330 | is created and other batches inside the container already have |
---|
| 331 | the same prefix, the new one will get the lowest unused number. |
---|
| 332 | |
---|
[5132] | 333 | .. method:: getImportFiles() |
---|
[5122] | 334 | |
---|
[5132] | 335 | Get a list of basenames of available import files, suitable for |
---|
| 336 | feeding :meth:`reimport`. |
---|
| 337 | |
---|
| 338 | .. method:: reimport(filename, creator=u'UNKNOWN') |
---|
| 339 | |
---|
| 340 | Reimport a CSV log file of previously created AC batch. |
---|
| 341 | |
---|
| 342 | ``filename`` is the name (basename) of the file residing in the |
---|
| 343 | accesscode storage's ``import`` directory.``creator`` is the |
---|
| 344 | user ID of the current user. |
---|
| 345 | |
---|
| 346 | |
---|
[5109] | 347 | Access code plugin |
---|
| 348 | ================== |
---|
| 349 | |
---|
| 350 | .. class:: AccessCodePlugin |
---|
| 351 | |
---|
[5138] | 352 | A `waeup.sirp` plugin that updates existing WAeUP SIRP university |
---|
| 353 | instances so that they provide support for access-codes. |
---|
| 354 | |
---|
[5109] | 355 | .. attribute:: grok.implements(IWAeUPSIRPPluggable) |
---|
| 356 | .. attribute:: grok.name('accesscodes') |
---|
| 357 | |
---|
| 358 | .. method:: setup(site, name, logger) |
---|
| 359 | |
---|
| 360 | Create an accesscodebatch container in ``site``. Any events are |
---|
| 361 | logged to ``logger``. |
---|
| 362 | |
---|
| 363 | .. method:: update(site, name, logger) |
---|
| 364 | |
---|
| 365 | Check if ``site`` contains an accesscodebatch container and add |
---|
| 366 | it if missing. Any events are logged to ``logger``. |
---|
| 367 | |
---|
| 368 | The AccessCodePlugin is available as a global named utility for the |
---|
| 369 | IWAeUPSIRPPluggable interface named ``accesscodes``. |
---|
| 370 | |
---|
[5138] | 371 | It is looked up by a university instance when created, so that this |
---|
| 372 | instance has nothing to know about accesscodes at all and can |
---|
| 373 | although provide support for them. |
---|
[5109] | 374 | |
---|
| 375 | >>> from zope.component import getUtility |
---|
| 376 | >>> from waeup.sirp.interfaces import IWAeUPSIRPPluggable |
---|
| 377 | >>> plugin = getUtility(IWAeUPSIRPPluggable, name='accesscodes') |
---|
| 378 | >>> plugin |
---|
| 379 | <waeup.sirp.accesscodes.accesscodes.AccessCodePlugin object at 0x...> |
---|
| 380 | |
---|
| 381 | It provides a `setup()` and an `update()` method. Both have to be |
---|
| 382 | fed with a site object (the `IUniversity` instance to modify), a |
---|
| 383 | logger and a name. |
---|
| 384 | |
---|
| 385 | We create a faked site and a logger: |
---|
| 386 | |
---|
| 387 | >>> import logging |
---|
| 388 | >>> faked_site = dict() |
---|
| 389 | >>> logger = logging.getLogger('ac_test') |
---|
| 390 | >>> logger.setLevel(logging.DEBUG) |
---|
| 391 | >>> ch = logging.FileHandler('ac_tests.log', 'w') |
---|
| 392 | >>> ch.setLevel(logging.DEBUG) |
---|
| 393 | >>> logger.addHandler(ch) |
---|
| 394 | |
---|
| 395 | Now we can install our stuff in the faked site: |
---|
| 396 | |
---|
| 397 | >>> plugin.setup(faked_site, 'blah', logger) |
---|
| 398 | |
---|
| 399 | The faked site now has an access code container: |
---|
| 400 | |
---|
| 401 | >>> faked_site.keys() |
---|
| 402 | ['accesscodes'] |
---|
| 403 | |
---|
| 404 | The action is described in the log: |
---|
| 405 | |
---|
| 406 | >>> print open('ac_tests.log', 'r').read() |
---|
| 407 | Installed container for access code batches. |
---|
| 408 | |
---|
| 409 | We can also update an existing site, by calling `update()`: |
---|
| 410 | |
---|
| 411 | >>> plugin.update(faked_site, 'blah', logger) |
---|
| 412 | |
---|
| 413 | There was nothing to do for the updater: |
---|
| 414 | |
---|
| 415 | >>> print open('ac_tests.log', 'r').read() |
---|
| 416 | Installed container for access code batches. |
---|
| 417 | AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do. |
---|
| 418 | |
---|
| 419 | But if we remove the created batch container and call the updater, it |
---|
| 420 | will create a new one: |
---|
| 421 | |
---|
| 422 | >>> del faked_site['accesscodes'] |
---|
| 423 | >>> plugin.update(faked_site, 'blah', logger) |
---|
| 424 | >>> print open('ac_tests.log', 'r').read() |
---|
| 425 | Installed container for access code batches. |
---|
| 426 | AccessCodePlugin: Updating site at {'accesscodes'...: Nothing to do. |
---|
| 427 | Updating site at {}. Installing access codes. |
---|
| 428 | Installed container for access code batches. |
---|
| 429 | |
---|
| 430 | Clean up: |
---|
| 431 | |
---|
| 432 | >>> import os |
---|
| 433 | >>> os.unlink('ac_tests.log') |
---|