def OnPopupAddMissingLastLap( self, event ): if not self.entry: return num = self.entry.num race = Model.race if not race or num not in race.riders: return rider = race.riders[num] times = [t for t in rider.times] if len(times) < 2: return if rider.status != rider.Finisher: Utils.MessageOK( self, _('Cannot add Last Lap unless Rider is Finisher'), _('Cannot add Last Lap') ) return undo.pushState() if rider.autocorrectLaps: if Utils.MessageOKCancel( self, _('Turn off Autocorrect first?'), _('Turn off Autocorrect') ): rider.autocorrectLaps = False waveCategory = race.getCategory( num ) if waveCategory: times[0] = waveCategory.getStartOffsetSecs() tNewLast = times[-1] + times[-1] - times[-2] race.numTimeInfo.add( num, tNewLast ) race.addTime( num, tNewLast ) race.setChanged() wx.CallAfter( self.refresh )
def commitChange( self ): num = self.num.GetValue() status = self.statusOption.GetSelection() relegatedPosition = self.relegatedPosition.GetValue() wx.CallAfter( Utils.refreshForecastHistory ) undo.pushState(); with Model.LockRace() as race: # Allow new numbers to be added if status is DNS, DNF or DQ. if race is None or (num not in race.riders and status not in [Model.Rider.DNS, Model.Rider.DNF, Model.Rider.DQ]): return rider = race.getRider(num) oldValues = (rider.status, rider.tStatus, rider.relegatedPosition) tStatus = None if status not in [Model.Rider.Finisher, Model.Rider.DNS, Model.Rider.DQ]: tStatus = Utils.StrToSeconds( self.atRaceTime.GetValue() ) rider.setStatus( status, tStatus ) rider.relegatedPosition = relegatedPosition newValues = (rider.status, rider.tStatus, rider.relegatedPosition) if oldValues != newValues: race.setChanged() wx.CallAfter( Utils.refresh )
def onOK( self, event ): race = Model.race if not race or not race.startTime: return secondsNew = self.timeMsEdit.GetSeconds() secondsOld = (race.startTime - race.startTime.replace(hour=0, minute=0, second=0)).total_seconds() dTime = secondsNew - secondsOld if dTime == 0: return if dTime > 0.0 and not Utils.MessageOKCancel( self, _('Are you Sure you want to change the Race Start to Later?\n(you can always undo).'), _('Are you sure?') ): return undo.pushState() for rider in race.riders.itervalues(): if getattr(rider, 'firstTime', None) is not None: rider.firstTime -= dTime # Adjust all the recorded times to account for the new start time. for k in xrange(len(rider.times)): rider.times[k] -= dTime race.numTimeInfo.adjustAllTimes( -dTime ) race.startTime += datetime.timedelta( seconds = dTime ) race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def StartRaceNow(): global undoResetTimer if undoResetTimer and undoResetTimer.IsRunning(): undoResetTimer.Stop() undoResetTimer = None JChip.reset() undo.clear() undo.pushState() with Model.LockRace() as race: if race is None: return if not getattr(race, 'enableJChipIntegration', False): race.resetStartClockOnFirstTag = False Model.resetCache() race.startRaceNow() OutputStreamer.writeRaceStart() VideoBuffer.ModelStartCamera() # Refresh the main window and switch to the Record pane. mainWin = Utils.getMainWin() if mainWin is not None: mainWin.showPageName( _('Record') ) mainWin.refresh() # For safety, clear the undo stack after 8 seconds. undoResetTimer = wx.CallLater( 8000, undo.clear ) if getattr(race, 'ftpUploadDuringRace', False): realTimeFtpPublish.publishEntry( True )
def onSetDNS( self, evt ): if not self.list.GetItemCount() or not Model.race: return nums = [int(self.list.GetItem(i, 0).GetText()) for i in Utils.GetListCtrlSelectedItems(self.list)] if not nums: Utils.MessageOK( self, _('No entrants selected to DNS'), _('No entrants selected to DNS') ) return lines = [] for i in range( 0, len(nums), 10 ): lines.append( ', '.join( '{}'.format(n) for n in itertools.islice( nums, i, min(i+10, len(nums)) ) ) ) message = u'{}\n\n{}'.format(_('DNS the following entrants?'), u',\n'.join(lines)) if not Utils.MessageOKCancel( self, message, _('DNS Entrants') ): return undo.pushState() DNS = Model.Rider.DNS with Model.LockRace() as race: for n in nums: if n > 0: race.getRider(n).status = DNS race.setChanged() race.resetAllCaches() wx.CallAfter( self.refresh ) wx.CallAfter( Utils.refresh ) wx.CallAfter( self.list.SetFocus )
def OnPopupNote( self, event ): self.grid.SelectRow( self.eventRow ) try: num = int(self.num.GetValue()) except: return if not Model.race or num not in Model.race: return lap = self.eventRow + 1 race = Model.race rider = race.riders[num] race.lapNote = getattr(race, 'lapNote', {}) dlg = wx.TextEntryDialog( self, u'{}: {}: {}: {}'.format(_("Bib"), num, _("Note on Lap"), lap), _("Lap Note"), Model.race.lapNote.get( (num, lap), '' ) ) ret = dlg.ShowModal() value = dlg.GetValue().strip() dlg.Destroy() if ret != wx.ID_OK: return undo.pushState() if value: race.lapNote[(self.entry.num, self.entry.lap)] = value race.setChanged() else: try: del race.lapNote[(self.entry.num, self.entry.lap)] race.setChanged() except KeyError: pass wx.CallAfter( self.refresh )
def AddLapSplits( num, lap, times, splits ): undo.pushState() with Model.LockRace() as race: rider = race.riders[num] try: tLeft = times[lap-1] tRight = times[lap] # Split the first lap time to the same ratio as the distances. category = race.getCategory( num ) if ( lap == 1 and category is not None and category.distanceType == category.DistanceByLap and category.distance and category.firstLapDistance and category.distance != category.firstLapDistance ): flr = float(category.firstLapDistance) / float(category.distance) splitTime = (tRight - tLeft) / (flr + (splits-1)) firstLapSplitTime = splitTime * flr else: splitTime = firstLapSplitTime = (tRight - tLeft) / float(splits) newTime = tLeft for i in range( 1, splits ): newTime += (firstLapSplitTime if i == 1 else splitTime) race.numTimeInfo.add( num, newTime, Model.NumTimeInfo.Split ) race.addTime( num, newTime + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) ) return True except (TypeError, KeyError, ValueError, IndexError) as e: Utils.logException( e, sys.exc_info() ) return False
def OnPopupPull(self, event): if not hasattr(self, 'rowPopup'): return entry = self.history[self.colPopup][self.rowPopup] if not entry: return if not Utils.MessageOKCancel( self, u'{}: {} {} {} - {}?'.format( _('Bib'), entry.num, _('Pull after lap'), entry.lap, Utils.formatTime(entry.t + 1, True), ), _('Pull Rider')): return try: undo.pushState() race = Model.race race.getRider(entry.num).setStatus(Model.Rider.Pulled, entry.t + 1) race.setChanged() except Exception as e: Utils.logException(e, sys.exc_info()) wx.CallAfter(self.refresh) wx.CallAfter(Utils.refreshForecastHistory)
def update( self, race = None ): undo.pushState() with Model.lock: if race is None: race = Model.getRace() if race is None: return race.name = self.raceName.GetValue().strip() race.city = self.raceCity.GetValue().strip() race.stateProv = self.raceStateProv.GetValue().strip() race.country = self.raceCountry.GetValue().strip() race.discipline = self.raceDiscipline.GetValue().strip() race.organizer = self.organizer.GetValue().strip() race.date = self.date.GetValue().Format(Properties.dateFormat) race.raceNum = self.raceNum.GetValue() race.scheduledStart = self.scheduledStart.GetValue() race.allCategoriesFinishAfterFastestRidersLastLap = self.allCategoriesFinishAfterFastestRidersLastLap.IsChecked() race.isTimeTrial = self.timeTrial.IsChecked() race.enableJChipIntegration = self.jchip.IsChecked() race.autocorrectLapsDefault = self.autocorrectLapsDefault.IsChecked() race.highPrecisionTimes = self.highPrecisionTimes.IsChecked() race.distanceUnit = self.distanceUnit.GetSelection() race.reverseDirection = self.reverseDirection.IsChecked() race.enableUSBCamera = self.enableUSBCamera.IsChecked() race.finishTop = self.finishTop.IsChecked() race.minutes = self.minutes.GetValue() race.commissaire = self.commissaire.GetValue().strip() race.memo = self.memo.GetValue().strip() race.notes = self.notes.GetValue().strip() race.setChanged() if Utils.getMainWin(): Utils.getMainWin().record.setTimeTrialInput( race.isTimeTrial )
def AddLapSplits(num, lap, times, splits): undo.pushState() with Model.LockRace() as race: rider = race.riders[num] try: tLeft = times[lap - 1] tRight = times[lap] # Split the first lap time to the same ratio as the distances. category = race.getCategory(num) if (lap == 1 and category is not None and category.distanceType == category.DistanceByLap and category.distance and category.firstLapDistance and category.distance != category.firstLapDistance): flr = float(category.firstLapDistance) / float( category.distance) splitTime = (tRight - tLeft) / (flr + (splits - 1)) firstLapSplitTime = splitTime * flr else: splitTime = firstLapSplitTime = (tRight - tLeft) / float(splits) newTime = tLeft for i in xrange(1, splits): newTime += (firstLapSplitTime if i == 1 else splitTime) race.numTimeInfo.add(num, newTime, Model.NumTimeInfo.Split) race.addTime( num, newTime + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0)) return True except (TypeError, KeyError, ValueError, IndexError) as e: Utils.logException(e, sys.exc_info()) return False
def OnPopupDNF(self, event): if not hasattr(self, 'rowPopup'): return entry = self.history[self.colPopup][self.rowPopup] if not Utils.MessageOKCancel( self, u'{}: {} {} {} - {}?'.format( _('Bib'), entry.num, _('DNF after lap'), entry.lap, Utils.formatTime(entry.t + 1, True), ), _('DNF Rider')): return try: undo.pushState() with Model.LockRace() as race: race.getRider(entry.num).setStatus(Model.Rider.DNF, entry.t + 1) race.setChanged() except: pass wx.CallAfter(self.refresh) wx.CallAfter(Utils.refreshForecastHistory)
def onOK( self, event ): stOld, ftOld, laps = getStFtLaps(self.rider) st, ft = self.startTime.GetSeconds(), self.finishTime.GetSeconds() if st is not None and ft is not None and st >= ft: Utils.MessageOK( self, _('Start Time must be before Finish Time'), _('Time Error'), wx.ICON_ERROR ) return if stOld == st and ftOld == ft: Utils.refresh() self.EndModal( wx.ID_OK ) return undo.pushState() self.rider.firstTime = st self.rider.ttPenalty = self.penaltyTime.GetSeconds() self.rider.ttNote = self.note.GetValue() if st and ft: rt = ft - st if not self.rider.times: self.rider.addTime( rt ) elif len(self.rider.times) == 2: self.rider.times[1] = rt else: self.rider.times = [t for t in self.rider.times if t < rt] self.rider.times.append( rt ) elif st: self.rider.firstTime = st Model.race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def DeleteEntry(parent, entry): if entry.lap == 0: return race = Model.race raceStartTimeOfDay = Utils.StrToSeconds( race.startTime.strftime( '%H:%M:%S.%f')) if race and race.startTime else None dlg = wx.MessageDialog( parent, u'{}: {}\n{}: {}\n{}: {}\n{}: {}\n\n{}?'.format( _('Bib'), entry.num, _('Lap'), entry.lap, _('Race Time'), Utils.formatTime(entry.t, True), _('Clock Time'), Utils.formatTime(entry.t + raceStartTimeOfDay, True) if raceStartTimeOfDay is not None else u'', _('Confirm Delete')), _('Delete Entry'), wx.OK | wx.CANCEL | wx.ICON_QUESTION) # dlg.CentreOnParent(wx.BOTH) if dlg.ShowModal() == wx.ID_OK: undo.pushState() with Model.LockRace() as race: if race: race.numTimeInfo.delete(entry.num, entry.t) race.deleteTime(entry.num, entry.t) Utils.refresh() dlg.Destroy()
def OnPopupAddMissingLastLap( self, event ): if not self.entry: return num = self.entry.num race = Model.race if not race or num not in race: return rider = race.riders[num] times = [t for t in rider.times] if len(times) < 2: return if rider.status != rider.Finisher: Utils.MessageOK( self, _('Cannot add Last Lap unless Rider is Finisher'), _('Cannot add Last Lap') ) return undo.pushState() if rider.autocorrectLaps: if Utils.MessageOKCancel( self, _('Turn off Autocorrect first?'), _('Turn off Autocorrect') ): rider.autocorrectLaps = False waveCategory = race.getCategory( num ) if waveCategory: times[0] = waveCategory.getStartOffsetSecs() tNewLast = times[-1] + times[-1] - times[-2] race.numTimeInfo.add( num, tNewLast ) race.addTime( num, tNewLast ) race.setChanged() wx.CallAfter( self.refresh )
def onSetDNS( self, evt ): if not self.list.GetItemCount() or not Model.race: return # Get all selected items. nums = [self.list.GetItemData(row) for row in xrange(self.list.GetItemCount()) if self.list.GetItem(row).m_state & wx.LIST_STATE_SELECTED] if not nums: Utils.MessageOK( self, _('No entrants selected to DNS'), _('No entrants selected to DNS') ) return lines = [] for i in xrange( 0, len(nums), 10 ): lines.append( ', '.join( '{}'.format(n) for n in itertools.islice( nums, i, min(i+10, len(nums)) ) ) ) message = _('DNS the following entrants?\n\n{}').format(',\n'.join(lines)) if not Utils.MessageOKCancel( self, message, _('DNS Entrants') ): return undo.pushState() with Model.LockRace() as race: for n in nums: rider = race.getRider( n ) rider.status = rider.DNS race.setChanged() wx.CallAfter( self.refresh ) wx.CallAfter( Utils.refresh ) wx.CallAfter( self.list.SetFocus )
def OnPopupDNF(self, event): if self.numSelect is None or not Model.race: return rider = Model.race.riders.get(self.numSelect, None) if rider is None: return t = rider.getLastKnownTime() + 1 if not Utils.MessageOKCancel( self, u'{}: {} {} {}?'.format( _('Bib'), self.numSelect, _('DNF at '), Utils.formatTime(t, True), ), _('DNF Rider')): return try: undo.pushState() with Model.LockRace() as race: rider.setStatus(Model.Rider.DNF, t) race.setChanged() except Exception as e: Utils.logException(e, sys.exc_info()) self.list.Delete(self.item) wx.CallAfter(Utils.refresh) wx.CallAfter(Utils.refreshForecastHistory)
def OnPopupLapNote( self, event ): if not self.entry or not Model.race: return Model.race.lapNote = getattr(Model.race, 'lapNote', {}) dlg = wx.TextEntryDialog( self, u"{} {}: {} {}: {}:".format( _('Bib'), self.entry.num, _('Lap'), self.entry.lap, _('Note'), ), _("Lap Note"), Model.race.lapNote.get( (self.entry.num, self.entry.lap), '' ) ) ret = dlg.ShowModal() value = dlg.GetValue().strip() dlg.Destroy() if ret != wx.ID_OK: return undo.pushState() with Model.LockRace() as race: if value: race.lapNote[(self.entry.num, self.entry.lap)] = value race.setChanged() else: try: del race.lapNote[(self.entry.num, self.entry.lap)] race.setChanged() except KeyError: pass wx.CallAfter( self.refresh )
def onOK(self, event): num1 = self.numEdit1.GetValue() num2 = self.numEdit2.GetValue() if not num1 or not num2 or num1 == num2: return t1 = self.entry.t t2 = self.entry.t + 0.0001 * random.random() undo.pushState() with Model.LockRace() as race: rider = race.getRider(self.entry.num) race.numTimeInfo.delete(self.entry.num, self.entry.t) race.numTimeInfo.add(num1, t1) race.numTimeInfo.add(num2, t2) race.deleteTime(self.entry.num, self.entry.t) race.addTime( num1, t1 + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0)) race.addTime( num2, t2 + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0)) Utils.refresh() self.EndModal(wx.ID_OK)
def OnPopupDelete( self, event ): rows = Utils.GetSelectedRows( self.grid ) try: self.visibleRow = min( rows ) except: self.visibleRow = None if len(rows) > 1: try: num = int(self.num.GetValue()) except: return if not Model.race or num not in Model.race: return rider = Model.race[num] times = [rider.times[r] for r in rows] timeStr = [] timesPerRow = 4 for i in xrange(0, len(times), timesPerRow): timeStr.append( ', '.join( _('Lap {}: {}').format(rows[j]+1, Utils.formatTime(times[i])) for j in xrange(i, min(len(times), i+timesPerRow) ) ) ) timeStr = ',\n'.join( timeStr ) message = _('Delete entries of Rider {}:\n\n{}\n\nConfirm Delete?').format(num, timeStr) if Utils.MessageOKCancel( self, message, _('Delete Times'), wx.ICON_WARNING ): undo.pushState() with Model.LockRace() as race: if race: for t in times: race.deleteTime( num, t ) wx.CallAfter( self.refresh ) else: self.grid.SelectRow( self.eventRow ) DeleteEntry( self, self.entry )
def onSetDNS( self, evt ): if not self.list.GetItemCount() or not Model.race: return nums = [int(self.list.GetItem(i, 0).GetText()) for i in Utils.GetListCtrlSelectedItems(self.list)] if not nums: Utils.MessageOK( self, _('No entrants selected to DNS'), _('No entrants selected to DNS') ) return lines = [] for i in xrange( 0, len(nums), 10 ): lines.append( ', '.join( '{}'.format(n) for n in itertools.islice( nums, i, min(i+10, len(nums)) ) ) ) message = u'{}\n\n{}'.format(_('DNS the following entrants?'), u',\n'.join(lines)) if not Utils.MessageOKCancel( self, message, _('DNS Entrants') ): return undo.pushState() DNS = Model.Rider.DNS with Model.LockRace() as race: for n in nums: if n > 0: race.getRider(n).status = DNS race.setChanged() race.resetAllCaches() wx.CallAfter( self.refresh ) wx.CallAfter( Utils.refresh ) wx.CallAfter( self.list.SetFocus )
def onOK( self, event ): num = self.numEdit.GetValue() t = self.timeMsEdit.GetSeconds() if self.timeChoice.GetSelection() == 1 and Model.race and Model.race.startTime: dtStart = Model.race.startTime dtInput = datetime.datetime(dtStart.year, dtStart.month, dtStart.day) + datetime.timedelta(seconds = t) if dtInput < dtStart: Utils.MessageOK( self, _('Cannot Enter Clock Time Before Race Start.\n\n(reminder: clock time is in 24-hour format)'), _('Time Entry Error'), iconMask = wx.ICON_ERROR ) return t = (dtInput - dtStart).total_seconds() if self.entry.num != num or self.entry.t != t: undo.pushState() with Model.LockRace() as race: rider = race.getRider( num ) if self.entry.lap != 0: race.numTimeInfo.change( self.entry.num, self.entry.t, t ) race.deleteTime( self.entry.num, self.entry.t ) race.addTime( num, t + (rider.firstTime if getattr(race, 'isTimeTrial', False) and getattr(rider, 'firstTime', None) is not None else 0.0) ) else: race.numTimeInfo.change( self.entry.num, rider.firstTime, t ) rider.firstTime = t race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def onUpdateStartWaveNumbers( self, event ): self.commit() undo.pushState() with Model.LockRace() as race: race.adjustAllCategoryWaveNumbers() wx.CallAfter( self.refresh ) wx.CallAfter( Utils.refreshForecastHistory )
def OnPopupLapNote(self, event): if not self.entry or not Model.race: return Model.race.lapNote = getattr(Model.race, 'lapNote', {}) dlg = wx.TextEntryDialog( self, u"{} {}: {} {}: {}:".format( _('Bib'), self.entry.num, _('Lap'), self.entry.lap, _('Note'), ), _("Lap Note"), Model.race.lapNote.get((self.entry.num, self.entry.lap), '')) ret = dlg.ShowModal() value = dlg.GetValue().strip() dlg.Destroy() if ret != wx.ID_OK: return undo.pushState() with Model.LockRace() as race: if value: race.lapNote[(self.entry.num, self.entry.lap)] = value race.setChanged() else: try: del race.lapNote[(self.entry.num, self.entry.lap)] race.setChanged() except KeyError: pass wx.CallAfter(self.refresh)
def onUpdateStartWaveNumbers(self, event): self.commit() undo.pushState() with Model.LockRace() as race: race.adjustAllCategoryWaveNumbers() self.state.reset() wx.CallAfter(self.refresh) wx.CallAfter(Utils.refreshForecastHistory)
def DoImportTTStartTimes( race, excelLink ): if not excelLink: return ['Missing excelLink'] info = excelLink.read() startTimes = {} errors = [] for num, data in info.iteritems(): try: startTime = data['StartTime'] except KeyError: errors.append( u'{} {}: {}'.format(_('Bib'), num, _('missing start time')) ) continue # Try to make sense of the StartTime (Stopwatch time, not clock time). if isinstance(startTime, float): t = startTime * 24.0*60.0*60.0 # Excel decimal days. elif isinstance(startTime, (str, unicode)): # Otherwise, string of format hh:mm:ss.ddd or mm:ss.ddd. fields = startTime.split( ':' ) try: hh, mm, ss = [float(f.strip()) for f in fields[:3]] except: try: hh = 0.0 mm, ss = [float(f.strip()) for f in fields[:2]] except: errors.append( u'{} {}: {}: "{}"'.format(_('Bib'), num, _('cannot read time format'), startTime) ) continue t = hh * 60.0*60.0 + mm * 60.0 + ss else: errors.append( u'{} {}: {}'.format(_('Bib'), num, _('cannot read start time (neither Excel time nor String)') ) ) continue startTimes[num] = t if startTimes: undo.pushState() for num, t in startTimes.iteritems(): rider = race.getRider( num ) # Compute the time change difference. try: dTime = getattr(rider, 'firstTime', t) - t except TypeError: dTime = 0.0 rider.firstTime = t # Adjust the lap times to account for the new start time. for k in xrange(len(rider.times)): if k != 0: rider.times[k] += dTime race.setChanged() return errors
def OnPopupMassPull(self, event): if not self.entry: return if not Utils.MessageOKCancel( self, u'{} {}: {} {}, {}.\n{} {}.\n\n{}?'.format( _('Bib'), self.entry.num, _('Pull Rider after lap'), self.entry.lap, Utils.formatTime(self.entry.t + 1, True), _('And, Pull all riders ranked lower after lap'), self.entry.lap, _('Continue'), ), _('Mass Pull')): return try: undo.pushState() with Model.LockRace() as race: if not race: return category = race.getCategory(self.entry.num) if not category: Utils.MessageOK( self, _('Cannot apply Mass Pull to rider with unknown category.' ), _('Unknown Category'), wx.ICON_ERROR, ) return results = GetResults(category) toPull = [] Finisher, Pulled = Model.Rider.Finisher, Model.Rider.Pulled found = False for rr in results: if not found: found = (rr.num == self.entry.num) if found and rr.status == Finisher and rr.raceTimes: toPull.append(rr) tSearch = self.entry.t for rr in toPull: i = bisect_right(rr.raceTimes, tSearch) try: race.getRider(rr.num).setStatus( Pulled, rr.raceTimes[i] + 1) except IndexError: pass race.setChanged() wx.CallAfter(self.refresh) except Exception as e: pass
def onOK( self, event ): fname = self.chipDataFile.GetValue() try: with open(fname) as f: pass except IOError: Utils.MessageOK( self, u'{}:\n\n"{}"'.format(_('Could not open data file for import'), fname), title = _('Cannot Open File'), iconMask = wx.ICON_ERROR) return clearExistingData = (self.importPolicy.GetSelection() == 0) timeAdjustment = self.timeAdjustment.GetSeconds() if self.behindAhead.GetSelection() == 1: timeAdjustment *= -1 # Get the start time. if not clearExistingData: if not Model.race or not Model.race.startTime: Utils.MessageOK( self, u'\n\n'.join( [_('Cannot Merge into Unstarted Race.'), _('Clear All Existing Data is allowed.')] ), title = _('Import Merge Failed'), iconMask = wx.ICON_ERROR ) return startTime = Model.race.startTime.time() else: if self.manualStartTime.IsChecked(): startTime = datetime.time(*[int(x) for x in self.raceStartTime.GetValue().split(':')]) else: startTime = None undo.pushState() errors = DoChipImport( fname, self.parseTagTime, startTime, clearExistingData, datetime.timedelta(seconds = timeAdjustment) ) if errors: # Copy the tags to the clipboard. clipboard = wx.Clipboard.Get() if not clipboard.IsOpened(): clipboard.Open() clipboard.SetData( wx.TextDataObject('\n'.join(errors)) ) clipboard.Close() if len(errors) > 10: errors = errors[:10] errors.append( '...' ) tagStr = '\n'.join(errors) Utils.MessageOK( self, u'{}:\n\n{}\n\n{}.'.format(_('Import File contains errors'), tagStr, _('All errors have been copied to the clipboard')), _('Import Warning'), iconMask = wx.ICON_WARNING ) else: Utils.MessageOK( self, _('Import Successful'), _('Import Successful') ) wx.CallAfter( Utils.refresh ) self.EndModal( wx.ID_OK )
def doCommit( self ): undo.pushState() with Model.LockRace() as race: if race is None: return for prop, PropClass, name in self.propClassName: getattr(self, prop).commit() race.setChanged() if Utils.getMainWin(): Utils.getMainWin().record.setTimeTrialInput( race.isTimeTrial )
def onCopyRider( self, event ): if not Model.race: return try: num = int(self.num.GetValue()) except: return with Model.LockRace() as race: if not num in race: return dlg = wx.TextEntryDialog( self, _("All time entries for {} will be copied to the new bib number.\n\nNew Bib Number:").format(num), _('Copy Rider Times'), '{}'.format(self.num.GetValue()) ) ret = dlg.ShowModal() newNum = dlg.GetValue() dlg.Destroy() if ret != wx.ID_OK: return try: newNum = int(re.sub( '[^0-9]', '', newNum)) except ValueError: return with Model.LockRace() as race: inRace = (newNum in race) if inRace: if num != newNum: Utils.MessageOK( self, _("New Bib {} already exists.\nIf you really want to copy times to this number, delete it first.").format(newNum), _('New Bib Number Already Exists'), iconMask = wx.ICON_ERROR ) else: Utils.MessageOK( self, _("Cannot copy to the same number ({}).").format(newNum), _('Cannot Copy to Same Number'), iconMask = wx.ICON_ERROR ) return if Utils.MessageOKCancel( self, _("Entries from {} will be copied to new Bib {}.\n\nAll entries for {} will be slightly earlier then entries for {}.\nContinue?").format( num, newNum, newNum, num), _("Confirm Copy Rider Times") ): undo.pushState() with Model.LockRace() as race: race.copyRiderTimes( num, newNum ) rNew = race.getRider( newNum ) numTimeInfo = race.numTimeInfo for t in rNew.times: numTimeInfo.add( newNum, t ) self.setRider( newNum ) self.onNumChange() wx.CallAfter( Utils.refreshForecastHistory ) wx.CallAfter( Utils.refresh )
def DoStatusChange( parent, num, message, title, newStatus ): if num is None or not Utils.MessageOKCancel(parent, message.format(num), title): return False undo.pushState() with Model.LockRace() as race: if not race: return False rider = race.getRider( num ) rider.setStatus( newStatus ) race.setChanged() Utils.refresh() return True
def onAutocorrectLaps( self, event ): num = self.num.GetValue() if not Model.race or num not in Model.race: self.autocorrectLaps.SetValue( True ) return undo.pushState() with Model.LockRace() as race: rider = race.riders[num] rider.autocorrectLaps = self.autocorrectLaps.GetValue() race.setChanged() self.refresh() wx.CallAfter( Utils.refreshForecastHistory ) wx.CallAfter( Utils.refresh )
def onOK(self, event): race = Model.race if not race: return if race.isTimeTrial and race.hasRiderTimes(): Utils.MessageOKCancel( self, _('Cannot change Time Trial Start Time') + u'\n\n' + _('There are already recorded results.'), _('Cannot change Start Time')) self.EndModal(wx.ID_OK) tOld = race.startTime startTimeNew = tOld.replace(hour=0, minute=0, second=0, microsecond=0) + datetime.timedelta( seconds=self.timeMsEdit.GetSeconds()) dTime = (startTimeNew - race.startTime).total_seconds() if not dTime: return if dTime > 0.0 and not Utils.MessageOKCancel( self, _('Are you Sure you want to change the Race Start to Later?') + u'\n' + _('(you can always undo).'), _('Are you sure?')): return undo.pushState() # Adjust all rider times to account for the new start time. if not race.isTimeTrial: for rider in race.riders.values(): try: rider.firstTime = max(0.0, rider.firstTime - dTime) except TypeError: pass rider.times[:] = [max(0.0, v - dTime) for v in rider.times] race.numTimeInfo.adjustAllTimes(-dTime) # Also fix any unread tags. if race.unmatchedTags: for times in race.unmatchedTags.values(): times[:] = [max(0.0, v - dTime) for v in times] race.startTime = startTimeNew race.setChanged() Utils.refresh() self.EndModal(wx.ID_OK)
def OnPopupSwapAfter( self, event ): if not hasattr(self, 'rowPopup'): return c, r, h = self.colPopup, self.rowPopup, self.history success = False undo.pushState() with Model.LockRace() as race: for rNext in xrange( r + 1, len(h[c]) ): if not h[c][rNext].interp and (self.category is None or race.inCategory(h[c][rNext].num, self.category)): EditEntry.SwapEntry( h[c][r], h[c][rNext] ) success = True break if success and Utils.isMainWin(): Utils.getMainWin().refresh()
def AddLapSplits( num, lap, times, splits ): undo.pushState() with Model.LockRace() as race: try: tLeft = times[lap-1] tRight = times[lap] splitTime = (tRight - tLeft) / float(splits) for i in xrange( 1, splits ): newTime = tLeft + splitTime * i race.numTimeInfo.add( num, newTime, Model.NumTimeInfo.Split ) race.addTime( num, newTime ) return True except (TypeError, KeyError, ValueError, IndexError): return False
def OnGanttPopupDNF( self, event ): if not Utils.MessageOKCancel( self, _('DNF Rider {} at {} after lap {}?').format(self.entry.num, Utils.formatTime(self.entry.t+1, True), self.entry.lap), _('DNF Rider') ): return try: undo.pushState() with Model.LockRace() as race: race.getRider(self.entry.num).setStatus( Model.Rider.DNF, self.entry.t + 1 ) race.setChanged() except: pass wx.CallAfter( self.refresh ) wx.CallAfter( Utils.refreshForecastHistory )
def DoStatusChange(parent, num, message, title, newStatus, lapTime=None): if num is None: return False race = Model.race externalData = [] try: excelLink = race.excelLink externalInfo = excelLink.read() for f in ['LastName', 'FirstName', 'Team']: try: externalData.append(unicode(externalInfo[num][f])) if f == 'Team': externalData[-1] = u'({})'.format(externalData[-1]) except KeyError: pass if len(externalData ) == 3: # Format the team name slightly differently. externalData = u'{}: {}'.format(unicode(num), u', '.join( externalData[:-1])) + u' ' + externalData[-1] else: externalData = u'{}: {}'.format( unicode(num), u', '.join(externalData)) if externalData else None except: externalData = None d = StatusChangeDialog(parent, message=message.format(num), title=title, externalData=externalData, t=lapTime) ret = d.ShowModal() lapTime = lapTime if d.getSetEntryTime() else None d.Destroy() if ret != wx.ID_OK: return False undo.pushState() with Model.LockRace() as race: if not race: return False if lapTime: race.addTime(num, lapTime) rider = race.getRider(num) rider.setStatus(newStatus) race.setChanged() Utils.refresh() Utils.refreshForecastHistory() return True
def onCategoryChoice( self, event ): if self.num.GetValue() is None: return num = int(self.num.GetValue()) catName = self.category.GetStringSelection() undo.pushState() with Model.LockRace() as race: if not race: return for c in race.getCategories( startWaveOnly = False, excludeCustom = True, excludeCombined = True ): if c.fullname == catName: race.addCategoryException( c, num ) break wx.CallAfter( self.refresh )
def onOK( self, event ): num = self.numEdit.GetValue() t = self.timeMsEdit.GetSeconds() if self.timeChoice.GetSelection() == 1 and Model.race and Model.race.startTime: dtStart = Model.race.startTime dtInput = datetime.datetime(dtStart.year, dtStart.month, dtStart.day) + datetime.timedelta(seconds = t) if dtInput < dtStart: Utils.MessageOK( self, u'\n\n'.join( [_('Cannot Enter Clock Time Before Race Start.'), _('(reminder: clock time is in 24-hour format)')] ), _('Time Entry Error'), iconMask = wx.ICON_ERROR ) return t = (dtInput - dtStart).total_seconds() race = Model.race offset = race.getStartOffset( num ) if t <= offset: Utils.MessageOK( self, u'{}: {}\n\n{}\n{}'.format( _('Cannot enter a time that is before the Category Start Offset'), Utils.formatTime(offset, highPrecision=True), _('All times earlier than the Start Offset are ignored.'), _('Please enter a time after the Start Offset.') ), _('Time Entry Error'), iconMask = wx.ICON_ERROR ) return race.lapNote = getattr( race, 'lapNote', {} ) if self.noteEdit.GetValue() != race.lapNote.get( (self.entry.num, self.entry.lap), u'' ) or self.entry.num != num or self.entry.t != t: undo.pushState() note = self.noteEdit.GetValue().strip() if not note: race.lapNote.pop( (self.entry.num, self.entry.lap), None ) else: race.lapNote[(self.entry.num, self.entry.lap)] = note if self.entry.num != num or self.entry.t != t: rider = race.getRider( num ) if self.entry.lap != 0: race.numTimeInfo.change( self.entry.num, self.entry.t, t ) race.deleteTime( self.entry.num, self.entry.t ) race.addTime( num, t + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) ) else: race.numTimeInfo.change( self.entry.num, rider.firstTime, t ) rider.firstTime = t race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def OnPopupSwapAfter(self, event): if not hasattr(self, 'rowPopup'): return c, r, h = self.colPopup, self.rowPopup, self.history success = False undo.pushState() with Model.LockRace() as race: for rNext in xrange(r + 1, len(h[c])): if not h[c][rNext].interp and ( self.category is None or race.inCategory(h[c][rNext].num, self.category)): EditEntry.SwapEntry(h[c][r], h[c][rNext]) success = True break if success and Utils.isMainWin(): Utils.getMainWin().refresh()
def onSwapNumber( self, event ): if not Model.race: return try: num = int(self.num.GetValue()) except: return with Model.LockRace() as race: if not num in race: return dlg = wx.TextEntryDialog( self, _("Number to swap with:"), _('Swap Numbers'), u'{}'.format(self.num.GetValue()) ) ret = dlg.ShowModal() newNum = dlg.GetValue() dlg.Destroy() if ret != wx.ID_OK: return try: newNum = int(re.sub( '[^0-9]', '', newNum)) except ValueError: return with Model.LockRace() as race: inRace = (newNum in race) if not inRace: Utils.MessageOK( self, u'{}\n{}'.format( _("Cannot swap with specified rider."), _("This rider is not in race."), ), _('Cannot Swap Rider Numbers'), iconMask = wx.ICON_ERROR ) return if Utils.MessageOKCancel( self, u"{}\n\n {} <==> {}.".format(_('Confirm Swap numbers'), num, newNum), _("Swap Rider Number") ): undo.pushState() with Model.LockRace() as race: race.swapRiders( num, newNum ) race.numTimeInfo.swapRiders( num, newNum ) self.setRider( newNum ) self.refresh() wx.CallAfter( Utils.refreshForecastHistory ) wx.CallAfter( Utils.refresh )
def OnPopupAutocorrect( self, event ): if not self.entry: return if not Utils.MessageOKCancel( self, u'{} {}: {}?'.format(_('Bib'), self.entry.num, _('Turn off Autocorrect')), _('Turn off Autocorrect') ): return try: undo.pushState() with Model.LockRace() as race: if not race: return race.getRider(self.entry.num).setAutoCorrect( False ) race.setChanged() except: pass wx.CallAfter( self.refresh )
def DeleteEntry( parent, entry ): if entry.lap == 0: return dlg = wx.MessageDialog(parent, _('Num: {} Lap: {} RaceTime: {}\n\nConfirm Delete?').format( entry.num, entry.lap, Utils.formatTime(entry.t, True)), _('Delete Entry'), wx.OK | wx.CANCEL | wx.ICON_QUESTION ) # dlg.CentreOnParent(wx.BOTH) if dlg.ShowModal() == wx.ID_OK: undo.pushState() with Model.LockRace() as race: if race: race.numTimeInfo.delete( entry.num, entry.t ) race.deleteTime( entry.num, entry.t ) Utils.refresh() dlg.Destroy()
def commit( self ): undo.pushState() with Model.LockRace() as race: self.grid.SaveEditControlValue() self.grid.DisableCellEditControl() # Make sure the current edit is committed. if race is None: return numStrTuples = [] for r in range(self.grid.GetNumberRows()): values = { name:self.grid.GetCellValue(r, c) for name, c in six.iteritems(self.iCol) if name not in self.computedFields } values['catType'] = self.CategoryTypeChoices.index(values['catType']) values['distanceType'] = self.DistanceTypeChoices.index(values['distanceType']) numStrTuples.append( values ) race.setCategories( numStrTuples ) race.adjustAllCategoryWaveNumbers() wx.CallAfter( Utils.refreshForecastHistory )
def onOK( self, event ): num = self.numEdit.GetValue() t = self.getNewTime() if self.entry.num != num or self.entry.t != t: undo.pushState() with Model.LockRace() as race: rider = race.getRider( num ) if self.entry.lap != 0: race.numTimeInfo.change( self.entry.num, self.entry.t, t ) race.deleteTime( self.entry.num, self.entry.t ) race.addTime( num, t + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) ) else: race.numTimeInfo.change( self.entry.num, rider.firstTime, t ) rider.firstTime = t race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def commit( self ): undo.pushState() with Model.LockRace() as race: self.grid.SaveEditControlValue() self.grid.DisableCellEditControl() # Make sure the current edit is committed. if race is None: return numStrTuples = [] for r in xrange(self.grid.GetNumberRows()): values = { name:self.grid.GetCellValue(r, c) for name, c in self.iCol.iteritems() if name not in self.computedFields } values['catType'] = self.CategoryTypeChoices.index(values['catType']) values['distanceType'] = self.DistanceTypeChoices.index(values['distanceType']) numStrTuples.append( values ) race.setCategories( numStrTuples ) race.adjustAllCategoryWaveNumbers() wx.CallAfter( Utils.refreshForecastHistory )
def onOK( self, event ): num = self.numEdit.GetValue() t = self.getNewTime() if self.entry.num != num or self.entry.t != t: undo.pushState() with Model.LockRace() as race: rider = race.getRider( num ) if self.entry.lap != 0: race.numTimeInfo.change( self.entry.num, self.entry.t, t ) race.deleteTime( self.entry.num, self.entry.t ) race.addTime( num, t + (rider.firstTime if getattr(race, 'isTimeTrial', False) and getattr(rider, 'firstTime', None) is not None else 0.0) ) else: race.numTimeInfo.change( self.entry.num, rider.firstTime, t ) rider.firstTime = t race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def OnPopupDNF( self, event ): if not self.entry: return if not Utils.MessageOKCancel( self, u'{} {}: {} {}, {}?'.format( _('Bib'), self.entry.num, _('DNF Rider after lap'), self.entry.lap, Utils.formatTime(self.entry.t+1, True)), _('DNF Rider') ): return try: undo.pushState() with Model.LockRace() as race: if not race: return race.getRider(self.entry.num).setStatus( Model.Rider.DNF, self.entry.t + 1 ) race.setChanged() except: pass wx.CallAfter( self.refresh )
def onOK( self, event ): num = self.numEdit.GetValue() if not num or num == self.entry.num: return tAdjust = 0.0001 + random.random() / 10000.0 # Add some randomness so that all inserted times will be unique. if self.beforeAfterBox.GetSelection() == 0: tAdjust = -tAdjust tInsert = self.entry.t + tAdjust undo.pushState() with Model.LockRace() as race: rider = race.getRider( num ) race.numTimeInfo.add( num, tInsert ) race.addTime( num, tInsert + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) ) Utils.refresh() self.EndModal( wx.ID_OK )
def swapEntries(self, num, numAdjacent): if not num or not numAdjacent: return with Model.LockRace() as race: if (not race or num not in race or numAdjacent not in race.riders): return e1 = race.getRider(num).interpolate() e2 = race.getRider(numAdjacent).interpolate() category = FixCategories(self.categoryChoice, getattr(race, 'ganttCategory', 0)) riderResults = dict((r.num, r) for r in GetResults(category)) try: laps = riderResults[num].laps undo.pushState() with Model.LockRace() as race: EditEntry.SwapEntry(e1[laps], e2[laps]) wx.CallAfter(self.refresh) except KeyError: pass
def onOK( self, event ): race = Model.race if not race or not race.startTime: return tNow = datetime.datetime.now() startTimeNew = tNow.replace(hour=0, minute=0, second=0) + datetime.timedelta(seconds=self.timeMsEdit.GetSeconds()) dTime = (startTimeNew - race.startTime).total_seconds() if dTime == 0: return if dTime > 0.0 and not Utils.MessageOKCancel( self, _('Are you Sure you want to change the Race Start to Later?') + u'\n' + _('(you can always undo).'), _('Are you sure?') ): return undo.pushState() if not race.isTimeTrial: for rider in race.riders.itervalues(): if getattr(rider, 'firstTime', None) is not None: rider.firstTime -= dTime # Adjust all the recorded times to account for the new start time. for k in xrange(len(rider.times)): rider.times[k] -= dTime race.numTimeInfo.adjustAllTimes( -dTime ) # Fix any unread tags. if race.unmatchedTags: for times in race.unmatchedTags.itervalues(): for k in xrange(len(times)): times[k] -= dTime race.startTime = startTimeNew race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def onAddExceptions( self, event ): with Model.LockRace() as race: if not race or not race.getAllCategories(): return r = self.grid.GetGridCursorRow() if r is None or r < 0: Utils.MessageOK( self, _('You must select a Category first'), _('Select a Category') ) return with Model.LockRace() as race: categories = race.getAllCategories() category = categories[r] dlg = wx.TextEntryDialog( self, u'{}: {}'.format( category.name, _('''Add Bib Exceptions (comma separated). This will add the given list of Bibs to this category, and remove them from other categories.'''), ), _('Add Bib Exceptions') ) good = (dlg.ShowModal() == wx.ID_OK) if good: response = dlg.GetValue() dlg.Destroy() if not good: return undo.pushState() response = re.sub( '[^0-9,]', '', response.replace(' ', ',') ) with Model.LockRace() as race: for numException in response.split(','): race.addCategoryException( category, numException ) self.state.reset() self.refresh()
def StartRaceNow(page=_('Record')): global undoResetTimer if undoResetTimer and undoResetTimer.IsRunning(): undoResetTimer.Stop() undoResetTimer = None ChipReader.chipReaderCur.reset( Model.race.chipReaderType if Model.race else None) undo.clear() undo.pushState() with Model.LockRace() as race: if race is None: return if not race.enableJChipIntegration: race.resetStartClockOnFirstTag = False Model.resetCache() race.startRaceNow() isTimeTrial = race.isTimeTrial OutputStreamer.writeRaceStart() # Refresh the main window and switch to the specified pane. mainWin = Utils.getMainWin() if mainWin is not None: mainWin.showPageName(page) mainWin.updateLapCounter() mainWin.refresh() if isTimeTrial: mainWin.menuPublishHtmlTTStart() # For safety, clear the undo stack after 8 seconds. undoResetTimer = wx.CallLater(8000, undo.clear) if race.ftpUploadDuringRace: realTimeFtpPublish.publishEntry(True)
def swapEntries(self, num, numAdjacent): if not num or not numAdjacent: return with Model.LockRace() as race: if (not race or num not in race.riders or numAdjacent not in race): return e1 = race.getRider(num).interpolate() e2 = race.getRider(numAdjacent).interpolate() category = FixCategories(self.categoryChoice, getattr(race, 'resultsCategory', 0)) riderResults = dict((r.num, r) for r in GetResults(category)) try: rr1, rr2 = riderResults[num], riderResults[numAdjacent] laps = rr1.laps undo.pushState() ee1 = next(e for e in e1 if e.t == rr1.raceTimes[laps]) ee2 = next(e for e in e2 if e.t == rr2.raceTimes[laps]) with Model.LockRace() as race: SwapEntry(ee1, ee2) wx.CallAfter(self.refresh) except (KeyError, StopIteration): pass
def onOK( self, event ): num = self.numEdit.GetValue() t = self.timeMsEdit.GetSeconds() if self.timeChoice.GetSelection() == 1 and Model.race and Model.race.startTime: dtStart = Model.race.startTime dtInput = datetime.datetime(dtStart.year, dtStart.month, dtStart.day) + datetime.timedelta(seconds = t) if dtInput < dtStart: Utils.MessageOK( self, u'\n\n'.join( [_('Cannot Enter Clock Time Before Race Start.'), _('(reminder: clock time is in 24-hour format)')] ), _('Time Entry Error'), iconMask = wx.ICON_ERROR ) return t = (dtInput - dtStart).total_seconds() offset = Model.race.getStartOffset( num ) if t <= offset: Utils.MessageOK( self, u'{}: {}\n\n{}\n{}'.format( _('Cannot enter a time that is before the Category Start Offset'), Utils.formatTime(offset, highPrecision=True), _('All times earlier than the Start Offset are ignored.'), _('Please enter a time after the Start Offset.') ), _('Time Entry Error'), iconMask = wx.ICON_ERROR ) return if self.entry.num != num or self.entry.t != t: undo.pushState() with Model.LockRace() as race: rider = race.getRider( num ) if self.entry.lap != 0: race.numTimeInfo.change( self.entry.num, self.entry.t, t ) race.deleteTime( self.entry.num, self.entry.t ) race.addTime( num, t + ((rider.firstTime or 0.0) if race.isTimeTrial else 0.0) ) else: race.numTimeInfo.change( self.entry.num, rider.firstTime, t ) rider.firstTime = t race.setChanged() Utils.refresh() self.EndModal( wx.ID_OK )
def doSet(self, action): selections = self.categoryList.GetSelections() if not selections: Utils.MessageOK( self, _("No Categories Selected.\n\nSelect some Categories, or Cancel" ), _("No Categories Selected"), wx.ICON_EXCLAMATION) return False if 0 in selections: doAll = True selectedCats = set() else: doAll = False selectedCats = set(self.categories[s - 1] for s in selections) undo.pushState() with Model.LockRace() as race: for num, rider in race.riders.iteritems(): if doAll or race.getCategory(num) in selectedCats: rider.autocorrectLaps = action race.setChanged() Utils.refresh() return True
def DoImportTTStartTimes(race, excelLink): startTimes = {} errors = [] if not excelLink: errors.append(_('Missing excelLink')) return errors, startTimes info = excelLink.read() for num, data in info.iteritems(): try: startTime = data['StartTime'] except KeyError: errors.append(u'{} {}: {}'.format(_('Bib'), num, _('missing start time'))) continue # Try to make sense of the StartTime (Stopwatch time, not clock time). if isinstance(startTime, float): t = startTime * 24.0 * 60.0 * 60.0 # Excel decimal days. elif isinstance(startTime, (str, unicode)): # Otherwise, string of format hh:mm:ss.ddd or mm:ss.ddd. fields = startTime.split(':') try: hh, mm, ss = [float(f.strip()) for f in fields[:3]] except: try: hh = 0.0 mm, ss = [float(f.strip()) for f in fields[:2]] except: errors.append(u'{} {}: {}: "{}"'.format( _('Bib'), num, _('cannot read time format'), startTime)) continue t = hh * 60.0 * 60.0 + mm * 60.0 + ss else: errors.append(u'{} {}: {}'.format( _('Bib'), num, _('cannot read start time (neither Excel time nor String)'))) continue startTimes[num] = t changeCount = 0 if startTimes: undo.pushState() for num, startTime in startTimes.iteritems(): rider = race.getRider(num) # Compute the time change difference. try: firstTime = getattr(rider, 'firstTime', startTime) except TypeError: continue if rider.times: # Convert the race times to time of day by adding the existing first time. riderTimeOfDay = [firstTime + rt for rt in rider.times] # Compute new lap times by subtracting the new start time. rider.times = [rtod - startTime for rtod in riderTimeOfDay] if rider.firstTime != startTime: rider.firstTime = startTime changeCount += 1 race.setChanged() return errors, startTimes, changeCount