1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
37
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
77 self._header['rankings'] = [ r[0] for r in self.list_rankings() ]
78
79 @staticmethod
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
95 """Removes the cache. No caching occurs anymore."""
96 self._cache = None
97 self._strategies = {}
98
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
111
112
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
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
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
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
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
225 """
226 @return: list of possible rankings
227 """
228
229 l = [ (c.code, self.ranking(c)) for c in self.list_courses()]
230
231
232 l.extend([ (c.name, self.ranking(c)) for c in self.list_categories() ])
233
234
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
241 """
242 @return: list of all courses in the event
243 """
244 return list(self._store.find(Course))
245
247 """
248 @return: list of all categories in the event
249 """
250 return list(self._store.find(Category))
251
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):
296
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
317 self._legs = legs
318
319 Event.__init__(self, header, extra_rankings, template_dir, print_template,
320 html_template, cache, store)
321
322
323
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
334
335 course = self._store.find(Course, Course.code == c).one()
336 if course is None:
337
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
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
359 if not 'legs' in args:
360
361 args['legs'] = len(self._legs[cat])
362
363 if type(args['legs']) == int:
364 args['legs'] = self._legs[cat][:args['legs']]
365 args['event'] = self
366
367
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
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
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
412 if not 'legs' in args:
413
414 args['legs'] = self._legs[cat][:len(self._legs[cat])]
415
416 if type(args['legs']) == int:
417 args['legs'] = self._legs[cat][:args['legs']]
418
419 args['event'] = self
420
421
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
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
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
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
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
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
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