Package bosco :: Module event
[hide private]
[frames] | no frames]

Source Code for Module bosco.event

  1  #/usr/bin/env python 
  2  # 
  3  #    Copyright (C) 2008  Gaudenz Steinlin <gaudenz@soziologie.ch> 
  4  # 
  5  #    This program is free software: you can redistribute it and/or modify 
  6  #    it under the terms of the GNU General Public License as published by 
  7  #    the Free Software Foundation, either version 3 of the License, or 
  8  #    (at your option) any later version. 
  9  # 
 10  #    This program is distributed in the hope that it will be useful, 
 11  #    but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  #    GNU General Public License for more details. 
 14  # 
 15  #    You should have received a copy of the GNU General Public License 
 16  #    along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 17  """ 
 18  event.py - Event configuration. All front-end programs should use 
 19             conf.event which is a subclass of Event 
 20  """ 
 21   
 22  from datetime import timedelta 
 23   
 24  from course import Course, CombinedCourse 
 25  from runner import (Category, RunnerException) 
 26  from ranking import (SequenceCourseValidator, TimeScoreing, SelfstartStarttime, 
 27                       RelayStarttime, RelayMassstartStarttime, MassstartStarttime, 
 28                       Ranking, RelayRanking, CourseValidator, OpenRuns, 
 29                       ControlPunchtimeScoreing, RelayScoreing, 
 30                       Relay24hScoreing, Relay12hScoreing, 
 31                       ValidationError, UnscoreableException, Validator, RoundCountScoreing) 
 32   
 33  from formatter import MakoRankingFormatter 
34 35 -class EventException(Exception):
36 pass
37
38 -class Event(object):
39 """Model of all event specific ranking information. The default 40 implementation uses SequenceCourseValidator, TimeScoreing and SelfstartStarttime. 41 Subclass this class to customize your event.""" 42
43 - def __init__(self, header = {}, extra_rankings = [], 44 template_dir = 'templates', 45 print_template = 'course.tex', html_template = 'course.html', 46 cache = None, store = None):
47 """ 48 @param header: gerneral information for the ranking header. 49 Typical keys: 50 'organiser', 'map', 'place', 'date', 'event' 51 @type header: dict of strings 52 @param extra_rankings: list of extra rankings 53 @type extra_rankings: list of tuples (description (string), ranking_args), 54 ranking_args is a dicts with keys for the 55 ranking method: 56 'rankable', 'scoreing_class', 57 'validation_class', 'scoreing_args', 'validation_args', 58 'reverse', 59 @param template_dir: templates directory 60 @param print_template: template for printing (latex) 61 @param html_template: template for html output (screen display) 62 @param cache: Cache to use for this object 63 @param store: Store to retrieve possible rankings 64 """ 65 66 self._strategies = {} 67 self._cache = cache 68 self._header = header 69 self._extra_rankings = extra_rankings 70 self._template_dir = template_dir 71 self._template = {} 72 self._template['print'] = print_template 73 self._template['html'] = html_template 74 self._store = store 75 76 # add list of rankings to header 77 self._header['rankings'] = [ r[0] for r in self.list_rankings() ]
78 79 @staticmethod
80 - def _var_key(var):
81 """recursively make a hashable key out of a variable""" 82 if type(var) == list: 83 return tuple([Event._var_key(v) for v in var]) 84 elif type(var) == dict: 85 return tuple([(k, Event._var_key(v)) for k,v in var.items()]) 86 else: 87 return var
88 89 @staticmethod
90 - def _key(cls, args):
91 """make a hashable key out of cls and args""" 92 return (cls, Event._var_key(args))
93
94 - def remove_cache(self):
95 """Removes the cache. No caching occurs anymore.""" 96 self._cache = None 97 self._strategies = {}
98
99 - def clear_cache(self, obj = None):
100 """ 101 Clear the cache 102 @param obj: Only clear the cache for obj 103 """ 104 if obj is not None: 105 self._cache.update(obj) 106 else: 107 self._cache.clear()
108
109 - def validate(self, obj, validator_class = None, args = None):
110 # Don't define args={}, default arguments are created at function definition time 111 # and you would end up modifing the default args below! You must create a new 112 # empty dictionary on every invocation! 113 114 """ 115 Get a validator 116 @param obj: object to validate, 117 @param validator_class: validation class used 118 @param args: dict of keyword arguments for the validation strategy object 119 @return: validation result from validator_class.validate(obj) 120 @see: Validator for more information about validation classes 121 """ 122 123 from run import Run 124 from runner import Runner 125 126 if obj is None: 127 raise ValidationError("Can't validate objects of type %s" % type(obj)) 128 129 if validator_class is None: 130 validator_class = SequenceCourseValidator 131 132 if args is None: 133 args = {} 134 135 if 'cache' not in args: 136 args['cache'] = self._cache 137 138 if issubclass(validator_class, CourseValidator): 139 if type(obj) == Runner: 140 # validate run of this runner 141 obj = obj.run 142 143 if type(obj) == Run and 'course' not in args: 144 if obj.course is None: 145 raise ValidationError("Can't validate run without course") 146 args['course'] = obj.course 147 148 if self._key(validator_class, args) not in self._strategies: 149 # create validator instance 150 self._strategies[self._key(validator_class, args)] = validator_class(**args) 151 152 return self._strategies[self._key(validator_class, args)].validate(obj)
153
154 - def score(self, obj, scoreing_class = None, args = None):
155 """ 156 Get the score of an object 157 @param obj: object to score 158 @param scoreing_class: scoreing strategy used 159 @param args: additional arguments for the scoreing class's constructor 160 @type args: dict of keyword arguments 161 @return: scoreing result from scoreing_class.score(obj) 162 @see: AbstractScoreing for more information about scoreing classes 163 """ 164 165 from runner import Runner 166 167 if obj is None: 168 raise UnscoreableException("Can't score objects of type %s" % type(obj)) 169 170 if args is None: 171 args = {} 172 173 if not 'cache' in args: 174 args['cache'] = self._cache 175 176 if scoreing_class is None: 177 scoreing_class = TimeScoreing 178 if 'starttime_strategy' not in args: 179 args['starttime_strategy'] = SelfstartStarttime(args['cache']) 180 181 if self._key(scoreing_class, args) not in self._strategies: 182 # create scoreing instance 183 if not 'cache' in args: 184 args['cache'] = self._cache 185 self._strategies[self._key(scoreing_class, args)] = scoreing_class(**args) 186 187 if type(obj) == Runner: 188 # Score run of this runner 189 obj = obj.run 190 191 return self._strategies[self._key(scoreing_class, args)].score(obj)
192
193 - def ranking(self, obj, scoreing_class = None, validation_class = None, 194 scoreing_args = None, validation_args = None, reverse = False):
195 """ 196 Get a ranking for a Rankable object 197 @param obj: ranked object (Category, Course, ...) 198 @param scoreing_class: scoreing strategy used, None for default strategy 199 @param validation_class: validation strategy used, None for default strategy 200 @param scoreing_args: scoreing args, None for default args 201 @param validation_args: validation args, None for default args 202 @param reverse: produce reversed ranking 203 """ 204 205 if type(obj) == OpenRuns: 206 scoreing_class = scoreing_class or ControlPunchtimeScoreing 207 validation_class = validation_class or ControlPunchtimeScoreing 208 validation_args = validation_args or scoreing_args 209 reverse = True 210 211 return Ranking(obj, self, scoreing_class, validation_class, 212 scoreing_args, validation_args, reverse)
213
214 - def format_ranking(self, rankings, type = 'html'):
215 """ 216 @param ranking: Rankings to format 217 @type ranking: list of objects of class Ranking 218 @param type: 'html' (default) or 'print' 219 @return: RankingFormatter object for the ranking 220 """ 221 222 return MakoRankingFormatter(rankings, self._header, 223 self._template[type], self._template_dir)
224 - def list_rankings(self):
225 """ 226 @return: list of possible rankings 227 """ 228 # all courses 229 l = [ (c.code, self.ranking(c)) for c in self.list_courses()] 230 231 # all categories 232 l.extend([ (c.name, self.ranking(c)) for c in self.list_categories() ]) 233 234 # all extra rankings 235 l.extend([ (e[0], self.ranking(**e[1])) for e in self._extra_rankings ]) 236 237 l.sort(key = lambda x: x[0]) 238 return l
239
240 - def list_courses(self):
241 """ 242 @return: list of all courses in the event 243 """ 244 return list(self._store.find(Course))
245
246 - def list_categories(self):
247 """ 248 @return: list of all categories in the event 249 """ 250 return list(self._store.find(Category))
251
252 -class MassstartEvent(Event):
253 """Massstart individual race event.""" 254
255 - def __init__(self, categories, strict=True, header={}, extra_rankings=[], 256 template_dir = 'templates', 257 print_template = 'relay.tex', 258 html_template = 'relay.html', 259 cache = None, store = None):
260 """ 261 @param categories: dict keyed with category names containing category definitions: 262 dicts with the following keys: 263 * 'variants': tuple of course codes that are valid variants for this leg. 264 * 'starttime': start for this category 265 @param strict: boolean value wheter manual starttimes on the card take precedence over 266 the category starttime (strict = False) or the massstart time is strict 267 (strict = True) 268 @see: Event for other arguments 269 """ 270 271 self.categories = categories 272 self._strict = strict 273 super(MassstartEvent, self).__init__(header, extra_rankings, template_dir, print_template, 274 html_template, cache, store)
275
276 - def score(self, obj, scoreing_class = None, args = None):
277 278 if args is None: 279 args = {} 280 281 if 'cache' not in args: 282 args['cache'] = self._cache 283 284 if 'starttime_strategy' not in args: 285 try: 286 category = obj.category.name 287 except AttributeError: 288 try: 289 category = obj.sicard.runner.category.name 290 except AttributeError: 291 raise UnscoreableException(u"Can't score runner without a category") 292 starttime = self.categories[category]['starttime'] 293 args['starttime_strategy'] = MassstartStarttime(starttime, self._strict, args['cache']) 294 295 return super(MassstartEvent, self).score(obj, scoreing_class, args)
296
297 -class RelayEvent(Event):
298 """Event class for a traditional relay.""" 299
300 - def __init__(self, legs, header={}, extra_rankings=[], 301 template_dir = 'templates', 302 print_template = 'relay.tex', 303 html_template = 'relay.html', 304 cache = None, store = None):
305 """ 306 @param legs: dict keyed with category names containing relay category definitions: 307 lists of leg dicts with the following keys: 308 * 'name': Name of the Leg 309 * 'variants': tuple of course codes that are valid variants for this leg. 310 * 'starttime': start time for all non replaced runners, type datetime 311 * 'defaulttime': time scored if no runner of the team successfully 312 completes this leg, type timedelta or None if there is no defaulttime 313 @see: Event for other arguments 314 """ 315 316 # assign legs first as this is needed to list all rankings in Event.__init__ 317 self._legs = legs 318 319 Event.__init__(self, header, extra_rankings, template_dir, print_template, 320 html_template, cache, store) 321 322 # create dict of starttimes with course codes as key 323 # used for easier access to the starttimes 324 self._starttimes = {} 325 for cat, legs in self._legs.iteritems(): 326 self._starttimes[cat] = {} 327 for l in legs: 328 for c in l['variants']: 329 if c in self._starttimes[cat]: 330 raise EventException('Multiple legs with the same course are not supported in RelayEvent!') 331 self._starttimes[cat][c] = l['starttime'] 332 333 # set validation and scoreing strategies 334 # TODO: This is a temporary workaround until everything is adapted to "new-style" validation 335 course = self._store.find(Course, Course.code == c).one() 336 if course is None: 337 # Skip this if course is not yet defined, needs a restart after the course is loaded 338 continue 339 reorder = l.get('reorder', {}).get(c, None) 340 course._validator = SequenceCourseValidator(course, reorder, cache=cache) 341 course._scoreing = TimeScoreing(starttime_strategy=RelayMassstartStarttime(l['starttime'], cache=cache))
342
343 - def validate(self, obj, validator_class = None, args = None):
344 345 from runner import Team 346 from run import Run 347 348 # use new style validation for runs 349 if type(obj) == Run: 350 return obj.validate(validator_class, args) 351 352 if args is None: 353 args = {} 354 355 if type(obj) == Team and validator_class is None: 356 validator_class = RelayScoreing 357 cat = obj.category.name 358 # build arguments for the RelayScoreing object 359 if not 'legs' in args: 360 # score all legs if no leg parameter is specified 361 args['legs'] = len(self._legs[cat]) 362 # build args parameter for RelayScoreing 363 if type(args['legs']) == int: 364 args['legs'] = self._legs[cat][:args['legs']] 365 args['event'] = self 366 367 # defer validation to the superclass 368 return Event.validate(self, obj, validator_class, args)
369
370 - def score(self, obj, scoreing_class = None, args = None):
371 """ 372 @args: for a team the key 'legs' specifies to score after 373 this leg number (starting from 1). 374 @see: Event 375 """ 376 377 from runner import Team 378 from run import Run 379 380 # use new style scoreing for runs 381 if type(obj) == Run: 382 return obj.score(scoreing_class, args) 383 384 if args is None: 385 args = {} 386 387 if not 'cache' in args: 388 args['cache'] = self._cache 389 390 if type(obj) == Run and scoreing_class is None: 391 if obj.course is None: 392 raise UnscoreableException("Can't score a relay leg without a course.") 393 if obj.sicard.runner is None: 394 raise UnscoreableException("Can't score a relay leg without a runner.") 395 if obj.sicard.runner.team is None: 396 raise UnscoreableException("Can't score a relay leg without a team.") 397 if obj.sicard.runner.team.category is None: 398 raise UnscoreableException("Can't score a realy leg without a category.") 399 scoreing_class = TimeScoreing 400 try: 401 cat = obj.sicard.runner.team.category.name 402 args['starttime_strategy'] = RelayMassstartStarttime(self._starttimes[cat][obj.course.code], cache = args['cache']) 403 except AttributeError: 404 # default to selfstart if we can't find the runner or team 405 args['starttime_strategy'] = SelfstartStarttime() 406 407 elif type(obj) == Team and scoreing_class is None: 408 scoreing_class = RelayScoreing 409 cat = obj.category.name 410 411 # build arguments for the RelayScoreing object 412 if not 'legs' in args: 413 # score all legs if no leg parameter is specified 414 args['legs'] = self._legs[cat][:len(self._legs[cat])] 415 # build args parameter for RelayScoreing 416 if type(args['legs']) == int: 417 args['legs'] = self._legs[cat][:args['legs']] 418 419 args['event'] = self 420 421 # if scoreing_class is not None use specified scoreing_class 422 return Event.score(self, obj, scoreing_class, args)
423
424 - def ranking(self, obj, scoreing_class = None, validation_class = None, 425 scoreing_args = None, validation_args = None, reverse = False):
426 """ 427 @see: Event.ranking 428 """ 429 430 if type(obj) == Category: 431 return RelayRanking(obj, self, scoreing_class, validation_class, 432 scoreing_args, validation_args, reverse) 433 434 return super(RelayEvent, self).ranking(obj, scoreing_class, validation_class, 435 scoreing_args, validation_args, reverse)
436
437 - def list_rankings(self):
438 l = [] 439 for c in self.list_categories(): 440 for leg in self._legs[c.name]: 441 l.append((leg['name'], self.ranking(CombinedCourse(leg['variants'], leg['name'], self._store)))) 442 for i,leg in enumerate(self._legs[c.name]): 443 l.append(('%s %s' % (c.name, leg['name']), self.ranking(c, scoreing_args = {'legs': i+1}, 444 validation_args = {'legs': i+1}))) 445 return l
446
447 - def list_legs(self, category):
448 """ 449 Lists all legs in a category. 450 @category: Category object to list legs for 451 @return: list of CombinedCourse objects 452 """ 453 454 result = [] 455 for leg in self._legs[category.name]: 456 result.append(CombinedCourse(leg['variants'], leg['name'], self._store)) 457 return result
458
459 -class Relay24hEvent(Event):
460 """Event class for the 24h orientieering relay.""" 461
462 - def __init__(self, starttime_24h, starttime_12h, speed, 463 header = {}, 464 duration_24h = timedelta(hours=24), 465 duration_12h = timedelta(hours=12), 466 extra_rankings = [], 467 template_dir = 'templates', 468 print_template = '24h.tex', 469 html_template = '24h.html', 470 cache = None, store = None):
471 472 Event.__init__(self, header, extra_rankings, template_dir, print_template, 473 html_template, 474 cache, store) 475 476 self._starttime = {u'24h':starttime_24h, 477 u'12h':starttime_12h} 478 self._speed = speed 479 self._duration = {u'24h':duration_24h, 480 u'12h':duration_12h} 481 self._strategy = {u'24h':Relay24hScoreing, 482 u'12h':Relay12hScoreing}
483
484 - def _get_team_strategy(self, team, args):
485 cat = team.category.name 486 args['event'] = self 487 if 'starttime' not in args: 488 args['starttime'] = self._starttime[cat] 489 if 'speed' not in args: 490 args['speed'] = self._speed 491 if 'duration' not in args: 492 args['duration'] = self._duration[cat] 493 return (self._strategy[cat], args)
494
495 - def _get_run_strategy(self, run, args):
496 try: 497 cat = run.sicard.runner.team.category.name 498 except AttributeError: 499 return (None, args) 500 501 if 'starttime_strategy' not in args: 502 args['starttime_strategy'] = RelayStarttime(self._starttime[cat], 503 ordered = False, 504 cache = self._cache) 505 return (TimeScoreing, args)
506
507 - def validate(self, obj, validator_class = None, args = None):
508 509 from runner import Team 510 511 if args is None: 512 args = {} 513 514 if type(obj) == Team and validator_class is None: 515 (validator_class, args) = self._get_team_strategy(obj, args) 516 517 return Event.validate(self, obj, validator_class, args)
518 519
520 - def score(self, obj, scoreing_class = None, args = None):
521 522 from runner import Team 523 from run import Run 524 525 if args is None: 526 args = {} 527 528 if type(obj) == Team and scoreing_class is None: 529 (scoreing_class, args) = self._get_team_strategy(obj, args) 530 elif type(obj) == Run and scoreing_class is None: 531 (scoreing_class, args) = self._get_run_strategy(obj, args) 532 533 return Event.score(self, obj, scoreing_class, args)
534
535 -class RoundCountEvent(Event):
536
537 - def __init__(self, course, mindiff = timedelta(0), header = {}, extra_rankings = [], 538 template_dir = 'templates', 539 print_template = 'course.tex', html_template = 'course.html', 540 cache = None, store = None):
541 """ 542 @param mindiff: Minimal time difference between two valid punches 543 @see Event for the other parameters 544 """ 545 546 super(RoundCountEvent, self).__init__(header, extra_rankings, template_dir, 547 print_template, html_template, cache, store) 548 self._course = self._store.find(Course, Course.code == course).one() 549 self._mindiff = mindiff
550
551 - def validate(self, obj, validator_class = None, args = None):
552 553 if args is None: 554 args = {} 555 556 if 'mindiff' not in args: 557 args['mindiff'] = self._mindiff 558 559 if 'course' not in args: 560 args['course'] = self._course 561 562 if validator_class is None: 563 validator_class = RoundCountScoreing 564 565 return super(RoundCountEvent, self).validate(obj, validator_class, args)
566
567 - def score(self, obj, scoreing_class = None, args = None):
568 569 if args is None: 570 args = {} 571 572 if 'mindiff' not in args: 573 args['mindiff'] = self._mindiff 574 575 if 'course' not in args: 576 args['course'] = self._course 577 578 if scoreing_class is None: 579 scoreing_class = RoundCountScoreing 580 581 return super(RoundCountEvent, self).score(obj, scoreing_class, args)
582