1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """
19 run.py - Classes for runs. This is the data generated during
20 an event. Opposed to classes in course.py which model
21 static data.
22 """
23
24 from copy import copy
25 from datetime import datetime
26 from storm.locals import *
27 from storm.exceptions import NoStoreError
28 from storm.expr import Column, Func, LeftJoin
29 import re
30
31 from base import MyStorm
32 from course import SIStation, Course, Control
33 from runner import SICard
34 from ranking import RankableItem, ValidationError, UnscoreableException
35
64
66 """
67 Wraps a punch object which has it's punchtime shifted because it's
68 part of a course wrapped by a ReorderedCourseWrapper.
69 """
70
72 self._punch = punch
73 self._shift = timeshift
74
76 if attr == 'punchtime':
77 return self._punch.punchtime + self._shift
78 else:
79 return getattr(self._punch, attr)
80
81 -class Run(MyStorm, RankableItem):
82 """A run is directly connected to a single readout of an SI-Card.
83 Competitors can have multiple runs during an event, but one
84 run can not be associated to several SI-Card readouts. You have
85 to create multiple runs in this case and an appropriate validator
86 class which checks for the correct sequence of runs (e.g. for a
87 relay)."""
88
89 __storm_table__ = 'run'
90
91 id = Int(primary=True)
92 _sicard_id = Int(name='sicard')
93 sicard = Reference(_sicard_id, 'SICard.id')
94 _course_id = Int(name='course')
95 course = Reference(_course_id, 'Course.id')
96 complete = Bool()
97 override = Int()
98 card_start_time = DateTime()
99 manual_start_time = DateTime()
100 start_time = property(lambda obj: obj.manual_start_time or obj.card_start_time)
101 card_finish_time = DateTime()
102 manual_finish_time = DateTime()
103 finish_time = property(lambda obj: obj.manual_finish_time or obj.card_finish_time)
104 check_time = DateTime()
105 clear_time = DateTime()
106 readout_time = DateTime()
107 punches = ReferenceSet(id, 'Punch._run_id')
108
109
110 - def __init__(self, card, course=None, punches = [], card_start_time = None,
111 card_finish_time = None, check_time = None, clear_time = None,
112 readout_time = None,
113 store = None):
114 """Creates a new Run object.
115
116 @param card: SICard
117 @type card: Object of class L{SICard} or card number as integer.
118 If the card number is given the store parameter is
119 mandatory.
120 @param course: Course
121 @type course: Object of class L{Course} or course code as string.
122 @param punches: Punches to add to the run.
123 @type punches: List of (stationcode, punchtime) tuples.
124 @param card_start_time: Time the SI-Card last punched a start control
125 @type card_start_time: datetime or None if unknown
126 @param card_finish_time: Time the SI-Card last punched a finish control
127 @type card_finish_time: datetime or None if unknown
128 @param check_time: Time the SI-Card was checked
129 @type check_time: datetime or None if unknown
130 @param clear_time: Time the SI-Card was cleared
131 @type clear_time: datetime or None if unknown
132 @param readout_time: Time the run was read from the SI-Card
133 @type readout_time: datetime or None if unknown
134 @param store: Storm store for the objects referenced by this run.
135 A store is needed if card or course are given as int/string
136 or if punches is non empty.
137 """
138
139 if type(card) == int:
140 cardnr = card
141 card = store.get(SICard, card)
142 if not card:
143 card = SICard(cardnr)
144
145 self.sicard = card
146
147 if store is not None:
148 self._store = store
149
150 if type(course) == unicode:
151 self.set_coursecode(course)
152 else:
153 self.course = course
154
155 self.readout_time = readout_time
156 self.card_start_time = card_start_time
157 self.card_finish_time = card_finish_time
158 self.check_time = check_time
159 self.clear_time = clear_time
160
161 self.add_punchlist(punches)
162
169
170 - def add_punch(self, punch, sequence_nr=None):
185
187 """Adds a list of (stationnumber, punchtime) tupeles to the run."""
188 errors = ''
189 for i,p in enumerate(punchlist):
190 try:
191 self.add_punch(p, i+1)
192 except RunException, msg:
193 errors = '%s%s\n' % (errors, msg)
194
195 if not errors == '':
196 raise RunException(errors)
197
199 """Sets the course for this run.
200 @param coursecode: The code of the course or None to clear the code.
201 @type coursecode: unicode or None
202 """
203 if code is None:
204 self.course = None
205 return
206
207 course = self._store.find(Course,
208 Course.code == code).one()
209 if course is None:
210 raise RunException("course '%s' not found" % code)
211
212 self.course = course
213
215 """
216 Return all valid 'normal' punches ordered by punchtime
217 @param ignored: Return punches normally ignored
218 @rtype: (Punch, Control) tuples
219 """
220
221
222
223
224 punch_cond = And(Punch.ignore != True,
225 Func('COALESCE', Punch.manual_punchtime, Punch.card_punchtime)
226 > (self.start_time or datetime.min),
227 Func('COALESCE', Punch.manual_punchtime, Punch.card_punchtime)
228 < (self.finish_time or datetime.max),
229 Not(SIStation.control == None),
230 )
231
232 if ignored is True:
233 punch_cond = Not(punch_cond)
234
235 return list(self._store.using(Join(Punch, SIStation, Punch.sistation == SIStation.id),
236 LeftJoin(Control, SIStation.control == Control.id)
237 ).find((Punch, Control),
238 punch_cond,
239 Punch.run == self.id,
240 ).order_by(Func('COALESCE',
241 Punch.manual_punchtime,
242 Punch.card_punchtime))
243 )
244
255
256 - def validate(self, validator_class=None, args=None):
257 """Validate this run. Validation of runs is normally refered to the course, but
258 passing a special validator class is supported.
259 @param validator_class: Class to use as a validation strategy. This must be a subclass
260 of bosco.ranking.Validator
261 @param args: Arguments to pass to the validation strategy.
262 @type args: dict of keyword arguments
263 @return: validation result from validation_class.validate(obj)
264 @see: bosco.ranking.Validator for more information about validation strategies
265 """
266 if validator_class is not None:
267 return validator_class(**args).validate(self)
268 elif self.course is None:
269 raise ValidationError("Can't validate a run without a course.")
270 else:
271 return self.course.validate(self)
272
273 - def score(self, scoreing_class=None, args=None):
274 """Score this run. Scoreing of runs is normally refered to the course, but
275 passing a special scoreing class is supported.
276 @param scoreing_class: Class to use a scoreing stratey. This must be a subclass
277 of bosco.ranking.AbstracScoreing.
278 @type args: dict of keyword arguments
279 @return: scoreing result from scoreing_class.score(obj)
280 @see: bosco.ranking.AbstractScoreing for more information about scoreing strategies
281 """
282 if scoreing_class is not None:
283 return scoreing_class(**args).score(self)
284 elif self.course is None:
285 raise UnscoreableException("Can't score a run without a course")
286 else:
287 return self.course.score(self)
288
291