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

Source Code for Module bosco.run

  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  """ 
 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   
36 -class Punch(Storm):
37 __storm_table__ = 'punch' 38 39 id = Int(primary=True) 40 _run_id = Int(name='run') 41 run = Reference(_run_id, 'Run.id') 42 _sistation_id = Int(name='sistation') 43 sistation = Reference(_sistation_id, 'SIStation.id') 44 card_punchtime = DateTime(name = 'card_punchtime') 45 manual_punchtime = DateTime() 46 ignore = Bool() 47 sequence = Int() 48
49 - def __init__(self, sistation, card_punchtime=None, manual_punchtime = None, 50 sequence = None):
51 """Creates a new punch object. This object needs to be added to a 52 run. It can not "live" on its own.""" 53 self.sistation = sistation 54 self.card_punchtime = card_punchtime 55 self.manual_punchtime = manual_punchtime 56 self.sequence = sequence
57
58 - def _get_punchtime(self):
59 if self.manual_punchtime is not None: 60 return self.manual_punchtime 61 else: 62 return self.card_punchtime
63 punchtime = property(_get_punchtime)
64
65 -class ShiftedPunch(object):
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
71 - def __init__(self, punch, timeshift):
72 self._punch = punch 73 self._shift = timeshift
74
75 - def __getattr__(self, attr):
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
163 - def __str__(self):
164 runner = self.sicard.runner 165 if runner is not None: 166 return '%s %s' % (runner.given_name, runner.surname) 167 else: 168 return 'SI-Card: %s' % self.sicard.id
169
170 - def add_punch(self, punch, sequence_nr=None):
171 """Adds a (stationnumber, punchtime) tuple to the run.""" 172 173 (number, punchtime) = punch 174 175 # Only add punch if it does not yet exist on this run 176 if self._store.find(Punch, And(Punch.run == self.id, 177 Punch.sistation == number, 178 Punch.card_punchtime == punchtime)).count() == 0: 179 180 station = self._store.get(SIStation, number) 181 if station is None: 182 station = SIStation(number) 183 184 self.punches.add(Punch(station, punchtime, sequence=sequence_nr))
185
186 - def add_punchlist(self, punchlist):
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
198 - def set_coursecode(self, code):
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
214 - def punchlist(self, ignored = False):
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 # Do a direct search in the store for Punch, Control tuples. This is much faster 222 # than first fetching punches from self.punches and then getting their controls via 223 # punch.sistation.control 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
245 - def check_sequence(self):
246 """Check if punchtimes match punch sequence numbers.""" 247 punchsequence = list(self.punches.find(Not(Punch.card_punchtime == None), 248 Not(Punch.ignore == True), 249 Punch.sistation == SIStation.id, 250 SIStation.control == Control.id, 251 Not(Control.override == True)).order_by('COALESCE(manual_punchtime, card_punchtime)').values(Column('sequence'))) 252 sorted = copy(punchsequence) 253 sorted.sort() 254 return punchsequence == sorted
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
289 -class RunException(Exception):
290 pass
291