1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 """
19 course.py - Classes for orienteering courses. Everything here is
20 static during an event. Dynamic data (e.g. runs) is
21 handled by the classes in run.py
22 """
23
24 from storm.locals import *
25
26 from datetime import timedelta
27
28 from ranking import Rankable, ValidationError, UnscoreableException
29 from base import MyStorm
30
50
52 """A control point. Control points are part of one or several courses.
53 The possible orders of the control points in a course is defined
54 with the ControlSequence relation."""
55
56 __storm_table__ = 'control'
57
58 id = Int(primary=True)
59 code = Unicode()
60 override = Bool()
61 sistations = ReferenceSet(id, 'SIStation._control_id')
62
63 - def __init__(self, code, sistation = None, store = None):
64 """
65 @param code: Code for this control.
66 @type code: unicode
67 @param sistation: SI-Station for this control. If this is an integer,
68 a corresponding SIStation object is created if necessary.
69 If sistation is None a SIStation with id int(code) ist added
70 if possible.
71 @type sistation: SIStation object or int
72 @param store: Storm store for the sistation. A store is needed if
73 sistation is given as int. The newly created object is
74 automatically added to this store.
75 """
76
77 self.code = code
78 if store is not None:
79 self._store = store
80
81 if sistation is None:
82 try:
83 sistation = int(code)
84 except ValueError:
85 pass
86
87 if sistation is not None:
88 self.add_sistation(sistation)
89
105
107 """Connects controls and courses. The sequence_number defines the
108 correct sequence of the controls. It's possible the have several
109 controls with the same sequence_number on the same course. The
110 interpretation of the sequence number is up to the validate method
111 of the course. The sequence number may be None."""
112
113 __storm_table__ = 'controlsequence'
114
115 id = Int(primary=True)
116 length = Int()
117 climb = Int()
118 _course_id = Int(name='course')
119 course = Reference(_course_id, 'Course.id')
120 _control_id = Int(name='control')
121 control = Reference(_control_id, 'Control.id')
122 sequence_number = Int()
123
124 - def __init__(self, control, sequence_number = None,
125 length = None, climb = None):
130
131
133 """Common base class for Course and CombinedCourse."""
134
135
136 -class Course(MyStorm, BaseCourse):
137 """Base class for all kinds of courses. Special kinds of courses should
138 be derived from this class. Derived class must at least override the
139 append or validate methods. This class implements an unordered set
140 of controls as a course without any validation.
141
142 The distance and altitude attributes are in meters and may be None."""
143
144 __storm_table__ = 'course'
145
146 id = Int(primary=True)
147 code = Unicode()
148 length = Int()
149 climb = Int()
150 members = ReferenceSet(id, 'Run._course_id')
151 controls = ReferenceSet(id, ControlSequence._course_id,
152 ControlSequence._control_id,
153 Control.id,
154 order_by=ControlSequence.sequence_number)
155 sequence = ReferenceSet(id, 'ControlSequence._course_id',
156 order_by=ControlSequence.sequence_number)
157
158 - def __init__(self, code, length = None, climb = None, validator=None, scoreing=None):
159 """
160 @param code: Descriptive code for this course. Usually 3 characters long. For
161 'normal' events this corresponds to the category name.
162 @type code: unicode
163 @param length: Length of the course in meters
164 @type length: int
165 @param climb: Altitude differences in meters
166 @type climb: int
167 """
168
169 self.code = code
170 self.length = length
171 self.climb = climb
172 self._validator = validator
173 self._scoreing = scoreing
174
181
187
188 - def append(self, control, length = None, climb = None):
204
205 - def insert(self, control, index, length = None, climb = None):
206 """Insert an additional control into the course at an arbitrary
207 postition."""
208 raise Exception('Not yet implemented')
209
210 - def extend(self, control_list):
211 """Extend the course with the controls from control_list."""
212 for c in control_list:
213 self.append(c)
214
216 """
217 @return: 'Leistungskilometer': length/1000.0+climb/100.0
218 """
219 return self.length/1000.0 + self.climb/100.0
220
222 """Returns the expected time for this course.
223 @param speed: expected speed in minutes per kilometer
224 """
225 try:
226 return timedelta(minutes=self.lkm()*speed)
227 except TypeError:
228 return None
229
232
234 """
235 @return list of controls in this course with sistations that are not overriden.
236 """
237 return [ c for c in
238 self.controls
239 if (c.sistations.count() > 0 and
240 c.override is not True) ]
241
243 """Validate a run according to this course.
244 @param run: Run to be validated.
245 @return: Validation status
246 @see: bosco.ranking.Validator
247 """
248 if self._validator is not None:
249 return self._validator.validate(run)
250 else:
251 raise ValidationError("Can't validate a run without a validation strategy.")
252
254 """Score a run according to this course.
255 @param run: Run to be scored.
256 @return: Scoreing result
257 @see: bosco.ranking.AbstractScoreing
258 """
259 if self._scoreing is not None:
260 return self._scoreing.score(run)
261 else:
262 raise UnscoreableException("Can't score a run without a scoreing strategy.")
263
265 return unicode(self).encode('utf-8')
266
269
270
272 """
273 This class combines several courses to generate a joint ranking of all runns of
274 all the combined courses. This is primarily usefull for rankings of relay legs with
275 different variants. This class is not derived from Course and this is not a Storm object
276 and not stored in the database.
277 """
278
279 - def __init__(self, course_list, code, store=None):
280 """
281 @param course_list: List of courses to combine
282 @type course_list: list of either instances of Course or unicode course codes
283 @param code: Code of this course. This is only for display purposes.
284 @type code: Unicode
285 @param store: Storm store which contains the courses referenced by course
286 codes in the course list. May be None if the course list only
287 contains Course objects.
288 """
289 self._code = code
290 self.course_list = []
291 for c in course_list:
292 if type(c) == Course:
293 self.course_list.append(c)
294 else:
295 if store is None:
296 raise CombinedCourseException("Can't add course '%s' without a store." % c)
297
298 course = store.find(Course, Course.code == c).one()
299 if course is None:
300 raise CombinedCourseException("Can't find course with code '%s'." % c)
301 self.course_list.append(course)
302
303 self.length = self.course_list[0].length
304 self.climb = self.course_list[0].climb
305 self._controlcount = self.course_list[0].controls.count()
306
308 """Get all runs of all the courses in self.course_list."""
309
310 runs = []
311 for c in self.course_list:
312 runs.extend([r for r in c.members])
313 return runs
314 members = property(_get_members)
315
317 return self._controlcount
318
320 return unicode(self).encode('utf-8')
321
324
327