From f0303742c06ca192d0d0caa57429d26ac1602039 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 25 Nov 2022 22:18:42 +0000 Subject: [PATCH 01/53] 'Machine' field in CrossMgr --- ReadSignOnSheet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ReadSignOnSheet.py b/ReadSignOnSheet.py index 5b18d01f0..087277338 100644 --- a/ReadSignOnSheet.py +++ b/ReadSignOnSheet.py @@ -34,6 +34,7 @@ Fields = [ _('Bib#'), _('LastName'), _('FirstName'), + _('Machine'), _('Team'), _('City'), _('State'), _('Prov'), _('StateProv'), _('Nat.'), _('Category'), _('EventCategory'), _('Age'), _('Gender'), From 0cc914e7dc81d4d35a0b8e76c6fc6e7d69d83449 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 25 Nov 2022 22:20:05 +0000 Subject: [PATCH 02/53] 'Machine' field in SeriesMgr --- SeriesMgr/AliasesMachine.py | 108 ++++++++++++++++++++++++++++++ SeriesMgr/FieldMap.py | 4 ++ SeriesMgr/GetModelInfo.py | 39 ++++++++--- SeriesMgr/MainWin.py | 2 + SeriesMgr/ReadRaceResultsSheet.py | 2 +- SeriesMgr/Results.py | 42 +++++++----- SeriesMgr/SeriesModel.py | 40 ++++++++++- 7 files changed, 207 insertions(+), 30 deletions(-) create mode 100644 SeriesMgr/AliasesMachine.py diff --git a/SeriesMgr/AliasesMachine.py b/SeriesMgr/AliasesMachine.py new file mode 100644 index 000000000..99cd37db6 --- /dev/null +++ b/SeriesMgr/AliasesMachine.py @@ -0,0 +1,108 @@ +import wx +import os +import sys +import SeriesModel +import Utils +from AliasGrid import AliasGrid + +class AliasesMachine(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + text = ( + 'Machine Aliases match alternate machine names to the same machine.\n' + 'This can be more convenient than editing race results when the same machine appears with a different spelling.\n' + '\n' + 'To create a machine Alias, first press the "Add Reference Machine" button.\n' + 'The first column is the Machine that will appear in Results.\n' + 'The second column are the Machine Aliases, separated by ";". These are the alternate machine names.\n' + 'SeriesMgr will match all aliased Machines to the Reference Machine in the Results.\n' + '\n' + 'For example, Reference Machine="ICE Trike", AliasesMachine="Trice; ICE". Results for the alternate Machines will appear as "ICE Trike".\n' + '\n' + 'You can Copy-and-Paste Machines from the Results without retyping them. Right-click and Copy the name in the Results page,' + 'then Paste the Machine into a Reference Machine or Alias field.\n' + 'Aliased Machines will not be applied until you press the "Refresh" button on the Results screen (or reload).\n' + 'This allows you to configure many Machines without having to wait for the Results update after each change.\n' + ) + + self.explain = wx.StaticText( self, label=text ) + self.explain.SetFont( wx.Font((0,15), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) ) + + self.addButton = wx.Button( self, label='Add Reference Machine' ) + self.addButton.Bind( wx.EVT_BUTTON, self.onAddButton ) + + headerNames = ('Machine','Aliases separated by ";"') + self.itemCur = None + self.grid = AliasGrid( self ) + self.grid.CreateGrid( 0, len(headerNames) ) + self.grid.SetRowLabelSize( 64 ) + for col in range(self.grid.GetNumberCols()): + self.grid.SetColLabelValue( col, headerNames[col] ) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.explain, 0, flag=wx.ALL, border=4 ) + sizer.Add(self.addButton, 0, flag=wx.ALL, border = 4) + sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.ALL, border = 4) + self.SetSizer(sizer) + + def onAddButton( self, event ): + defaultText = '' + + # Initialize the team from the clipboard. + if wx.TheClipboard.Open(): + do = wx.TextDataObject() + if wx.TheClipboard.GetData(do): + defaultText = do.GetText() + wx.TheClipboard.Close() + + self.grid.AppendRows( 1 ) + self.grid.SetCellValue( self.grid.GetNumberRows()-1, 0, defaultText ) + self.grid.AutoSize() + self.GetSizer().Layout() + self.grid.MakeCellVisible( self.grid.GetNumberRows()-1, 0 ) + + def refresh( self ): + model = SeriesModel.model + + Utils.AdjustGridSize( self.grid, rowsRequired=len(model.referenceMachines) ) + for row, (reference, aliases) in enumerate(model.referenceMachines): + self.grid.SetCellValue( row, 0, reference ) + self.grid.SetCellValue( row, 1, '; '.join(aliases) ) + + self.grid.AutoSize() + self.GetSizer().Layout() + + def commit( self ): + references = [] + + self.grid.SaveEditControlValue() + for row in range(self.grid.GetNumberRows()): + reference = self.grid.GetCellValue( row, 0 ).strip() + if reference: + aliases = [a.strip() for a in self.grid.GetCellValue(row, 1).split(';')] + references.append( (reference, sorted( a for a in aliases if a )) ) + + references.sort() + + model = SeriesModel.model + model.setReferenceMachines( references ) + +#---------------------------------------------------------------------------- + +class AliasesMachineFrame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="AliasesMachine Test", size=(800,600) ) + self.panel = AliasesMachine(self) + self.Show() + +if __name__ == "__main__": + app = wx.App(False) + model = SeriesModel.model + model.setReferenceMachines( [ + ['ICE Trike', ['ICE', 'I.C.E.', 'Trice']], + ['Windcheetah', ['Windy', 'Speedy']], + ] ) + frame = AliasesMachineFrame() + frame.panel.refresh() + app.MainLoop() diff --git a/SeriesMgr/FieldMap.py b/SeriesMgr/FieldMap.py index e85fc03aa..489c2d58f 100644 --- a/SeriesMgr/FieldMap.py +++ b/SeriesMgr/FieldMap.py @@ -112,6 +112,10 @@ def get_name_from_alias( self, alias ): ('Team','Team Name','TeamName','Rider Team','Club','Club Name','ClubName','Rider Club','Rider Club/Team',), "Team", ), + ('machine', + ('Machine','Machine Name','MachineName','Rider Machine','Cycle','Cycle Name','CycleName','Rider Cycle','Bike','Bike Name','BikeName','Rider Bike','Vehicle','Vehicle Name','VehicleName','Rider Vehicle','HPV','Car','Velo','Velomobile','Wheelchair','Fiets','Rad'), + "Machine", + ), ('discipline', ('Discipline',), "Discipline", diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index fd4279564..828ce17f3 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -77,7 +77,7 @@ def safe_upper( f ): class RaceResult: rankDNF = 999999 - def __init__( self, firstName, lastName, license, team, categoryName, raceName, raceDate, raceFileName, bib, rank, raceOrganizer, + def __init__( self, firstName, lastName, license, machine, team, categoryName, raceName, raceDate, raceFileName, bib, rank, raceOrganizer, raceURL=None, raceInSeries=None, tFinish=None, tProjected=None, primePoints=0, timeBonus=0, laps=1, pointsInput=None ): self.firstName = str(firstName or '') self.lastName = str(lastName or '') @@ -87,6 +87,8 @@ def __init__( self, firstName, lastName, license, team, categoryName, raceName, self.license = int(self.license) self.license = str(self.license) + self.machine = str(machine or '') + self.team = str(team or '') self.categoryName = str(categoryName or '') @@ -110,6 +112,7 @@ def __init__( self, firstName, lastName, license, team, categoryName, raceName, self.upgradeFactor = 1 self.upgradeResult = False + @property def teamIsValid( self ): @@ -134,7 +137,7 @@ def full_name( self ): return ', '.join( [name for name in [self.lastName.upper(), self.firstName] if name] ) def __repr__( self ): - return ', '.join( '{}'.format(p) for p in [self.full_name, self.license, self.categoryName, self.raceName, self.raceDate] if p ) + return ', '.join( '{}'.format(p) for p in [self.full_name, self.license, self.machine, self.categoryName, self.raceName, self.raceDate] if p ) def ExtractRaceResults( r ): if os.path.splitext(r.fileName)[1] == '.cmn': @@ -153,6 +156,7 @@ def toInt( n ): def ExtractRaceResultsExcel( raceInSeries ): getReferenceName = SeriesModel.model.getReferenceName getReferenceLicense = SeriesModel.model.getReferenceLicense + getReferenceMachine = SeriesModel.model.getReferenceMachine getReferenceTeam = SeriesModel.model.getReferenceTeam excel = GetExcelReader( raceInSeries.getFileName() ) @@ -183,6 +187,7 @@ def ExtractRaceResultsExcel( raceInSeries ): 'firstName': str(f('first_name','')).strip(), 'lastName' : str(f('last_name','')).strip(), 'license': str(f('license_code','')).strip(), + 'machine': str(f('machine','')).strip(), 'team': str(f('team','')).strip(), 'categoryName': f('category_code',None), 'laps': f('laps',1), @@ -234,6 +239,7 @@ def ExtractRaceResultsExcel( raceInSeries ): info['lastName'], info['firstName'] = getReferenceName(info['lastName'], info['firstName']) info['license'] = getReferenceLicense(info['license']) + info['machine'] = getReferenceMachine(info['machine']) info['team'] = getReferenceTeam(info['team']) # If there is a bib it must be numeric. @@ -264,7 +270,6 @@ def ExtractRaceResultsExcel( raceInSeries ): # Check if this is a team-only sheet. raceInSeries.pureTeam = ('team' in fm and not any(n in fm for n in ('name', 'last_name', 'first_name', 'license'))) raceInSeries.resultsType = SeriesModel.Race.TeamResultsOnly if raceInSeries.pureTeam else SeriesModel.Race.IndividualAndTeamResults - return True, 'success', raceResults def FixExcelSheetLocal( fileName, race ): @@ -302,6 +307,7 @@ def ExtractRaceResultsCrossMgr( raceInSeries ): getReferenceName = SeriesModel.model.getReferenceName getReferenceLicense = SeriesModel.model.getReferenceLicense + getReferenceMachine = SeriesModel.model.getReferenceMachine getReferenceTeam = SeriesModel.model.getReferenceTeam Finisher = Model.Rider.Finisher @@ -340,6 +346,7 @@ def ExtractRaceResultsCrossMgr( raceInSeries ): info['categoryName'] = category.fullname info['lastName'], info['firstName'] = getReferenceName(info['lastName'], info['firstName']) info['license'] = getReferenceLicense(info['license']) + info['machine'] = getReferenceMachine(rr.Machine) info['team'] = getReferenceTeam(info['team']) info['laps'] = rr.laps @@ -468,6 +475,8 @@ def GetCategoryResults( categoryName, raceResults, pointsForRank, useMostEventsC riderTeam = defaultdict( lambda : '' ) riderUpgrades = defaultdict( lambda : [False] * len(races) ) riderNameLicense = {} + riderMachine = defaultdict( lambda : '' ) + def asInt( v ): return int(v) if int(v) == v else v @@ -507,6 +516,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): continue rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) + if rr.machine and rr.machine != '0': + riderMachine[rider] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team riderResults[rider][raceSequence[rr.raceInSeries]] = ( @@ -546,8 +557,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderGap = { r : formatTimeGap(gap) if gap else '' for r, gap in riderGap.items() } # List of: - # lastName, firstName, license, team, tTotalFinish, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderTeam[rider], formatTime(riderTFinish[rider],True), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, machine, team, tTotalFinish, [list of (points, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], formatTime(riderTFinish[rider],True), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) elif scoreByPercent: @@ -574,6 +585,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): continue rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) + if rr.machine and rr.machine != '0': + riderMachine[rider] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team percent = min( 100.0, (tFastest / tFinish) * 100.0 if tFinish > 0.0 else 0.0 ) * (rr.upgradeFactor if rr.upgradeResult else 1) @@ -612,8 +625,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderGap = { r : percentFormat.format(gap) if gap else '' for r, gap in riderGap.items() } # List of: - # lastName, firstName, license, team, totalPercent, [list of (percent, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderTeam[rider], percentFormat.format(riderPercentTotal[rider]), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, machine, team, totalPercent, [list of (percent, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], percentFormat.format(riderPercentTotal[rider]), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) elif scoreByTrueSkill: @@ -635,6 +648,8 @@ def formatRating( rating ): for rr in raceResults: rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) + if rr.machine and rr.machine != '0': + riderMachine[rider] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team if rr.rank != RaceResult.rankDNF: @@ -686,8 +701,8 @@ def formatRating( rating ): results.reverse() # List of: - # lastName, firstName, license, team, points, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, machine, team, points, [list of (points, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) else: # Score by points. @@ -696,6 +711,8 @@ def formatRating( rating ): for rr in raceResults: rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) + if rr.machine and rr.machine != '0': + riderMachine[rider] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team primePoints = rr.primePoints if considerPrimePointsOrTimeBonus else 0 @@ -764,8 +781,8 @@ def formatRating( rating ): results.reverse() # List of: - # lastName, firstName, license, team, points, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, machine, team, points, [list of (points, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) #------------------------------------------------------------------------------------------------ diff --git a/SeriesMgr/MainWin.py b/SeriesMgr/MainWin.py index 835c06c75..faf340b20 100644 --- a/SeriesMgr/MainWin.py +++ b/SeriesMgr/MainWin.py @@ -40,6 +40,7 @@ from CategorySequence import CategorySequence from Aliases import Aliases from AliasesLicense import AliasesLicense +from AliasesMachine import AliasesMachine from AliasesTeam import AliasesTeam from Options import Options from Errors import Errors @@ -265,6 +266,7 @@ def addPage( page, name ): [ 'upgrades', Upgrades, 'Upgrades' ], [ 'aliases', Aliases, 'Name Aliases' ], [ 'licenseAliases', AliasesLicense, 'License Aliases' ], + [ 'machineAliases', AliasesMachine, 'Machine Aliases' ], [ 'teamAliases', AliasesTeam, 'Team Aliases' ], [ 'options', Options, 'Options' ], [ 'errors', Errors, 'Errors' ], diff --git a/SeriesMgr/ReadRaceResultsSheet.py b/SeriesMgr/ReadRaceResultsSheet.py index 4cdcb73ab..a6378522c 100644 --- a/SeriesMgr/ReadRaceResultsSheet.py +++ b/SeriesMgr/ReadRaceResultsSheet.py @@ -12,7 +12,7 @@ from Excel import GetExcelReader #----------------------------------------------------------------------------------------------------- -Fields = ['Bib#', 'Pos', 'Time', 'FirstName', 'LastName', 'Category', 'License', 'Team'] +Fields = ['Bib#', 'Pos', 'Time', 'FirstName', 'LastName', 'Category', 'License', 'Machine', 'Team'] class FileNamePage(wx.adv.WizardPageSimple): def __init__(self, parent): diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 3ed168656..7d64e10db 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -26,7 +26,7 @@ reNoDigits = re.compile( '[^0-9]' ) -HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Team'] +HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Machine', 'Team'] def getHeaderNames(): return HeaderNamesTemplate + ['Total Time' if SeriesModel.model.scoreByTime else 'Points', 'Gap'] @@ -445,7 +445,7 @@ def write( s ): pointsForRank, useMostEventsCompleted=model.useMostEventsCompleted, numPlacesTieBreaker=model.numPlacesTieBreaker ) - results = [rr for rr in results if toFloat(rr[3]) > 0.0] + results = [rr for rr in results if toFloat(rr[4]) > 0.0] headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] @@ -489,7 +489,7 @@ def write( s ): with tag(html, 'span', {'class': 'smallFont'}): write( 'Top {}'.format(len(r[3].pointStructure)) ) with tag(html, 'tbody'): - for pos, (name, license, team, points, gap, racePoints) in enumerate(results): + for pos, (name, license, machine, team, points, gap, racePoints) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(pos+1) ) @@ -501,6 +501,8 @@ def write( s ): write( '{}'.format(license or '') ) else: write( '{}'.format(license or '') ) + with tag(html, 'td'): + write( '{}'.format(machine or '') ) with tag(html, 'td'): write( '{}'.format(team or '') ) with tag(html, 'td', {'class':'rightAlign'}): @@ -794,6 +796,7 @@ def doCellClick( self, event ): self.popupInfo = [ ('{}...'.format(_('Copy Name to Clipboard')), wx.NewId(), self.onCopyName), ('{}...'.format(_('Copy License to Clipboard')), wx.NewId(), self.onCopyLicense), + ('{}...'.format(_('Copy Machine to Clipboard')), wx.NewId(), self.onCopyMachine), ('{}...'.format(_('Copy Team to Clipboard')), wx.NewId(), self.onCopyTeam), ] for p in self.popupInfo: @@ -830,14 +833,17 @@ def onCopyName( self, event ): def onCopyLicense( self, event ): self.copyCellToClipboard( self.rowCur, 2 ) - def onCopyTeam( self, event ): + def onCopyMachine( self, event ): self.copyCellToClipboard( self.rowCur, 3 ) + def onCopyTeam( self, event ): + self.copyCellToClipboard( self.rowCur, 4 ) + def setColNames( self, headerNames ): for col, headerName in enumerate(headerNames): self.grid.SetColLabelValue( col, headerName ) attr = gridlib.GridCellAttr() - if headerName in ('Name', 'Team', 'License'): + if headerName in ('Name', 'Team', 'License', 'Machine'): attr.SetAlignment( wx.ALIGN_LEFT, wx.ALIGN_TOP ) elif headerName in ('Pos', 'Points', 'Gap'): attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP ) @@ -896,23 +902,24 @@ def refresh( self ): numPlacesTieBreaker=model.numPlacesTieBreaker, ) - results = [rr for rr in results if toFloat(rr[3]) > 0.0] + results = [rr for rr in results if toFloat(rr[4]) > 0.0] headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) - for row, (name, license, team, points, gap, racePoints) in enumerate(results): + for row, (name, license, machine, team, points, gap, racePoints) in enumerate(results): self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) self.grid.SetCellValue( row, 1, '{}'.format(name or '') ) self.grid.SetCellBackgroundColour( row, 1, wx.Colour(255,255,0) if name in potentialDuplicates else wx.Colour(255,255,255) ) self.grid.SetCellValue( row, 2, '{}'.format(license or '') ) - self.grid.SetCellValue( row, 3, '{}'.format(team or '') ) - self.grid.SetCellValue( row, 4, '{}'.format(points) ) - self.grid.SetCellValue( row, 5, '{}'.format(gap) ) + self.grid.SetCellValue( row, 3, '{}'.format(machine or '') ) + self.grid.SetCellValue( row, 4, '{}'.format(team or '') ) + self.grid.SetCellValue( row, 5, '{}'.format(points) ) + self.grid.SetCellValue( row, 6, '{}'.format(gap) ) for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): - self.grid.SetCellValue( row, 6 + q, + self.grid.SetCellValue( row, 7 + q, '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints @@ -1005,7 +1012,7 @@ def onPublishToExcel( self, event ): useMostEventsCompleted=model.useMostEventsCompleted, numPlacesTieBreaker=model.numPlacesTieBreaker, ) - results = [rr for rr in results if toFloat(rr[3]) > 0.0] + results = [rr for rr in results if toFloat(rr[4]) > 0.0] headerNames = HeaderNames + [r[1] for r in races] @@ -1037,15 +1044,16 @@ def onPublishToExcel( self, event ): wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) rowCur += 1 - for pos, (name, license, team, points, gap, racePoints) in enumerate(results): + for pos, (name, license, machine, team, points, gap, racePoints) in enumerate(results): wsFit.write( rowCur, 0, pos+1, numberStyle ) wsFit.write( rowCur, 1, name, textStyle ) wsFit.write( rowCur, 2, license, textStyle ) - wsFit.write( rowCur, 3, team, textStyle ) - wsFit.write( rowCur, 4, points, numberStyle ) - wsFit.write( rowCur, 5, gap, numberStyle ) + wsFit.write( rowCur, 3, machine, textStyle ) + wsFit.write( rowCur, 4, team, textStyle ) + wsFit.write( rowCur, 5, points, numberStyle ) + wsFit.write( rowCur, 6, gap, numberStyle ) for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): - wsFit.write( rowCur, 6 + q, + wsFit.write( rowCur, 7 + q, '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 8ed971c6b..6ed3428ff 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -237,9 +237,11 @@ class SeriesModel: references = [] referenceLicenses = [] + referenceMachines = [] referenceTeams = [] aliasLookup = {} aliasLicenseLookup = {} + aliasMachineLoopup = {} aliasTeamLookup = {} ftpHost = '' @@ -393,6 +395,39 @@ def setReferenceLicenses( self, referenceLicenses ): #if updated: # memoize.clear() + def setReferenceMachines( self, referenceMachines ): + dNew = dict( referenceMachines ) + dExisting = dict( self.referenceMachines ) + + changed = (len(dNew) != len(dExisting)) + updated = False + + for name, aliases in dNew.items(): + if name not in dExisting: + changed = True + if aliases: + updated = True + elif aliases != dExisting[name]: + changed = True + updated = True + + for name, aliases in dExisting.items(): + if name not in dNew: + changed = True + if aliases: + updated = True + + if changed: + self.changed = changed + self.referenceMachines = referenceMachines + self.aliasMachineLookup = {} + for machine, aliases in self.referenceMachines: + for alias in aliases: + key = nameToAliasKey( alias ) + self.aliasMachineLookup[key] = machine + + #if updated: + # memoize.clear() def setReferenceTeams( self, referenceTeams ): dNew = dict( referenceTeams ) @@ -437,9 +472,12 @@ def getReferenceLicense( self, license ): key = Utils.removeDiacritic(license).upper() return self.aliasLicenseLookup.get( key, key ) + def getReferenceMachine( self, machine ): + return self.aliasMachineLookup.get( nameToAliasKey(machine), machine ) + def getReferenceTeam( self, team ): return self.aliasTeamLookup.get( nameToAliasKey(team), team ) - + def fixCategories( self ): categorySequence = getattr( self, 'categorySequence', None ) if self.categorySequence or not isinstance(self.categories, dict): From 2a5665796374c9227017ee3a9543036efdbbb208 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 25 Nov 2022 22:33:45 +0000 Subject: [PATCH 03/53] Be consistent... --- SeriesMgr/GetModelInfo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index 828ce17f3..c83d74100 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -337,7 +337,7 @@ def ExtractRaceResultsCrossMgr( raceInSeries ): 'raceURL': raceURL, 'raceInSeries': raceInSeries, } - for fTo, fFrom in [('firstName', 'FirstName'), ('lastName', 'LastName'), ('license', 'License'), ('team', 'Team')]: + for fTo, fFrom in [('firstName', 'FirstName'), ('lastName', 'LastName'), ('license', 'License'), ('machine', 'Machine'), ('team', 'Team')]: info[fTo] = getattr(rr, fFrom, '') if not info['firstName'] and not info['lastName']: @@ -346,7 +346,7 @@ def ExtractRaceResultsCrossMgr( raceInSeries ): info['categoryName'] = category.fullname info['lastName'], info['firstName'] = getReferenceName(info['lastName'], info['firstName']) info['license'] = getReferenceLicense(info['license']) - info['machine'] = getReferenceMachine(rr.Machine) + info['machine'] = getReferenceMachine(info['machine']) info['team'] = getReferenceTeam(info['team']) info['laps'] = rr.laps From c5ff7acda5a6d0febdec466554b792faf6dc2f9a Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 19:30:19 +0000 Subject: [PATCH 04/53] Handle rider using multiple bikes Pretty-print multiple bikes as a list in order of frequency --- SeriesMgr/GetModelInfo.py | 49 ++++++++++++++++++++++++++++----------- SeriesMgr/Results.py | 16 ++++++------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index c83d74100..af2a2ee8d 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -4,7 +4,7 @@ import datetime import operator import itertools -from collections import defaultdict, namedtuple +from collections import defaultdict, namedtuple, Counter import trueskill @@ -475,7 +475,8 @@ def GetCategoryResults( categoryName, raceResults, pointsForRank, useMostEventsC riderTeam = defaultdict( lambda : '' ) riderUpgrades = defaultdict( lambda : [False] * len(races) ) riderNameLicense = {} - riderMachine = defaultdict( lambda : '' ) + #riderMachine = defaultdict( lambda : '' ) #fixme should be a list of machines per rider: + riderMachines = defaultdict( lambda : [''] * len(races) ) def asInt( v ): @@ -492,6 +493,19 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): v = riderResults[rider][i] riderResults[rider][i] = tuple([upgradeFormat.format(v[0] if v[0] else '')] + list(v[1:])) + def TidyMachinesList (riderMachines): + for rider, machines in riderMachines.items(): + #remove Nones + while('None' in machines): + machines.remove('None') + #sort by frequency + counts = Counter(machines) + machines = sorted(machines, key=counts.get, reverse=True) + #remove duplicates + machines = list(dict.fromkeys(machines)) + #overwrite + riderMachines[rider] = machines + riderResults = defaultdict( lambda : [(0,0,0,0)] * len(races) ) riderFinishes = defaultdict( lambda : [None] * len(races) ) if scoreByTime: @@ -517,7 +531,7 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) if rr.machine and rr.machine != '0': - riderMachine[rider] = rr.machine + riderMachines[rider][raceSequence[rr.raceInSeries]] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team riderResults[rider][raceSequence[rr.raceInSeries]] = ( @@ -528,6 +542,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderUpgrades[rider][raceSequence[rr.raceInSeries]] = rr.upgradeResult riderPlaceCount[rider][(raceGrade[rr.raceFileName],rr.rank)] += 1 riderEventsCompleted[rider] += 1 + + #fixme call ListMachines() to generate a dict of strings for each rider # Adjust for the best times. if bestResultsToConsider > 0: @@ -556,9 +572,11 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderGap = { r : riderTFinish[r] - leaderTFinish if riderEventsCompleted[r] == leaderEventsCompleted else None for r in riderOrder } riderGap = { r : formatTimeGap(gap) if gap else '' for r, gap in riderGap.items() } + # List of: - # lastName, firstName, license, machine, team, tTotalFinish, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], formatTime(riderTFinish[rider],True), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, [list of machines], team, tTotalFinish, [list of (points, position) for each race in series] + #fixme need to return machine list string (line 540) here! + categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], formatTime(riderTFinish[rider],True), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) elif scoreByPercent: @@ -586,7 +604,7 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) if rr.machine and rr.machine != '0': - riderMachine[rider] = rr.machine + riderMachines[rider][raceSequence[rr.raceInSeries]] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team percent = min( 100.0, (tFastest / tFinish) * 100.0 if tFinish > 0.0 else 0.0 ) * (rr.upgradeFactor if rr.upgradeResult else 1) @@ -625,8 +643,8 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderGap = { r : percentFormat.format(gap) if gap else '' for r, gap in riderGap.items() } # List of: - # lastName, firstName, license, machine, team, totalPercent, [list of (percent, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], percentFormat.format(riderPercentTotal[rider]), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, [list of machines], team, totalPercent, [list of (percent, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], percentFormat.format(riderPercentTotal[rider]), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) elif scoreByTrueSkill: @@ -649,7 +667,7 @@ def formatRating( rating ): rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) if rr.machine and rr.machine != '0': - riderMachine[rider] = rr.machine + riderMachines[rider][raceSequence[rr.raceInSeries]] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team if rr.rank != RaceResult.rankDNF: @@ -701,8 +719,8 @@ def formatRating( rating ): results.reverse() # List of: - # lastName, firstName, license, machine, team, points, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, [list of machines], team, points, [list of (points, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) else: # Score by points. @@ -712,7 +730,7 @@ def formatRating( rating ): rider = rr.key() riderNameLicense[rider] = (rr.full_name, rr.license) if rr.machine and rr.machine != '0': - riderMachine[rider] = rr.machine + riderMachines[rider][raceSequence[rr.raceInSeries]] = rr.machine if rr.team and rr.team != '0': riderTeam[rider] = rr.team primePoints = rr.primePoints if considerPrimePointsOrTimeBonus else 0 @@ -780,9 +798,12 @@ def formatRating( rating ): for results in riderResults.values(): results.reverse() + # Tidy up the machines list for display + TidyMachinesList( riderMachines ) + # List of: - # lastName, firstName, license, machine, team, points, [list of (points, position) for each race in series] - categoryResult = [list(riderNameLicense[rider]) + [riderMachine[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] + # lastName, firstName, license, [list of machines], team, points, [list of (points, position) for each race in series] + categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], riderPoints[rider], riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) #------------------------------------------------------------------------------------------------ diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 7d64e10db..585be5ffb 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -460,7 +460,7 @@ def write( s ): with tag(html, 'tr'): for iHeader, col in enumerate(HeaderNames): colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } - if col in ('License', 'Gap'): + if col in ('License', 'Machine', 'Gap'): colAttr['class'] = 'noprint' with tag(html, 'th', colAttr): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): @@ -489,7 +489,7 @@ def write( s ): with tag(html, 'span', {'class': 'smallFont'}): write( 'Top {}'.format(len(r[3].pointStructure)) ) with tag(html, 'tbody'): - for pos, (name, license, machine, team, points, gap, racePoints) in enumerate(results): + for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(pos+1) ) @@ -501,8 +501,8 @@ def write( s ): write( '{}'.format(license or '') ) else: write( '{}'.format(license or '') ) - with tag(html, 'td'): - write( '{}'.format(machine or '') ) + with tag(html, 'td', {'class':'noprint'}): + write( '{}'.format(',
'.join(machines) or '') ) with tag(html, 'td'): write( '{}'.format(team or '') ) with tag(html, 'td', {'class':'rightAlign'}): @@ -909,12 +909,12 @@ def refresh( self ): Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) - for row, (name, license, machine, team, points, gap, racePoints) in enumerate(results): + for row, (name, license, machines, team, points, gap, racePoints) in enumerate(results): self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) self.grid.SetCellValue( row, 1, '{}'.format(name or '') ) self.grid.SetCellBackgroundColour( row, 1, wx.Colour(255,255,0) if name in potentialDuplicates else wx.Colour(255,255,255) ) self.grid.SetCellValue( row, 2, '{}'.format(license or '') ) - self.grid.SetCellValue( row, 3, '{}'.format(machine or '') ) + self.grid.SetCellValue( row, 3, '{}'.format(',\n'.join(machines) or '') ) self.grid.SetCellValue( row, 4, '{}'.format(team or '') ) self.grid.SetCellValue( row, 5, '{}'.format(points) ) self.grid.SetCellValue( row, 6, '{}'.format(gap) ) @@ -1044,11 +1044,11 @@ def onPublishToExcel( self, event ): wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) rowCur += 1 - for pos, (name, license, machine, team, points, gap, racePoints) in enumerate(results): + for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): wsFit.write( rowCur, 0, pos+1, numberStyle ) wsFit.write( rowCur, 1, name, textStyle ) wsFit.write( rowCur, 2, license, textStyle ) - wsFit.write( rowCur, 3, machine, textStyle ) + wsFit.write( rowCur, 3, ', '.join(machines), textStyle ) wsFit.write( rowCur, 4, team, textStyle ) wsFit.write( rowCur, 5, points, numberStyle ) wsFit.write( rowCur, 6, gap, numberStyle ) From e6b719c8ec818839edd4e4bf839c0f96a63e4bbe Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 19:56:28 +0000 Subject: [PATCH 05/53] Tidy up comments --- SeriesMgr/GetModelInfo.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index af2a2ee8d..95da44a3a 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -475,7 +475,6 @@ def GetCategoryResults( categoryName, raceResults, pointsForRank, useMostEventsC riderTeam = defaultdict( lambda : '' ) riderUpgrades = defaultdict( lambda : [False] * len(races) ) riderNameLicense = {} - #riderMachine = defaultdict( lambda : '' ) #fixme should be a list of machines per rider: riderMachines = defaultdict( lambda : [''] * len(races) ) @@ -494,10 +493,13 @@ def FixUpgradeFormat( riderUpgrades, riderResults ): riderResults[rider][i] = tuple([upgradeFormat.format(v[0] if v[0] else '')] + list(v[1:])) def TidyMachinesList (riderMachines): + # Format list of unique machines used by each rider in frequency order for rider, machines in riderMachines.items(): - #remove Nones + #remove Nones and empty strings while('None' in machines): machines.remove('None') + while('' in machines): + machines.remove('') #sort by frequency counts = Counter(machines) machines = sorted(machines, key=counts.get, reverse=True) @@ -543,8 +545,6 @@ def TidyMachinesList (riderMachines): riderPlaceCount[rider][(raceGrade[rr.raceFileName],rr.rank)] += 1 riderEventsCompleted[rider] += 1 - #fixme call ListMachines() to generate a dict of strings for each rider - # Adjust for the best times. if bestResultsToConsider > 0: for rider, finishes in riderFinishes.items(): @@ -575,7 +575,6 @@ def TidyMachinesList (riderMachines): # List of: # lastName, firstName, license, [list of machines], team, tTotalFinish, [list of (points, position) for each race in series] - #fixme need to return machine list string (line 540) here! categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], formatTime(riderTFinish[rider],True), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] return categoryResult, races, GetPotentialDuplicateFullNames(riderNameLicense) From 4265b17d13e7920d1e1ece5f48462d274f3dc193 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 19:58:20 +0000 Subject: [PATCH 06/53] Update Quickstart --- SeriesMgr/helptxt/QuickStart.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 8ef429118..5404c5a93 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -47,6 +47,12 @@ If you are interested in Team results, the Team names must be present in the Rac Blank team names, or "Independent", "Ind.", "Ind" are treated separately and are not combined into an overall result. +## Machine Identification + +For sports where the engineering of the bike (or human-powered vehicle, etc.) being ridden is an integral element of the competition. SeriesMgr keeps track of the machines used by each rider over the course of a series, and in the case of multiple machines being use presents a list for each rider in order of frequency. + +You can map variations of spelling etc. to a single name in the Machine Aliases screen. + # Races Screen At the top, add the name of your Series and Organizer. From 749579691f403d1357181701c83335984ba92138 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 19:59:32 +0000 Subject: [PATCH 07/53] Fix typo --- SeriesMgr/helptxt/QuickStart.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 5404c5a93..e5bb936d1 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -49,7 +49,7 @@ Blank team names, or "Independent", "Ind.", "Ind" are treated separately and are ## Machine Identification -For sports where the engineering of the bike (or human-powered vehicle, etc.) being ridden is an integral element of the competition. SeriesMgr keeps track of the machines used by each rider over the course of a series, and in the case of multiple machines being use presents a list for each rider in order of frequency. +For sports where the engineering of the bike (or human-powered vehicle, etc.) being ridden is an integral element of the competition. SeriesMgr keeps track of the machines used by each rider over the course of a series, and in the case of multiple machines being used presents a list for each rider in order of frequency. You can map variations of spelling etc. to a single name in the Machine Aliases screen. From 951ff315d309e0d8bdc7e6cb7adde980b06e3f94 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:00:09 +0000 Subject: [PATCH 08/53] 'machine' field in CrossMgrVideo --- CrossMgrVideo/AddPhotoHeader.py | 3 ++- CrossMgrVideo/Database.py | 5 +++-- CrossMgrVideo/MainWin.py | 14 ++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/CrossMgrVideo/AddPhotoHeader.py b/CrossMgrVideo/AddPhotoHeader.py index a31a4d251..d4de86001 100644 --- a/CrossMgrVideo/AddPhotoHeader.py +++ b/CrossMgrVideo/AddPhotoHeader.py @@ -86,7 +86,7 @@ def setDrawResources( dc, w, h ): drawResources.labelHeight = int(drawResources.bibHeight * 1.25 + 0.5) + int(drawResources.fontHeight * 1.25 + 0.5) -def AddPhotoHeader( bitmap, bib=None, ts=None, raceSeconds=None, first_name='', last_name='', team='', race_name='', kmh=None, mph=None ): +def AddPhotoHeader( bitmap, bib=None, ts=None, raceSeconds=None, first_name='', last_name='', machine='', team='', race_name='', kmh=None, mph=None ): global drawResources if bitmap is None: @@ -124,6 +124,7 @@ def AddPhotoHeader( bitmap, bib=None, ts=None, raceSeconds=None, first_name='', if mph: tsTxt += ', {:.2f}mph'.format(mph) if isinstance(mph, float) else str(kmh) nameTxt = ' '.join( n for n in (first_name, last_name) if n ) + nameTxt = ' - '.join( n for n in (nameTxt, machine) if n ) frameWidth = 4 borderWidth = 1 diff --git a/CrossMgrVideo/Database.py b/CrossMgrVideo/Database.py index cd41610da..5d70ce80e 100644 --- a/CrossMgrVideo/Database.py +++ b/CrossMgrVideo/Database.py @@ -59,7 +59,7 @@ def default_str( d ): class Database: triggerFieldsAll = ( - 'id','ts','s_before','s_after','ts_start','closest_frames','bib','first_name','last_name','team','wave','race_name', + 'id','ts','s_before','s_after','ts_start','closest_frames','bib','first_name','last_name','machine','team','wave','race_name', 'note','kmh','frames', 'finish_direction', 'zoom_frame', 'zoom_x', 'zoom_y', 'zoom_width', 'zoom_height', @@ -67,7 +67,7 @@ class Database: TriggerRecord = namedtuple( 'TriggerRecord', triggerFieldsAll ) triggerFieldsInput = set(triggerFieldsAll) - {'id', 'note', 'kmh', 'frames', 'finish_direction', 'zoom_frame', 'zoom_x', 'zoom_y', 'zoom_width', 'zoom_height',} # Fields to compare for equality of triggers. triggerFieldsUpdate = ('wave','race_name',) - triggerEditFields = ('bib', 'first_name', 'last_name', 'team', 'wave', 'race_name', 'note',) + triggerEditFields = ('bib', 'first_name', 'last_name', 'machine', 'team', 'wave', 'race_name', 'note',) @staticmethod def isValidDatabase( fname ): @@ -165,6 +165,7 @@ def __init__( self, fname=None, initTables=True, fps=30 ): ('bib', 'INTEGER', 'ASC', None), ('first_name', 'TEXT', 'ASC', None), ('last_name', 'TEXT', 'ASC', None), + ('machine', 'TEXT', 'ASC', None), ('team', 'TEXT', 'ASC', None), ('wave', 'TEXT', 'ASC', None), ('race_name', 'TEXT', 'ASC', None), diff --git a/CrossMgrVideo/MainWin.py b/CrossMgrVideo/MainWin.py index 21e98bf79..781e46b17 100644 --- a/CrossMgrVideo/MainWin.py +++ b/CrossMgrVideo/MainWin.py @@ -625,8 +625,8 @@ def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ): self.sm_dn = self.il.Add( Utils.GetPngBitmap('SmallDownArrow.png')) self.triggerList.SetImageList(self.il, wx.IMAGE_LIST_SMALL) - self.fieldCol = {f:c for c, f in enumerate('ts bib name team wave race_name frames view kmh mph note'.split())} - headers = ['Time', 'Bib', 'Name', 'Team', 'Wave', 'Race', 'Frames', 'View', 'km/h', 'mph', 'Note'] + self.fieldCol = {f:c for c, f in enumerate('ts bib name machine team wave race_name frames view kmh mph note'.split())} + headers = ['Time', 'Bib', 'Name', 'Machine', 'Team', 'Wave', 'Race', 'Frames', 'View', 'km/h', 'mph', 'Note'] formatRightHeaders = {'Bib','Frames','km/h','mph'} formatMiddleHeaders = {'View',} for i, h in enumerate(headers): @@ -1042,7 +1042,7 @@ def write_photos( dirname, infoList ): tsBest, jpgBest = GlobalDatabase().getBestTriggerPhoto( info['id'] ) if jpgBest is None: continue - args = {k:info[k] for k in ('ts', 'bib', 'first_name', 'last_name', 'team', 'race_name', 'kmh')} + args = {k:info[k] for k in ('ts', 'bib', 'first_name', 'last_name', 'machine', 'team', 'race_name', 'kmh')} try: args['raceSeconds'] = (info['ts'] - info['ts_start']).total_seconds() except Exception: @@ -1058,7 +1058,7 @@ def write_photos( dirname, infoList ): info['first_name'], ) ) - comment = json.dumps( {k:info[k] for k in ('bib', 'first_name', 'last_name', 'team', 'race_name')} ) + comment = json.dumps( {k:info[k] for k in ('bib', 'first_name', 'last_name', 'machine', 'team', 'race_name')} ) try: with open(os.path.join(dirname, fname), 'wb') as f: f.write( AddExifToJpeg(jpg, info['ts'], comment) ) @@ -1131,7 +1131,7 @@ def publish_web_photos( dirname, infoList, singleFile ): if jpgBest is None: continue - args = {k:info[k] for k in ('ts', 'bib', 'first_name', 'last_name', 'team', 'race_name', 'kmh')} + args = {k:info[k] for k in ('ts', 'bib', 'first_name', 'last_name', 'machine', 'team', 'race_name', 'kmh')} try: args['raceSeconds'] = (info['ts'] - info['ts_start']).total_seconds() except Exception: @@ -1139,7 +1139,7 @@ def publish_web_photos( dirname, infoList, singleFile ): if isinstance(args['kmh'], str): args['kmh'] = float( '0' + re.sub( '[^0-9.]', '', args['kmh'] ) ) - comment = json.dumps( {k:info[k] for k in ('bib', 'first_name', 'last_name', 'team', 'race_name')} ) + comment = json.dumps( {k:info[k] for k in ('bib', 'first_name', 'last_name', 'machine', 'team', 'race_name')} ) jpg = CVUtil.bitmapToJPeg( AddPhotoHeader(CVUtil.jpegToBitmap(jpgBest), **args) ) jpg = AddExifToJpeg( jpg, info['ts'], comment ) @@ -1303,6 +1303,7 @@ def updateSnapshot( self, t, f ): 'bib': self.snapshotCount, 'first_name': '', 'last_name': 'Snapshot', +# 'machine': '', 'team': '', 'wave': '', 'race_name': '', @@ -1728,6 +1729,7 @@ def refresh(): 'bib': msg.get('bib', 99999), 'first_name': msg.get('first_name','') or msg.get('firstName',''), 'last_name': msg.get('last_name','') or msg.get('lastName',''), + 'machine': msg.get('machine',''), 'team': msg.get('team',''), 'wave': msg.get('wave',''), 'race_name': msg.get('race_name','') or msg.get('raceName',''), From 5efb51a0a6b085b07d8afcccc2e0670364d8d8be Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:00:58 +0000 Subject: [PATCH 09/53] 'machine' field in headers --- CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html b/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html index faa50610f..1b3034b91 100644 --- a/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html +++ b/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html @@ -147,7 +147,7 @@ table.appendChild( thead ); let tr = document.createElement( 'tr' ); thead.appendChild( tr ); - const headers = ['', 'Bib', 'First Name', 'Last Name', 'Team', 'Time', 'Time of Day', 'Race']; + const headers = ['', 'Bib', 'First Name', 'Last Name', 'Machine', 'Team', 'Time', 'Time of Day', 'Race']; for( const h of headers ) { let th = document.createElement('th'); th.appendChild( document.createTextNode(h) ); @@ -155,7 +155,7 @@ } let tbody = document.createElement( 'tbody' ); table.appendChild( tbody ); - const attrs = ['bib', 'first_name', 'last_name', 'team', 'raceSeconds', 'ts', 'race_name']; + const attrs = ['bib', 'first_name', 'last_name', 'machine', 'team', 'raceSeconds', 'ts', 'race_name']; for( let i = 0; i < matched.length; ++i ) { const info = matched[i]; let tr = document.createElement( 'tr' ); From cc3bc904bb26651584e5b5cb5eeff9c8beb3db06 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 26 Nov 2022 22:04:45 +0000 Subject: [PATCH 10/53] Add 'machine' to search --- CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html b/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html index 1b3034b91..9782f7ec0 100644 --- a/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html +++ b/CrossMgrVideo/CrossMgrVideoHtml/PhotoPage.html @@ -128,7 +128,7 @@ matched = []; if( search_text ) { - const attrs = ['bib', 'first_name', 'last_name', 'team']; + const attrs = ['bib', 'first_name', 'last_name', 'machine', 'team']; for( const info of photo_info ) { for( const a of attrs ) { if( (info[a] + '').toLowerCase().includes( search_text ) ) { @@ -272,7 +272,7 @@
CrossMgr     -      +      Click and Drag in Photo to Zoom.    
From 673a2cccc9e5e2bfe7168c21302bf5c9c201b13e Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Mon, 28 Nov 2022 20:52:00 +0000 Subject: [PATCH 11/53] Add field to existing database --- CrossMgrVideo/Database.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CrossMgrVideo/Database.py b/CrossMgrVideo/Database.py index 5d70ce80e..f35648a68 100644 --- a/CrossMgrVideo/Database.py +++ b/CrossMgrVideo/Database.py @@ -122,6 +122,9 @@ def __init__( self, fname=None, initTables=True, fps=30 ): cols = cur.fetchall() if cols: col_names = {col[1] for col in cols} + if 'machine' not in col_names: + print( "Adding machine column to database..." ) + self.conn.execute( 'ALTER TABLE trigger ADD COLUMN machine TEXT DEFAULT ""' ) if 'note' not in col_names: self.conn.execute( 'ALTER TABLE trigger ADD COLUMN note TEXT DEFAULT ""' ) if 'kmh' not in col_names: From e5334c015ed9c1ee1ce7dc1b78e7f92e085b63db Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Mon, 28 Nov 2022 20:54:29 +0000 Subject: [PATCH 12/53] Remove debug statement --- CrossMgrVideo/Database.py | 1 - 1 file changed, 1 deletion(-) diff --git a/CrossMgrVideo/Database.py b/CrossMgrVideo/Database.py index f35648a68..08fa3040e 100644 --- a/CrossMgrVideo/Database.py +++ b/CrossMgrVideo/Database.py @@ -123,7 +123,6 @@ def __init__( self, fname=None, initTables=True, fps=30 ): if cols: col_names = {col[1] for col in cols} if 'machine' not in col_names: - print( "Adding machine column to database..." ) self.conn.execute( 'ALTER TABLE trigger ADD COLUMN machine TEXT DEFAULT ""' ) if 'note' not in col_names: self.conn.execute( 'ALTER TABLE trigger ADD COLUMN note TEXT DEFAULT ""' ) From 66ae6970f38d9776fa72c8f9c284dda1a944dc97 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 00:24:10 +0000 Subject: [PATCH 13/53] Add menu to toggle columns in trigger list --- CrossMgrVideo/MainWin.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/CrossMgrVideo/MainWin.py b/CrossMgrVideo/MainWin.py index 781e46b17..49338a9f4 100644 --- a/CrossMgrVideo/MainWin.py +++ b/CrossMgrVideo/MainWin.py @@ -30,6 +30,7 @@ from time import sleep import numpy as np from queue import Queue, Empty +import ast from datetime import datetime, timedelta, time @@ -626,10 +627,11 @@ def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ): self.triggerList.SetImageList(self.il, wx.IMAGE_LIST_SMALL) self.fieldCol = {f:c for c, f in enumerate('ts bib name machine team wave race_name frames view kmh mph note'.split())} - headers = ['Time', 'Bib', 'Name', 'Machine', 'Team', 'Wave', 'Race', 'Frames', 'View', 'km/h', 'mph', 'Note'] + self.fieldHeaders = ['Time', 'Bib', 'Name', 'Machine', 'Team', 'Wave', 'Race', 'Frames', 'View', 'km/h', 'mph', 'Note'] formatRightHeaders = {'Bib','Frames','km/h','mph'} formatMiddleHeaders = {'View',} - for i, h in enumerate(headers): + self.hiddenTriggerCols = [] + for i, h in enumerate(self.fieldHeaders): if h in formatRightHeaders: align = wx.LIST_FORMAT_RIGHT elif h in formatMiddleHeaders: @@ -644,6 +646,7 @@ def __init__( self, parent, id = wx.ID_ANY, title='', size=(1000,800) ): self.triggerList.Bind( wx.EVT_LIST_ITEM_RIGHT_CLICK, self.onTriggerRightClick ) self.triggerList.Bind( wx.EVT_LIST_KEY_DOWN, self.onTriggerKey ) #self.triggerList.Bind( wx.EVT_LIST_DELETE_ITEM, self.onTriggerDelete ) + self.triggerList.Bind( wx.EVT_LIST_COL_RIGHT_CLICK, self.onTriggerColumnRightClick ) vsTriggers = wx.BoxSizer( wx.VERTICAL ) vsTriggers.Add( hsDate ) @@ -1183,7 +1186,11 @@ def getTriggerRowFromID( self, id ): def updateTriggerColumnWidths( self ): for c in range(self.triggerList.GetColumnCount()): - self.triggerList.SetColumnWidth(c, wx.LIST_AUTOSIZE_USEHEADER if c != self.iNoteCol else 100 ) + if not c in self.hiddenTriggerCols: + self.triggerList.SetColumnWidth(c, wx.LIST_AUTOSIZE_USEHEADER if c != self.iNoteCol else 100 ) + else: + #if column is hidden, just set the width to zero + self.triggerList.SetColumnWidth( c, 0 ) def updateTriggerRow( self, row, fields ): ''' Update the row in the UI only. ''' @@ -1580,6 +1587,29 @@ def onTriggerEdit( self, event ): self.iTriggerSelect = event.Index self.doTriggerEdit() + def onTriggerColumnRightClick( self, event ): + # Create and display a popup menu of columns on right-click event + menu = wx.Menu() + menu.SetTitle( 'Show/Hide columns' ) + for c in range(self.triggerList.GetColumnCount()): + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.fieldHeaders[c] ) + self.Bind(wx.EVT_MENU, self.onToggleTriggerColumn) + if not c in self.hiddenTriggerCols: + menu.Check( menuItem.GetId(), True ) + self.PopupMenu(menu) + menu.Destroy() + + def onToggleTriggerColumn( self, event ): + #find the column number + label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() + c = self.fieldHeaders.index(label) + #add or remove from hidden columns and update width + if c in self.hiddenTriggerCols: + self.hiddenTriggerCols.remove( c ) + else: + self.hiddenTriggerCols.append( c ) + self.updateTriggerColumnWidths() + def showMessages( self ): while True: message = self.messageQ.get() @@ -1858,6 +1888,7 @@ def writeOptions( self ): self.config.Write( 'SecondsAfter', '{:.3f}'.format(self.tdCaptureAfter.total_seconds()) ) self.config.WriteFloat( 'ZoomMagnification', self.finishStrip.GetZoomMagnification() ) self.config.WriteInt( 'ClosestFrames', self.closestFrames ) + self.config.Write( 'HiddenTriggerCols', repr(self.hiddenTriggerCols) ) self.config.Flush() def readOptions( self ): @@ -1878,6 +1909,7 @@ def readOptions( self ): pass self.finishStrip.SetZoomMagnification( self.config.ReadFloat('ZoomMagnification', 0.5) ) self.closestFrames = self.config.ReadInt( 'ClosestFrames', 0 ) + self.hiddenTriggerCols = ast.literal_eval( self.config.Read( 'HiddenTriggerCols', '[]' ) ) def getCameraInfo( self ): width, height = self.getCameraResolution() From ad0a7bef11194128771f3e4cb2a0d79f1d0dcac1 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:27:31 +0000 Subject: [PATCH 14/53] Hiding of columns in SeriesMgr Results grid --- SprintMgr/Results.py | 1385 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 1145 insertions(+), 240 deletions(-) diff --git a/SprintMgr/Results.py b/SprintMgr/Results.py index e2966a8cf..3c4252f04 100644 --- a/SprintMgr/Results.py +++ b/SprintMgr/Results.py @@ -1,300 +1,1200 @@ -import os -import sys import wx import wx.grid as gridlib -from operator import attrgetter -import TestData -import Model +import os +import io +from html import escape +from urllib.parse import quote +import sys +import base64 +import datetime + import Utils +import SeriesModel +import GetModelInfo from ReorderableGrid import ReorderableGrid -from Competitions import SetDefaultData -from Utils import WriteCell -from Events import FontSize +from FitSheetWrapper import FitSheetWrapper +import FtpWriteFile +from ExportGrid import tag + +import xlwt +import io +import re +import webbrowser +import subprocess +import platform + +reNoDigits = re.compile( '[^0-9]' ) + +HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Machine', 'Team'] +def getHeaderNames(): + return HeaderNamesTemplate + ['Total Time' if SeriesModel.model.scoreByTime else 'Points', 'Gap'] + +#---------------------------------------------------------------------------------- + +def toFloat( n ): + try: + return float(n) + except Exception: + pass + try: + return float(n.split()[0]) + except Exception: + pass + try: + return float(n.split(',')[0]) + except Exception: + pass + + if ':' in n: + t = 0.0 + for v in n.split(':'): + try: + t = t * 60.0 + float(v) + except Exception: + return -1 + return t + + return -1.0 + +def getHeaderGraphicBase64(): + if Utils.mainWin: + b64 = Utils.mainWin.getGraphicBase64() + if b64: + return b64 + graphicFName = os.path.join(Utils.getImageFolder(), 'SeriesMgr128.png') + with open(graphicFName, 'rb') as f: + s64 = base64.standard_b64encode(f.read()) + return 'data:image/png;base64,{}'.format(s64) + +def getHtmlFileName(): + modelFileName = Utils.getFileName() if Utils.getFileName() else 'Test.smn' + fileName = os.path.basename( os.path.splitext(modelFileName)[0] + '.html' ) + defaultPath = os.path.dirname( modelFileName ) + return os.path.join( defaultPath, fileName ) + +def getHtml( htmlfileName=None, seriesFileName=None ): + model = SeriesModel.model + scoreByTime = model.scoreByTime + scoreByPercent = model.scoreByPercent + scoreByTrueSkill = model.scoreByTrueSkill + bestResultsToConsider = model.bestResultsToConsider + mustHaveCompleted = model.mustHaveCompleted + hasUpgrades = model.upgradePaths + considerPrimePointsOrTimeBonus = model.considerPrimePointsOrTimeBonus + raceResults = model.extractAllRaceResults() + + categoryNames = model.getCategoryNamesSortedPublish() + if not categoryNames: + return 'SeriesMgr: No Categories.' + + HeaderNames = getHeaderNames() + pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } + + if not seriesFileName: + seriesFileName = (os.path.splitext(Utils.mainWin.fileName)[0] if Utils.mainWin and Utils.mainWin.fileName else 'Series Results') + title = os.path.basename( seriesFileName ) + + licenseLinkTemplate = model.licenseLinkTemplate + + pointsStructures = {} + pointsStructuresList = [] + for race in model.races: + if race.pointStructure not in pointsStructures: + pointsStructures[race.pointStructure] = [] + pointsStructuresList.append( race.pointStructure ) + pointsStructures[race.pointStructure].append( race ) + + html = io.open( htmlfileName, 'w', encoding='utf-8', newline='' ) + + def write( s ): + html.write( '{}'.format(s) ) + + with tag(html, 'html'): + with tag(html, 'head'): + with tag(html, 'title'): + write( title.replace('\n', ' ') ) + with tag(html, 'meta', {'charset':'UTF-8'}): + pass + for k, v in model.getMetaTags(): + with tag(html, 'meta', {'name':k, 'content':v}): + pass + with tag(html, 'style', dict( type="text/css")): + write( ''' +body{ font-family: sans-serif; } + +h1{ font-size: 250%; } +h2{ font-size: 200%; } + +#idRaceName { + font-size: 200%; + font-weight: bold; +} +#idImgHeader { box-shadow: 4px 4px 4px #888888; } +.smallfont { font-size: 80%; } +.bigfont { font-size: 120%; } +.hidden { display: none; } + +#buttongroup { + margin:4px; + float:left; +} + +#buttongroup label { + float:left; + margin:4px; + background-color:#EFEFEF; + border-radius:4px; + border:1px solid #D0D0D0; + overflow:auto; + cursor: pointer; +} + +#buttongroup label span { + text-align:center; + padding:8px 8px; + display:block; +} + +#buttongroup label input { + position:absolute; + top:-20px; +} + +#buttongroup input:checked + span { + background-color:#404040; + color:#F7F7F7; +} + +#buttongroup .yellow { + background-color:#FFCC00; + color:#333; +} + +#buttongroup .blue { + background-color:#00BFFF; + color:#333; +} + +#buttongroup .pink { + background-color:#FF99FF; + color:#333; +} + +#buttongroup .green { + background-color:#7FE57F; + color:#333; +} +#buttongroup .purple { + background-color:#B399FF; + color:#333; +} + +table.results { + font-family:"Trebuchet MS", Arial, Helvetica, sans-serif; + border-collapse:collapse; +} +table.results td, table.results th { + font-size:1em; + padding:3px 7px 2px 7px; + text-align: left; +} +table.results th { + font-size:1.1em; + text-align:left; + padding-top:5px; + padding-bottom:4px; + background-color:#7FE57F; + color:#000000; + vertical-align:bottom; +} +table.results tr.odd { + color:#000000; + background-color:#EAF2D3; +} + +.smallFont { + font-size: 75%; +} + +table.results td.leftBorder, table.results th.leftBorder +{ + border-left:1px solid #98bf21; +} + +table.results tr:hover +{ + color:#000000; + background-color:#FFFFCC; +} +table.results tr.odd:hover +{ + color:#000000; + background-color:#FFFFCC; +} + +table.results td.colSelect +{ + color:#000000; + background-color:#FFFFCC; +}} + +table.results td { + border-top:1px solid #98bf21; +} + +table.results td.noborder { + border-top:0px solid #98bf21; +} + +table.results td.rightAlign, table.results th.rightAlign { + text-align:right; +} + +table.results td.leftAlign, table.results th.leftAlign { + text-align:left; +} + +.topAlign { + vertical-align:top; +} + +table.results th.centerAlign, table.results td.centerAlign { + text-align:center; +} -Arrow = '\u2192' +.ignored { + color: #999; + font-style: italic; +} + +table.points tr.odd { + color:#000000; + background-color:#EAF2D3; +} + +.rank { + color: #999; + font-style: italic; +} + +.points-cell { + text-align: right; + padding:3px 7px 2px 7px; +} + +select { + font: inherit; +} + +hr { clear: both; } + +@media print { + .noprint { display: none; } + .title { page-break-after: avoid; } +} +''') + + with tag(html, 'script', dict( type="text/javascript")): + write( '\nvar catMax={};\n'.format( len(categoryNames) ) ) + write( ''' +function removeClass( classStr, oldClass ) { + var classes = classStr.split( ' ' ); + var ret = []; + for( var i = 0; i < classes.length; ++i ) { + if( classes[i] != oldClass ) + ret.push( classes[i] ); + } + return ret.join(' '); +} + +function addClass( classStr, newClass ) { + return removeClass( classStr, newClass ) + ' ' + newClass; +} + +function selectCategory( iCat ) { + for( var i = 0; i < catMax; ++i ) { + var e = document.getElementById('catContent' + i); + if( i == iCat || iCat < 0 ) + e.className = removeClass(e.className, 'hidden'); + else + e.className = addClass(e.className, 'hidden'); + } +} + +function sortTable( table, col, reverse ) { + var tb = table.tBodies[0]; + var tr = Array.prototype.slice.call(tb.rows, 0); + + var parseRank = function( s ) { + if( !s ) + return 999999; + var fields = s.split( '(' ); + return parseInt( fields[1] ); + } + + var cmpPos = function( a, b ) { + return parseInt( a.cells[0].textContent.trim() ) - parseInt( b.cells[0].textContent.trim() ); + }; + + var MakeCmpStable = function( a, b, res ) { + if( res != 0 ) + return res; + return cmpPos( a, b ); + }; + + var cmpFunc; + if( col == 0 || col == 4 || col == 5 ) { // Pos, Points or Gap + cmpFunc = cmpPos; + } + else if( col >= 6 ) { // Race Points/Time and Rank + cmpFunc = function( a, b ) { + var x = parseRank( a.cells[6+(col-6)*2+1].textContent.trim() ); + var y = parseRank( b.cells[6+(col-6)*2+1].textContent.trim() ); + return MakeCmpStable( a, b, x - y ); + }; + } + else { // Rider data field. + cmpFunc = function( a, b ) { + return MakeCmpStable( a, b, a.cells[col].textContent.trim().localeCompare(b.cells[col].textContent.trim()) ); + }; + } + tr = tr.sort( function (a, b) { return reverse * cmpFunc(a, b); } ); + + for( var i = 0; i < tr.length; ++i) { + tr[i].className = (i % 2 == 1) ? addClass(tr[i].className,'odd') : removeClass(tr[i].className,'odd'); + tb.appendChild( tr[i] ); + } +} + +var ssPersist = {}; +function sortTableId( iTable, iCol ) { + var upChar = '  ▲', dnChar = '  ▼'; + var isNone = 0, isDn = 1, isUp = 2; + var id = 'idUpDn' + iTable + '_' + iCol; + var upDn = document.getElementById(id); + var sortState = ssPersist[id] ? ssPersist[id] : isNone; + var table = document.getElementById('idTable' + iTable); + + // Clear all sort states. + var row0Len = table.tBodies[0].rows[0].cells.length; + for( var i = 0; i < row0Len; ++i ) { + var idCur = 'idUpDn' + iTable + '_' + i; + var ele = document.getElementById(idCur); + if( ele ) { + ele.innerHTML = ''; + ssPersist[idCur] = isNone; + } + } + + if( iCol == 0 ) { + sortTable( table, 0, 1 ); + return; + } + + ++sortState; + switch( sortState ) { + case isDn: + upDn.innerHTML = dnChar; + sortTable( table, iCol, 1 ); + break; + case isUp: + upDn.innerHTML = upChar; + sortTable( table, iCol, -1 ); + break; + default: + sortState = isNone; + sortTable( table, 0, 1 ); + break; + } + ssPersist[id] = sortState; +} +''' ) + + with tag(html, 'body'): + with tag(html, 'table'): + with tag(html, 'tr'): + with tag(html, 'td', dict(valign='top')): + write( ''.format(getHeaderGraphicBase64()) ) + with tag(html, 'td'): + with tag(html, 'h1', {'style': 'margin-left: 1cm;'}): + write( escape(model.name) ) + with tag(html, 'h2', {'style': 'margin-left: 1cm;'}): + if model.organizer: + write( 'by {}'.format(escape(model.organizer)) ) + with tag(html, 'span', {'style': 'font-size: 60%'}): + write( ' ' * 5 ) + write( ' Updated: {}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ) + + with tag(html, 'h3' ): + with tag(html, 'label', {'for':'categoryselect'} ): + write( 'Category' + ':' ) + with tag(html, 'select', {'name': 'categoryselect', 'onchange':'selectCategory(parseInt(this.value,10))'} ): + with tag(html, 'option', {'value':-1} ): + with tag(html, 'span'): + write( 'All' ) + for iTable, categoryName in enumerate(categoryNames): + with tag(html, 'option', {'value':iTable} ): + with tag(html, 'span'): + write( '{}'.format(escape(categoryName)) ) + + for iTable, categoryName in enumerate(categoryNames): + results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( + categoryName, + raceResults, + pointsForRank, + useMostEventsCompleted=model.useMostEventsCompleted, + numPlacesTieBreaker=model.numPlacesTieBreaker ) + results = [rr for rr in results if toFloat(rr[4]) > 0.0] + + headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] + + with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): + write( '

') + write( '


') + + with tag(html, 'h2', {'class':'title'}): + write( escape(categoryName) ) + with tag(html, 'table', {'class': 'results', 'id': 'idTable{}'.format(iTable)} ): + with tag(html, 'thead'): + with tag(html, 'tr'): + for iHeader, col in enumerate(HeaderNames): + colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } + if col in ('License', 'Machine', 'Gap'): + colAttr['class'] = 'noprint' + with tag(html, 'th', colAttr): + with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): + pass + write( '{}'.format(escape(col).replace('\n', '
\n')) ) + for iRace, r in enumerate(races): + # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race + with tag(html, 'th', { + 'class':'leftBorder centerAlign noprint', + 'colspan': 2, + 'onclick': 'sortTableId({}, {})'.format(iTable, len(HeaderNames) + iRace), + } ): + with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,len(HeaderNames) + iRace)) ): + pass + if r[2]: + with tag(html,'a',dict(href='{}?raceCat={}'.format(r[2], quote(categoryName.encode('utf8')))) ): + write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + else: + write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + if r[0]: + write( '
' ) + with tag(html, 'span', {'class': 'smallFont'}): + write( '{}'.format(r[0].strftime('%b %d, %Y')) ) + if not scoreByTime and not scoreByPercent and not scoreByTrueSkill: + write( '
' ) + with tag(html, 'span', {'class': 'smallFont'}): + write( 'Top {}'.format(len(r[3].pointStructure)) ) + with tag(html, 'tbody'): + for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): + with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): + with tag(html, 'td', {'class':'rightAlign'}): + write( '{}'.format(pos+1) ) + with tag(html, 'td'): + write( '{}'.format(name or '') ) + with tag(html, 'td', {'class':'noprint'}): + if licenseLinkTemplate and license: + with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): + write( '{}'.format(license or '') ) + else: + write( '{}'.format(license or '') ) + with tag(html, 'td', {'class':'noprint'}): + write( '{}'.format(',
'.join(machines) or '') ) + with tag(html, 'td'): + write( '{}'.format(team or '') ) + with tag(html, 'td', {'class':'rightAlign'}): + write( '{}'.format(points or '') ) + with tag(html, 'td', {'class':'rightAlign noprint'}): + write( '{}'.format(gap or '') ) + for rPoints, rRank, rPrimePoints, rTimeBonus in racePoints: + if rPoints: + with tag(html, 'td', {'class':'leftBorder rightAlign noprint' + (' ignored' if '**' in '{}'.format(rPoints) else '')}): + write( '{}'.format(rPoints).replace('[','').replace(']','').replace(' ', ' ') ) + else: + with tag(html, 'td', {'class':'leftBorder noprint'}): + pass + + if rRank: + if rPrimePoints: + with tag(html, 'td', {'class':'rank noprint'}): + write( '({}) +{}'.format(Utils.ordinal(rRank).replace(' ', ' '), rPrimePoints) ) + elif rTimeBonus: + with tag(html, 'td', {'class':'rank noprint'}): + write( '({}) -{}'.format( + Utils.ordinal(rRank).replace(' ', ' '), + Utils.formatTime(rTimeBonus, twoDigitMinutes=False)), + ) + else: + with tag(html, 'td', {'class':'rank noprint'}): + write( '({})'.format(Utils.ordinal(rRank).replace(' ', ' ')) ) + else: + with tag(html, 'td', {'class':'noprint'}): + pass + + #----------------------------------------------------------------------------- + if considerPrimePointsOrTimeBonus: + with tag(html, 'p', {'class':'noprint'}): + if scoreByTime: + with tag(html, 'strong'): + with tag(html, 'span', {'style':'font-style: italic;'}): + write( '-MM:SS' ) + write( ' - {}'.format( 'Time Bonus subtracted from Finish Time.') ) + elif not scoreByTime and not scoreByPercent and not scoreByTrueSkill: + with tag(html, 'strong'): + with tag(html, 'span', {'style':'font-style: italic;'}): + write( '+N' ) + write( ' - {}'.format( 'Bonus Points added to Points for Place.') ) + + if bestResultsToConsider > 0 and not scoreByTrueSkill: + with tag(html, 'p', {'class':'noprint'}): + with tag(html, 'strong'): + write( '**' ) + write( ' - {}'.format( 'Result not considered. Not in best of {} scores.'.format(bestResultsToConsider) ) ) + + if hasUpgrades: + with tag(html, 'p', {'class':'noprint'}): + with tag(html, 'strong'): + write( 'pre-upg' ) + write( ' - {}'.format( 'Points carried forward from pre-upgrade category results (see Upgrades Progression below).' ) ) + + if mustHaveCompleted > 0: + with tag(html, 'p', {'class':'noprint'}): + write( 'Participants completing fewer than {} events are not shown.'.format(mustHaveCompleted) ) + + #----------------------------------------------------------------------------- + if scoreByTrueSkill: + with tag(html, 'div', {'class':'noprint'} ): + with tag(html, 'p'): + pass + with tag(html, 'hr'): + pass + + with tag(html, 'p'): + with tag(html, 'h2'): + write( 'TrueSkill' ) + with tag(html, 'p'): + write( u"TrueSkill is a ranking method developed by Microsoft Research for the XBox. ") + write( u"TrueSkill maintains an estimation of the skill of each competitor. Every time a competitor races, the system accordingly changes the perceived skill of the competitor and acquires more confidence about this perception. This is unlike a regular points system where a points can be accumulated through regular participation: not necessarily representing overall racing ability. ") + with tag(html, 'p'): + write( u"Results are shown above in the form RR (MM,VV). Competitor skill is represented by a normally distributed random variable with estimated mean (MM) and variance (VV). The mean is an estimation of the skill of the competitor and the variance represents how unsure the system is about it (bigger variance = more unsure). Competitors all start with mean = 25 and variance = 25/3 which corresponds to a zero ranking (see below). ") + with tag(html, 'p'): + write( u"The parameters of each distribution are updated based on the results from each race using a Bayesian approach. The extent of updates depends on each player's variance and on how 'surprising' the outcome is to the system. Changes to scores are negligible when outcomes are expected, but can be large when favorites surprisingly do poorly or underdogs surprisingly do well. ") + with tag(html, 'p'): + write( u"RR is the skill ranking defined by RR = MM - 3 * VV. This is a conservative estimate of the 'actual skill', which is expected to be higher than the estimate 99.7% of the time. " ) + write( u"There is no meaning to positive or negative skill levels which are a result of the underlying mathematics. The numbers are only meaningful relative to each other. ") + with tag(html, 'p'): + write( u"The TrueSkill score can be improved by 'consistently' (say, 2-3 times in a row) finishing ahead of higher ranked competitors. ") + write( u"Repeatedly finishing with similarly ranked competitors will not change the score much as it isn't evidence of improvement. ") + with tag(html, 'p'): + write("Full details ") + with tag(html, 'a', {'href': 'https://www.microsoft.com/en-us/research/publication/trueskilltm-a-bayesian-skill-rating-system/'} ): + write('here.') + + if not scoreByTime and not scoreByPercent and not scoreByTrueSkill: + with tag(html, 'div', {'class':'noprint'} ): + with tag(html, 'p'): + pass + with tag(html, 'hr'): + pass + + with tag(html, 'h2'): + write( 'Point Structures' ) + with tag(html, 'table' ): + for ps in pointsStructuresList: + with tag(html, 'tr'): + for header in [ps.name, 'Races Scored with {}'.format(ps.name)]: + with tag(html, 'th'): + write( header ) + + with tag(html, 'tr'): + with tag(html, 'td', {'class': 'topAlign'}): + write( ps.getHtml() ) + with tag(html, 'td', {'class': 'topAlign'}): + with tag(html, 'ul'): + for r in pointsStructures[ps]: + with tag(html, 'li'): + write( r.getRaceName() ) + + with tag(html, 'tr'): + with tag(html, 'td'): + pass + with tag(html, 'td'): + pass + + #----------------------------------------------------------------------------- + + with tag(html, 'p'): + pass + with tag(html, 'hr'): + pass + + with tag(html, 'h2'): + write( 'Tie Breaking Rules' ) + + with tag(html, 'p'): + write( u"If two or more riders are tied on points, the following rules are applied in sequence until the tie is broken:" ) + isFirst = True + tieLink = u"if still a tie, use " + with tag(html, 'ol'): + if model.useMostEventsCompleted: + with tag(html, 'li'): + write( u"{}number of events completed".format( tieLink if not isFirst else "" ) ) + isFirst = False + if model.numPlacesTieBreaker != 0: + finishOrdinals = [Utils.ordinal(p+1) for p in range(model.numPlacesTieBreaker)] + if model.numPlacesTieBreaker == 1: + finishStr = finishOrdinals[0] + else: + finishStr = ', '.join(finishOrdinals[:-1]) + ' then ' + finishOrdinals[-1] + with tag(html, 'li'): + write( u"{}number of {} place finishes".format( tieLink if not isFirst else "", + finishStr, + ) ) + isFirst = False + with tag(html, 'li'): + write( u"{}finish position in most recent event".format(tieLink if not isFirst else "") ) + isFirst = False + + if hasUpgrades: + with tag(html, 'p'): + pass + with tag(html, 'hr'): + pass + with tag(html, 'h2'): + write( u"Upgrades Progression" ) + with tag(html, 'ol'): + for i in range(len(model.upgradePaths)): + with tag(html, 'li'): + write( u"{}: {:.2f} points in pre-upgrade category carried forward".format(model.upgradePaths[i], model.upgradeFactors[i]) ) + #----------------------------------------------------------------------------- + with tag(html, 'p'): + with tag(html, 'a', dict(href='http://sites.google.com/site/crossmgrsoftware')): + write( 'Powered by CrossMgr' ) + + html.close() + +brandText = 'Powered by CrossMgr (sites.google.com/site/crossmgrsoftware)' + +textStyle = xlwt.easyxf( + "alignment: horizontal left;" + "borders: bottom thin;" +) +numberStyle = xlwt.easyxf( + "alignment: horizontal right;" + "borders: bottom thin;" +) +centerStyle = xlwt.easyxf( + "alignment: horizontal center;" + "borders: bottom thin;" +) +labelStyle = xlwt.easyxf( + "alignment: horizontal center;" + "borders: bottom medium;" +) class Results(wx.Panel): #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" - super().__init__(parent) - - self.font = wx.Font( (0,FontSize), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) - - self.showResultsLabel = wx.StaticText( self, label='Show:' ) - self.showResultsLabel.SetFont( self.font ) - self.showResults = wx.Choice( self, choices=['Qualifiers'] ) - self.showResults.SetFont( self.font ) - self.showResults.SetSelection( 0 ) - - self.communiqueLabel = wx.StaticText( self, label='Communiqu\u00E9:' ) - self.communiqueLabel.SetFont( self.font ) - self.communiqueNumber = wx.TextCtrl( self, value='', size=(80,-1) ) - self.communiqueNumber.SetFont( self.font ) - - self.showResults.Bind( wx.EVT_LEFT_DOWN, self.onClickResults ) - self.showResults.Bind( wx.EVT_CHOICE, self.onShowResults ) - self.showNames = wx.ToggleButton( self, label='Show Names' ) - self.showNames.SetFont( self.font ) - self.showNames.Bind( wx.EVT_TOGGLEBUTTON, self.onToggleShow ) - self.showTeams = wx.ToggleButton( self, label='Show Teams' ) - self.showTeams.SetFont( self.font ) - self.showTeams.Bind( wx.EVT_TOGGLEBUTTON, self.onToggleShow ) - self.competitionTime = wx.StaticText( self ) + wx.Panel.__init__(self, parent) - self.headerNames = ['Pos', 'Bib', 'Rider', 'Team', 'UCIID'] + self.categoryLabel = wx.StaticText( self, label='Category:' ) + self.categoryChoice = wx.Choice( self, choices = ['No Categories'] ) + self.categoryChoice.SetSelection( 0 ) + self.categoryChoice.Bind( wx.EVT_CHOICE, self.onCategoryChoice ) + self.statsLabel = wx.StaticText( self, label=' / ' ) + self.refreshButton = wx.Button( self, label='Refresh' ) + self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) + self.publishToHtml = wx.Button( self, label='Publish to Html' ) + self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) + self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) + self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) + self.publishToExcel = wx.Button( self, label='Publish to Excel' ) + self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) + + self.postPublishCmdLabel = wx.StaticText( self, label='Post Publish Cmd:' ) + self.postPublishCmd = wx.TextCtrl( self, size=(300,-1) ) + self.postPublishExplain = wx.StaticText( self, label='Command to run after publish. Use %* for all filenames (eg. "copy %* dirname")' ) + + hs = wx.BoxSizer( wx.HORIZONTAL ) + hs.Add( self.categoryLabel, flag=wx.TOP, border=4 ) + hs.Add( self.categoryChoice ) + hs.AddSpacer( 4 ) + hs.Add( self.statsLabel, flag=wx.TOP|wx.LEFT|wx.RIGHT, border=4 ) + hs.AddStretchSpacer() + hs.Add( self.refreshButton ) + hs.Add( self.publishToHtml, flag=wx.LEFT, border=48 ) + hs.Add( self.publishToFtp, flag=wx.LEFT, border=4 ) + hs.Add( self.publishToExcel, flag=wx.LEFT, border=4 ) + + hs2 = wx.BoxSizer( wx.HORIZONTAL ) + hs2.Add( self.postPublishCmdLabel, flag=wx.ALIGN_CENTRE_VERTICAL ) + hs2.Add( self.postPublishCmd, flag=wx.ALIGN_CENTRE_VERTICAL ) + hs2.Add( self.postPublishExplain, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=4 ) self.grid = ReorderableGrid( self, style = wx.BORDER_SUNKEN ) self.grid.DisableDragRowSize() self.grid.SetRowLabelSize( 64 ) - self.grid.CreateGrid( 0, len(self.headerNames) ) + self.grid.CreateGrid( 0, len(HeaderNamesTemplate)+1 ) self.grid.SetRowLabelSize( 0 ) self.grid.EnableReorderRows( False ) - self.setColNames() + self.grid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) + self.grid.Bind( wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.doCellClick ) + self.grid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.onResultsColumnRightClick ) + self.sortCol = None - sizer = wx.BoxSizer(wx.VERTICAL) - - hs = wx.BoxSizer(wx.HORIZONTAL) - hs.Add( self.showResultsLabel, 0, flag=wx.ALIGN_CENTRE_VERTICAL, border = 4 ) - hs.Add( self.showResults, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) - hs.Add( self.communiqueLabel, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) - hs.Add( self.communiqueNumber, 0, flag=wx.ALL|wx.EXPAND, border = 4 ) - hs.Add( self.showNames, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) - hs.Add( self.showTeams, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) - hs.Add( self.competitionTime, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) + self.setColNames(getHeaderNames()) + + sizer = wx.BoxSizer( wx.VERTICAL ) - sizer.Add(hs, 0, flag=wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, border = 6 ) - sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.ALL, border = 6) + sizer.Add(hs, flag=wx.TOP|wx.LEFT|wx.RIGHT, border = 4 ) + sizer.Add(hs2, flag=wx.ALIGN_RIGHT|wx.TOP|wx.LEFT|wx.RIGHT, border = 4 ) + sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.TOP|wx.ALL, border = 4) self.SetSizer(sizer) - def onToggleShow( self, e ): - model = Model.model - model.resultsShowNames = self.showNames.GetValue() - model.resultsShowTeams = self.showTeams.GetValue() + def onRefresh( self, event ): + SeriesModel.model.clearCache() self.refresh() - def setColNames( self ): - self.grid.SetLabelFont( self.font ) - for col, headerName in enumerate(self.headerNames): - self.grid.SetColLabelValue( col, headerName ) + def onCategoryChoice( self, event ): + try: + Utils.getMainWin().teamResults.setCategory( self.categoryChoice.GetString(self.categoryChoice.GetSelection()) ) + except AttributeError: + pass + wx.CallAfter( self.refresh ) + + def setCategory( self, catName ): + self.fixCategories() + model = SeriesModel.model + categoryNames = model.getCategoryNamesSortedPublish() + for i, n in enumerate(categoryNames): + if n == catName: + self.categoryChoice.SetSelection( i ) + break + + def readReset( self ): + self.sortCol = None + + def doLabelClick( self, event ): + col = event.GetCol() + label = self.grid.GetColLabelValue( col ) + if self.sortCol == col: + self.sortCol = -self.sortCol + elif self.sortCol == -col: + self.sortCol = None + else: + self.sortCol = col + if not self.sortCol: + self.sortCol = None + wx.CallAfter( self.refresh ) + + def doCellClick( self, event ): + if not hasattr(self, 'popupInfo'): + self.popupInfo = [ + ('{}...'.format(_('Copy Name to Clipboard')), wx.NewId(), self.onCopyName), + ('{}...'.format(_('Copy License to Clipboard')), wx.NewId(), self.onCopyLicense), + ('{}...'.format(_('Copy Machine to Clipboard')), wx.NewId(), self.onCopyMachine), + ('{}...'.format(_('Copy Team to Clipboard')), wx.NewId(), self.onCopyTeam), + ] + for p in self.popupInfo: + if p[2]: + self.Bind( wx.EVT_MENU, p[2], id=p[1] ) + + menu = wx.Menu() + for i, p in enumerate(self.popupInfo): + if p[2]: + menu.Append( p[1], p[0] ) + else: + menu.AppendSeparator() + + self.rowCur, self.colCur = event.GetRow(), event.GetCol() + self.PopupMenu( menu ) + menu.Destroy() + + def copyCellToClipboard( self, r, c ): + if wx.TheClipboard.Open(): + # Create a wx.TextDataObject + do = wx.TextDataObject() + do.SetText( self.grid.GetCellValue(r, c) ) + + # Add the data to the clipboard + wx.TheClipboard.SetData(do) + # Close the clipboard + wx.TheClipboard.Close() + else: + wx.MessageBox(u"Unable to open the clipboard", u"Error") + + def onCopyName( self, event ): + self.copyCellToClipboard( self.rowCur, 1 ) + + def onCopyLicense( self, event ): + self.copyCellToClipboard( self.rowCur, 2 ) + + def onCopyMachine( self, event ): + self.copyCellToClipboard( self.rowCur, 3 ) + + def onCopyTeam( self, event ): + self.copyCellToClipboard( self.rowCur, 4 ) + + def setColNames( self, headerNames ): + for col, headerName in enumerate(headerNames): + self.grid.SetColLabelValue( col, headerName ) attr = gridlib.GridCellAttr() - attr.SetFont( self.font ) - if self.headerNames[col] in {'Bib', 'Event'}: - attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP ) - elif 'Time' in self.headerNames[col]: + if headerName in ('Name', 'Team', 'License', 'Machine'): + attr.SetAlignment( wx.ALIGN_LEFT, wx.ALIGN_TOP ) + elif headerName in ('Pos', 'Points', 'Gap'): attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP ) - elif self.headerNames[col] == 'Pos': - attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP ) - elif Arrow in self.headerNames[col]: - attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_VERTICAL_CENTRE ) - elif self.headerNames[col].startswith( 'H' ): + else: attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP ) + attr.SetReadOnly( True ) self.grid.SetColAttr( col, attr ) - def getResultChoices( self ): - model = Model.model - competition = model.competition - choices = ['Seeding' if competition.isKeirin else 'Qualifiers'] + [system.name for system in competition.systems] - choices.append( 'Final Classification' ) - return choices - - def fixShowResults( self ): - model = Model.model - competition = model.competition - - choices = self.getResultChoices() - self.showResults.SetItems( choices ) - - if model.showResults >= len(choices): - model.showResults = 0 - self.showResults.SetSelection( model.showResults ) - - def getHideCols( self, headerNames ): - model = Model.model - toHide = set() - for col, h in enumerate(headerNames): - if h == 'Name' and not getattr(model, 'resultsShowNames', True): - toHide.add( col ) - elif h == 'Team' and not getattr(model, 'resultsShowTeams', True): - toHide.add( col ) - return toHide - def getGrid( self ): return self.grid - - def getPhase( self, num = None ): - if num is None: - num = self.showResults.GetSelection() - choices = self.getResultChoices() - return choices[num] - - def getTitle( self ): - phase = self.getPhase() - title = 'Communiqu\u00E9: {}\n{} {} '.format( - self.communiqueNumber.GetValue(), - phase, - '' if phase.startswith('Final') or phase.startswith('Time') else 'Draw Sheet/Intermediate Results' ) - return title - - def onClickResults( self, event ): - self.commit() - event.Skip() - def onShowResults( self, event ): - Model.model.showResults = self.showResults.GetSelection() - self.refresh() + def getTitle( self ): + return self.showResults.GetStringSelection() + ' Series Results' + def fixCategories( self ): + model = SeriesModel.model + categoryNames = model.getCategoryNamesSortedPublish() + lastSelection = self.categoryChoice.GetStringSelection() + self.categoryChoice.SetItems( categoryNames ) + iCurSelection = 0 + for i, n in enumerate(categoryNames): + if n == lastSelection: + iCurSelection = i + break + self.categoryChoice.SetSelection( iCurSelection ) + self.GetSizer().Layout() + def refresh( self ): - self.fixShowResults() + model = SeriesModel.model + scoreByTime = model.scoreByTime + scoreByPercent = model.scoreByPercent + scoreByTrueSkill = model.scoreByTrueSkill + HeaderNames = getHeaderNames() + + model = SeriesModel.model + self.postPublishCmd.SetValue( model.postPublishCmd ) + + with wx.BusyCursor() as wait: + self.raceResults = model.extractAllRaceResults() + + self.fixCategories() self.grid.ClearGrid() - model = Model.model - competition = model.competition + categoryName = self.categoryChoice.GetStringSelection() + if not categoryName: + return + + pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } + + results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( + categoryName, + self.raceResults, + pointsForRank, + useMostEventsCompleted=model.useMostEventsCompleted, + numPlacesTieBreaker=model.numPlacesTieBreaker, + ) - self.showNames.SetValue( getattr(model, 'resultsShowNames', True) ) - self.showTeams.SetValue( getattr(model, 'resultsShowTeams', True) ) + results = [rr for rr in results if toFloat(rr[4]) > 0.0] - self.communiqueNumber.SetValue( model.communique_number.get(self.getPhase(), '') ) + headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] - resultName = self.showResults.GetStringSelection() + Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) + self.setColNames( headerNames ) + #List of columns that can be hidden if empty + emptyCols = ['Name', 'License', 'Machine', 'Team'] - if 'Qualifiers' in resultName: - starters = competition.starters - - self.headerNames = ['Pos', 'Bib', 'Name', 'Team', 'Time'] - hideCols = self.getHideCols( self.headerNames ) - self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] - - riders = sorted( model.riders, key = Model.Rider.getKeyQualifying(competition.isKeirin) ) - for row, r in enumerate(riders): - if row >= starters or r.status == 'DNQ': - riders[row:] = sorted( riders[row:], key=attrgetter('iSeeding') ) - break - Utils.AdjustGridSize( self.grid, rowsRequired = len(riders), colsRequired = len(self.headerNames) ) - Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) - self.setColNames() - for row, r in enumerate(riders): - if row < starters and r.status != 'DNQ': - pos = '{}'.format(row + 1) - for col in range(self.grid.GetNumberCols()): - self.grid.SetCellBackgroundColour( row, col, wx.WHITE ) - else: - pos = 'DNQ' - for col in range(self.grid.GetNumberCols()): - self.grid.SetCellBackgroundColour( row, col, wx.Colour(200,200,200) ) - - writeCell = WriteCell( self.grid, row ) - for col, value in enumerate([pos,' {}'.format(r.bib), r.full_name, r.team, r.qualifying_time_text]): - if col not in hideCols: - writeCell( value ) - - competitionTime = model.qualifyingCompetitionTime - self.competitionTime.SetLabel( '{}: {}'.format(_('Est. Competition Time'), Utils.formatTime(competitionTime)) - if competitionTime else '' ) - - elif 'Final Classification' in resultName: - self.headerNames = ['Pos', 'Bib', 'Name', 'Team', 'UCIID'] - hideCols = self.getHideCols( self.headerNames ) - self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] + for row, (name, license, machines, team, points, gap, racePoints) in enumerate(results): + self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) + self.grid.SetCellValue( row, 1, '{}'.format(name or '') ) + self.grid.SetCellBackgroundColour( row, 1, wx.Colour(255,255,0) if name in potentialDuplicates else wx.Colour(255,255,255) ) + self.grid.SetCellValue( row, 2, '{}'.format(license or '') ) + self.grid.SetCellValue( row, 3, '{}'.format(',\n'.join(machines) or '') ) + self.grid.SetCellValue( row, 4, '{}'.format(team or '') ) + self.grid.SetCellValue( row, 5, '{}'.format(points) ) + self.grid.SetCellValue( row, 6, '{}'.format(gap) ) + for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): + self.grid.SetCellValue( row, 7 + q, + '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints + else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus + else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints + else '({})'.format(Utils.ordinal(rRank)) if rRank + else '' + ) + for c in range( len(headerNames) ): + self.grid.SetCellBackgroundColour( row, c, wx.WHITE ) + self.grid.SetCellTextColour( row, c, wx.BLACK ) + #Remove columns from emptyCols as soon as we see some data + if name is not (None or ''): + if 'Name' in emptyCols: emptyCols.remove('Name') + if license is not (None or ''): + if 'License' in emptyCols: emptyCols.remove('License') + if ''.join(machines) is not (None or ''): + if 'Machine' in emptyCols: emptyCols.remove('Machine') + if team is not (None or ''): + if 'Team' in emptyCols: emptyCols.remove('Team') + + if self.sortCol is not None: + def getBracketedNumber( v ): + numberMax = 99999 + if not v: + return numberMax + try: + return int(reNoDigits.sub('', v.split('(')[1])) + except (IndexError, ValueError): + return numberMax + + data = [] + for r in range(self.grid.GetNumberRows()): + rowOrig = [self.grid.GetCellValue(r, c) for c in range(0, self.grid.GetNumberCols())] + rowCmp = rowOrig[:] + rowCmp[0] = int(rowCmp[0]) + rowCmp[4] = Utils.StrToSeconds(rowCmp[4]) + rowCmp[5:] = [getBracketedNumber(v) for v in rowCmp[5:]] + rowCmp.extend( rowOrig ) + data.append( rowCmp ) - results, dnfs, dqs = competition.getResults() - Utils.AdjustGridSize( self.grid, rowsRequired = len(results), colsRequired = len(self.headerNames) ) - Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) - - self.setColNames() - for row, (classification, r) in enumerate(results): - writeCell = WriteCell( self.grid, row ) - if not r: - for col in range(self.grid.GetNumberCols()): - writeCell( '' ) - else: - for col, value in enumerate([classification, r.bib or '', r.full_name, r.team, r.uci_id]): - if col not in hideCols: - writeCell(' {}'.format(value) ) - self.competitionTime.SetLabel( '' ) + if self.sortCol > 0: + fg = wx.WHITE + bg = wx.Colour(0,100,0) + else: + fg = wx.BLACK + bg = wx.Colour(255,165,0) + + iCol = abs(self.sortCol) + data.sort( key = lambda x: x[iCol], reverse = (self.sortCol < 0) ) + for r, row in enumerate(data): + for c, v in enumerate(row[self.grid.GetNumberCols():]): + self.grid.SetCellValue( r, c, v ) + if c == iCol: + self.grid.SetCellTextColour( r, c, fg ) + self.grid.SetCellBackgroundColour( r, c, bg ) + if c < 4: + halign = wx.ALIGN_LEFT + elif c == 4 or c == 5: + halign = wx.ALIGN_RIGHT + else: + halign = wx.ALIGN_CENTRE + self.grid.SetCellAlignment( r, c, halign, wx.ALIGN_TOP ) + + self.statsLabel.SetLabel( '{} / {}'.format(self.grid.GetNumberRows(), GetModelInfo.GetTotalUniqueParticipants(self.raceResults)) ) + + #Hide the empty columns + for c in range(self.grid.GetNumberCols()): + if self.grid.GetColLabelValue(c) in emptyCols: + self.grid.HideCol(c) + else: + self.grid.ShowCol(c) + + self.grid.AutoSizeColumns( False ) + self.grid.AutoSizeRows( False ) + + self.GetSizer().Layout() + + def onResultsColumnRightClick( self, event ): + # Create and display a popup menu of columns on right-click event + menu = wx.Menu() + menu.SetTitle( 'Show/Hide columns' ) + for c in range(self.grid.GetNumberCols()): + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c) ) + self.Bind(wx.EVT_MENU, self.onToggleResultsColumn) + if self.grid.IsColShown(c): + menu.Check( menuItem.GetId(), True ) + self.PopupMenu(menu) + menu.Destroy() + + def onToggleResultsColumn( self, event ): + #find the column number + colLabels = [] + for c in range(self.grid.GetNumberCols()): + colLabels.append(self.grid.GetColLabelValue(c)) + label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() + c = colLabels.index(label) + if self.grid.IsColShown(c): + self.grid.HideCol(c) else: - # Find the System selected. - for system in competition.systems: - name = system.name - if name == resultName: - break + self.grid.ShowCol(c) + + def onPublishToExcel( self, event ): + model = SeriesModel.model + + scoreByTime = model.scoreByTime + scoreByPercent = model.scoreByPercent + scoreByTrueSkill = model.scoreByTrueSkill + HeaderNames = getHeaderNames() + + if Utils.mainWin: + if not Utils.mainWin.fileName: + Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) + return + + self.raceResults = model.extractAllRaceResults() + + categoryNames = model.getCategoryNamesSortedPublish() + if not categoryNames: + return - heatsMax = max( event.heatsMax for event in system.events ) - if heatsMax == 1: - self.headerNames = ['Event','Bib','Name','Note','Team',' ','Pos','Bib','Name','Note','Team','Time'] - else: - self.headerNames = ['Event','Bib','Name','Note','Team','H1','H2','H3',' ','Pos','Bib','Name','Note','Team','Time'] - hideCols = self.getHideCols( self.headerNames ) - self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] + pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } + + wb = xlwt.Workbook() + + for categoryName in categoryNames: + results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( + categoryName, + self.raceResults, + pointsForRank, + useMostEventsCompleted=model.useMostEventsCompleted, + numPlacesTieBreaker=model.numPlacesTieBreaker, + ) + results = [rr for rr in results if toFloat(rr[4]) > 0.0] + + headerNames = HeaderNames + [r[1] for r in races] - Utils.AdjustGridSize( self.grid, rowsRequired = len(system.events), colsRequired = len(self.headerNames) ) - Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) + ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) + wsFit = FitSheetWrapper( ws ) - self.setColNames() - state = competition.state + fnt = xlwt.Font() + fnt.name = 'Arial' + fnt.bold = True + fnt.height = int(fnt.height * 1.5) - for row, event in enumerate(system.events): - writeCell = WriteCell( self.grid, row ) - - writeCell( '{}'.format(row+1) ) - - riders = [state.labels.get(c, None) for c in event.composition] - writeCell( '\n'.join(['{}'.format(rider.bib) if rider and rider.bib else '' for rider in riders]) ) - if getattr(model, 'resultsShowNames', True): - writeCell( '\n'.join([rider.full_name if rider else '' for rider in riders]) ) - writeCell( '\n'.join([competition.getRelegationsWarningsStr(rider.bib, event, True) if rider else '' for rider in riders]) ) - if getattr(model, 'resultsShowTeams', True): - writeCell( '\n'.join([rider.team if rider else '' for rider in riders]) ) - - if heatsMax != 1: - for heat in range(heatsMax): - if event.heatsMax != 1: - writeCell( '\n'.join(event.getHeatPlaces(heat+1)) ) - else: - writeCell( '' ) - - #writeCell( ' ===> ', vert=wx.ALIGN_CENTRE ) - writeCell( ' '.join(['',Arrow,'']), vert=wx.ALIGN_CENTRE ) + headerStyle = xlwt.XFStyle() + headerStyle.font = fnt + + rowCur = 0 + ws.write_merge( rowCur, rowCur, 0, 8, model.name, headerStyle ) + rowCur += 1 + if model.organizer: + ws.write_merge( rowCur, rowCur, 0, 8, 'by {}'.format(model.organizer), headerStyle ) + rowCur += 1 + rowCur += 1 + colCur = 0 + ws.write_merge( rowCur, rowCur, colCur, colCur + 4, categoryName, xlwt.easyxf( + "font: name Arial, bold on;" + ) ); - out = [event.winner] + event.others - riders = [state.labels.get(c, None) for c in out] - writeCell( '\n'.join( '{}'.format(i+1) for i in range(len(riders))) ) - writeCell( '\n'.join(['{}'.format(rider.bib if rider.bib else '') if rider else '' for rider in riders]) ) - if getattr(model, 'resultsShowNames', True): - writeCell( '\n'.join([rider.full_name if rider else '' for rider in riders]) ) - writeCell( '\n'.join([competition.getRelegationsWarningsStr(rider.bib, event, False) if rider else '' for rider in riders]) ) - if getattr(model, 'resultsShowTeams', True): - writeCell( '\n'.join([rider.team if rider else '' for rider in riders]) ) - if event.winner in state.labels: - try: - value = '{:.3f}'.format(event.starts[-1].times[1]) - except (KeyError, IndexError, ValueError): - value = '' - writeCell( value ) + rowCur += 2 + for c, headerName in enumerate(headerNames): + wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) + rowCur += 1 - competitionTime = system.competitionTime - self.competitionTime.SetLabel( '{}: {}'.format(_('Est. Competition Time'), Utils.formatTime(competitionTime)) - if competitionTime else '' ) + for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): + wsFit.write( rowCur, 0, pos+1, numberStyle ) + wsFit.write( rowCur, 1, name, textStyle ) + wsFit.write( rowCur, 2, license, textStyle ) + wsFit.write( rowCur, 3, ', '.join(machines), textStyle ) + wsFit.write( rowCur, 4, team, textStyle ) + wsFit.write( rowCur, 5, points, numberStyle ) + wsFit.write( rowCur, 6, gap, numberStyle ) + for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): + wsFit.write( rowCur, 7 + q, + '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints + else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus + else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints + else '({})'.format(Utils.ordinal(rRank)) if rRank + else '', + centerStyle + ) + rowCur += 1 - self.grid.AutoSizeColumns( False ) - self.grid.AutoSizeRows( False ) + # Add branding at the bottom of the sheet. + style = xlwt.XFStyle() + style.alignment.horz = xlwt.Alignment.HORZ_LEFT + ws.write( rowCur + 2, 0, brandText, style ) + + if Utils.mainWin: + xlfileName = os.path.splitext(Utils.mainWin.fileName)[0] + '.xls' + else: + xlfileName = 'ResultsTest.xls' + + try: + wb.save( xlfileName ) + webbrowser.open( xlfileName, new = 2, autoraise = True ) + Utils.MessageOK(self, 'Excel file written to:\n\n {}'.format(xlfileName), 'Excel Write') + self.callPostPublishCmd( xlfileName ) + except IOError: + Utils.MessageOK(self, + 'Cannot write "{}".\n\nCheck if this spreadsheet is open.\nIf so, close it, and try again.'.format(xlfileName), + 'Excel File Error', iconMask=wx.ICON_ERROR ) + + def onPublishToHtml( self, event ): + if Utils.mainWin: + if not Utils.mainWin.fileName: + Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) + return + + htmlfileName = getHtmlFileName() + model = SeriesModel.model + model.postPublishCmd = self.postPublishCmd.GetValue().strip() + + try: + getHtml( htmlfileName ) + webbrowser.open( htmlfileName, new = 2, autoraise = True ) + Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') + except IOError: + Utils.MessageOK(self, + 'Cannot write "%s".\n\nCheck if this file is open.\nIf so, close it, and try again.' % htmlfileName, + 'Html File Error', iconMask=wx.ICON_ERROR ) + self.callPostPublishCmd( htmlfileName ) + + def onPublishToFtp( self, event ): + if Utils.mainWin: + if not Utils.mainWin.fileName: + Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) + return + + htmlfileName = getHtmlFileName() + + try: + getHtml( htmlfileName ) + except IOError: + return + html = io.open( htmlfileName, 'r', encoding='utf-8', newline='' ).read() + with FtpWriteFile.FtpPublishDialog( self, html=html ) as dlg: + dlg.ShowModal() + self.callPostPublishCmd( htmlfileName ) + def commit( self ): - model = Model.model - phase = self.getPhase() - cn = self.communiqueNumber.GetValue() - if cn != model.communique_number.get(phase, ''): - model.communique_number[phase] = cn + model = SeriesModel.model + postPublishCmd = self.postPublishCmd.GetValue().strip() + if model.postPublishCmd != postPublishCmd: + model.postPublishCmd = postPublishCmd model.setChanged() + + def callPostPublishCmd( self, fname ): + self.commit() + postPublishCmd = SeriesModel.model.postPublishCmd + if postPublishCmd and fname: + allFiles = [fname] + if platform.system() == 'Windows': + files = ' '.join('""{}""'.format(f) for f in allFiles) + else: + files = ' '.join('"{}"'.format(f) for f in allFiles) + + if '%*' in postPublishCmd: + cmd = postPublishCmd.replace('%*', files) + else: + cmd = ' '.join( [postPublishCmd, files] ) + + try: + subprocess.check_call( cmd, shell=True ) + except subprocess.CalledProcessError as e: + Utils.MessageOK( self, '{}\n\n {}\n{}: {}'.format('Post Publish Cmd Error', e, 'return code', e.returncode), _('Post Publish Cmd Error') ) + except Exception as e: + Utils.MessageOK( self, '{}\n\n {}'.format('Post Publish Cmd Error', e), 'Post Publish Cmd Error' ) ######################################################################## class ResultsFrame(wx.Frame): - """""" - #---------------------------------------------------------------------- def __init__(self): """Constructor""" @@ -306,6 +1206,11 @@ def __init__(self): #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.App(False) - Model.model = SetDefaultData() + Utils.disable_stdout_buffering() + model = SeriesModel.model + files = [ + r'C:\Projects\CrossMgr\ParkAvenue2\2013-06-26-Park Ave Bike Camp Arrowhead mtb 4-r1-.cmn', + ] + model.races = [SeriesModel.Race(fileName, model.pointStructures[0]) for fileName in files] frame = ResultsFrame() app.MainLoop() From 1bfb1c8c1b34fb17c2e6df40149cca58314016d1 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:29:29 +0000 Subject: [PATCH 15/53] Undo commit to wrong directory --- SprintMgr/Results.py | 1385 ++++++++---------------------------------- 1 file changed, 240 insertions(+), 1145 deletions(-) diff --git a/SprintMgr/Results.py b/SprintMgr/Results.py index 3c4252f04..e2966a8cf 100644 --- a/SprintMgr/Results.py +++ b/SprintMgr/Results.py @@ -1,1200 +1,300 @@ -import wx -import wx.grid as gridlib - import os -import io -from html import escape -from urllib.parse import quote import sys -import base64 -import datetime +import wx +import wx.grid as gridlib +from operator import attrgetter +import TestData +import Model import Utils -import SeriesModel -import GetModelInfo from ReorderableGrid import ReorderableGrid -from FitSheetWrapper import FitSheetWrapper -import FtpWriteFile -from ExportGrid import tag - -import xlwt -import io -import re -import webbrowser -import subprocess -import platform - -reNoDigits = re.compile( '[^0-9]' ) - -HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Machine', 'Team'] -def getHeaderNames(): - return HeaderNamesTemplate + ['Total Time' if SeriesModel.model.scoreByTime else 'Points', 'Gap'] - -#---------------------------------------------------------------------------------- - -def toFloat( n ): - try: - return float(n) - except Exception: - pass - try: - return float(n.split()[0]) - except Exception: - pass - try: - return float(n.split(',')[0]) - except Exception: - pass - - if ':' in n: - t = 0.0 - for v in n.split(':'): - try: - t = t * 60.0 + float(v) - except Exception: - return -1 - return t - - return -1.0 - -def getHeaderGraphicBase64(): - if Utils.mainWin: - b64 = Utils.mainWin.getGraphicBase64() - if b64: - return b64 - graphicFName = os.path.join(Utils.getImageFolder(), 'SeriesMgr128.png') - with open(graphicFName, 'rb') as f: - s64 = base64.standard_b64encode(f.read()) - return 'data:image/png;base64,{}'.format(s64) - -def getHtmlFileName(): - modelFileName = Utils.getFileName() if Utils.getFileName() else 'Test.smn' - fileName = os.path.basename( os.path.splitext(modelFileName)[0] + '.html' ) - defaultPath = os.path.dirname( modelFileName ) - return os.path.join( defaultPath, fileName ) - -def getHtml( htmlfileName=None, seriesFileName=None ): - model = SeriesModel.model - scoreByTime = model.scoreByTime - scoreByPercent = model.scoreByPercent - scoreByTrueSkill = model.scoreByTrueSkill - bestResultsToConsider = model.bestResultsToConsider - mustHaveCompleted = model.mustHaveCompleted - hasUpgrades = model.upgradePaths - considerPrimePointsOrTimeBonus = model.considerPrimePointsOrTimeBonus - raceResults = model.extractAllRaceResults() - - categoryNames = model.getCategoryNamesSortedPublish() - if not categoryNames: - return 'SeriesMgr: No Categories.' - - HeaderNames = getHeaderNames() - pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } - - if not seriesFileName: - seriesFileName = (os.path.splitext(Utils.mainWin.fileName)[0] if Utils.mainWin and Utils.mainWin.fileName else 'Series Results') - title = os.path.basename( seriesFileName ) - - licenseLinkTemplate = model.licenseLinkTemplate - - pointsStructures = {} - pointsStructuresList = [] - for race in model.races: - if race.pointStructure not in pointsStructures: - pointsStructures[race.pointStructure] = [] - pointsStructuresList.append( race.pointStructure ) - pointsStructures[race.pointStructure].append( race ) - - html = io.open( htmlfileName, 'w', encoding='utf-8', newline='' ) - - def write( s ): - html.write( '{}'.format(s) ) - - with tag(html, 'html'): - with tag(html, 'head'): - with tag(html, 'title'): - write( title.replace('\n', ' ') ) - with tag(html, 'meta', {'charset':'UTF-8'}): - pass - for k, v in model.getMetaTags(): - with tag(html, 'meta', {'name':k, 'content':v}): - pass - with tag(html, 'style', dict( type="text/css")): - write( ''' -body{ font-family: sans-serif; } - -h1{ font-size: 250%; } -h2{ font-size: 200%; } - -#idRaceName { - font-size: 200%; - font-weight: bold; -} -#idImgHeader { box-shadow: 4px 4px 4px #888888; } -.smallfont { font-size: 80%; } -.bigfont { font-size: 120%; } -.hidden { display: none; } - -#buttongroup { - margin:4px; - float:left; -} - -#buttongroup label { - float:left; - margin:4px; - background-color:#EFEFEF; - border-radius:4px; - border:1px solid #D0D0D0; - overflow:auto; - cursor: pointer; -} - -#buttongroup label span { - text-align:center; - padding:8px 8px; - display:block; -} - -#buttongroup label input { - position:absolute; - top:-20px; -} - -#buttongroup input:checked + span { - background-color:#404040; - color:#F7F7F7; -} - -#buttongroup .yellow { - background-color:#FFCC00; - color:#333; -} - -#buttongroup .blue { - background-color:#00BFFF; - color:#333; -} - -#buttongroup .pink { - background-color:#FF99FF; - color:#333; -} - -#buttongroup .green { - background-color:#7FE57F; - color:#333; -} -#buttongroup .purple { - background-color:#B399FF; - color:#333; -} - -table.results { - font-family:"Trebuchet MS", Arial, Helvetica, sans-serif; - border-collapse:collapse; -} -table.results td, table.results th { - font-size:1em; - padding:3px 7px 2px 7px; - text-align: left; -} -table.results th { - font-size:1.1em; - text-align:left; - padding-top:5px; - padding-bottom:4px; - background-color:#7FE57F; - color:#000000; - vertical-align:bottom; -} -table.results tr.odd { - color:#000000; - background-color:#EAF2D3; -} - -.smallFont { - font-size: 75%; -} - -table.results td.leftBorder, table.results th.leftBorder -{ - border-left:1px solid #98bf21; -} - -table.results tr:hover -{ - color:#000000; - background-color:#FFFFCC; -} -table.results tr.odd:hover -{ - color:#000000; - background-color:#FFFFCC; -} - -table.results td.colSelect -{ - color:#000000; - background-color:#FFFFCC; -}} - -table.results td { - border-top:1px solid #98bf21; -} - -table.results td.noborder { - border-top:0px solid #98bf21; -} - -table.results td.rightAlign, table.results th.rightAlign { - text-align:right; -} - -table.results td.leftAlign, table.results th.leftAlign { - text-align:left; -} - -.topAlign { - vertical-align:top; -} - -table.results th.centerAlign, table.results td.centerAlign { - text-align:center; -} +from Competitions import SetDefaultData +from Utils import WriteCell +from Events import FontSize -.ignored { - color: #999; - font-style: italic; -} - -table.points tr.odd { - color:#000000; - background-color:#EAF2D3; -} - -.rank { - color: #999; - font-style: italic; -} - -.points-cell { - text-align: right; - padding:3px 7px 2px 7px; -} - -select { - font: inherit; -} - -hr { clear: both; } - -@media print { - .noprint { display: none; } - .title { page-break-after: avoid; } -} -''') - - with tag(html, 'script', dict( type="text/javascript")): - write( '\nvar catMax={};\n'.format( len(categoryNames) ) ) - write( ''' -function removeClass( classStr, oldClass ) { - var classes = classStr.split( ' ' ); - var ret = []; - for( var i = 0; i < classes.length; ++i ) { - if( classes[i] != oldClass ) - ret.push( classes[i] ); - } - return ret.join(' '); -} - -function addClass( classStr, newClass ) { - return removeClass( classStr, newClass ) + ' ' + newClass; -} - -function selectCategory( iCat ) { - for( var i = 0; i < catMax; ++i ) { - var e = document.getElementById('catContent' + i); - if( i == iCat || iCat < 0 ) - e.className = removeClass(e.className, 'hidden'); - else - e.className = addClass(e.className, 'hidden'); - } -} - -function sortTable( table, col, reverse ) { - var tb = table.tBodies[0]; - var tr = Array.prototype.slice.call(tb.rows, 0); - - var parseRank = function( s ) { - if( !s ) - return 999999; - var fields = s.split( '(' ); - return parseInt( fields[1] ); - } - - var cmpPos = function( a, b ) { - return parseInt( a.cells[0].textContent.trim() ) - parseInt( b.cells[0].textContent.trim() ); - }; - - var MakeCmpStable = function( a, b, res ) { - if( res != 0 ) - return res; - return cmpPos( a, b ); - }; - - var cmpFunc; - if( col == 0 || col == 4 || col == 5 ) { // Pos, Points or Gap - cmpFunc = cmpPos; - } - else if( col >= 6 ) { // Race Points/Time and Rank - cmpFunc = function( a, b ) { - var x = parseRank( a.cells[6+(col-6)*2+1].textContent.trim() ); - var y = parseRank( b.cells[6+(col-6)*2+1].textContent.trim() ); - return MakeCmpStable( a, b, x - y ); - }; - } - else { // Rider data field. - cmpFunc = function( a, b ) { - return MakeCmpStable( a, b, a.cells[col].textContent.trim().localeCompare(b.cells[col].textContent.trim()) ); - }; - } - tr = tr.sort( function (a, b) { return reverse * cmpFunc(a, b); } ); - - for( var i = 0; i < tr.length; ++i) { - tr[i].className = (i % 2 == 1) ? addClass(tr[i].className,'odd') : removeClass(tr[i].className,'odd'); - tb.appendChild( tr[i] ); - } -} - -var ssPersist = {}; -function sortTableId( iTable, iCol ) { - var upChar = '  ▲', dnChar = '  ▼'; - var isNone = 0, isDn = 1, isUp = 2; - var id = 'idUpDn' + iTable + '_' + iCol; - var upDn = document.getElementById(id); - var sortState = ssPersist[id] ? ssPersist[id] : isNone; - var table = document.getElementById('idTable' + iTable); - - // Clear all sort states. - var row0Len = table.tBodies[0].rows[0].cells.length; - for( var i = 0; i < row0Len; ++i ) { - var idCur = 'idUpDn' + iTable + '_' + i; - var ele = document.getElementById(idCur); - if( ele ) { - ele.innerHTML = ''; - ssPersist[idCur] = isNone; - } - } - - if( iCol == 0 ) { - sortTable( table, 0, 1 ); - return; - } - - ++sortState; - switch( sortState ) { - case isDn: - upDn.innerHTML = dnChar; - sortTable( table, iCol, 1 ); - break; - case isUp: - upDn.innerHTML = upChar; - sortTable( table, iCol, -1 ); - break; - default: - sortState = isNone; - sortTable( table, 0, 1 ); - break; - } - ssPersist[id] = sortState; -} -''' ) - - with tag(html, 'body'): - with tag(html, 'table'): - with tag(html, 'tr'): - with tag(html, 'td', dict(valign='top')): - write( ''.format(getHeaderGraphicBase64()) ) - with tag(html, 'td'): - with tag(html, 'h1', {'style': 'margin-left: 1cm;'}): - write( escape(model.name) ) - with tag(html, 'h2', {'style': 'margin-left: 1cm;'}): - if model.organizer: - write( 'by {}'.format(escape(model.organizer)) ) - with tag(html, 'span', {'style': 'font-size: 60%'}): - write( ' ' * 5 ) - write( ' Updated: {}'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) ) - - with tag(html, 'h3' ): - with tag(html, 'label', {'for':'categoryselect'} ): - write( 'Category' + ':' ) - with tag(html, 'select', {'name': 'categoryselect', 'onchange':'selectCategory(parseInt(this.value,10))'} ): - with tag(html, 'option', {'value':-1} ): - with tag(html, 'span'): - write( 'All' ) - for iTable, categoryName in enumerate(categoryNames): - with tag(html, 'option', {'value':iTable} ): - with tag(html, 'span'): - write( '{}'.format(escape(categoryName)) ) - - for iTable, categoryName in enumerate(categoryNames): - results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( - categoryName, - raceResults, - pointsForRank, - useMostEventsCompleted=model.useMostEventsCompleted, - numPlacesTieBreaker=model.numPlacesTieBreaker ) - results = [rr for rr in results if toFloat(rr[4]) > 0.0] - - headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] - - with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): - write( '

') - write( '


') - - with tag(html, 'h2', {'class':'title'}): - write( escape(categoryName) ) - with tag(html, 'table', {'class': 'results', 'id': 'idTable{}'.format(iTable)} ): - with tag(html, 'thead'): - with tag(html, 'tr'): - for iHeader, col in enumerate(HeaderNames): - colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } - if col in ('License', 'Machine', 'Gap'): - colAttr['class'] = 'noprint' - with tag(html, 'th', colAttr): - with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): - pass - write( '{}'.format(escape(col).replace('\n', '
\n')) ) - for iRace, r in enumerate(races): - # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race - with tag(html, 'th', { - 'class':'leftBorder centerAlign noprint', - 'colspan': 2, - 'onclick': 'sortTableId({}, {})'.format(iTable, len(HeaderNames) + iRace), - } ): - with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,len(HeaderNames) + iRace)) ): - pass - if r[2]: - with tag(html,'a',dict(href='{}?raceCat={}'.format(r[2], quote(categoryName.encode('utf8')))) ): - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) - else: - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) - if r[0]: - write( '
' ) - with tag(html, 'span', {'class': 'smallFont'}): - write( '{}'.format(r[0].strftime('%b %d, %Y')) ) - if not scoreByTime and not scoreByPercent and not scoreByTrueSkill: - write( '
' ) - with tag(html, 'span', {'class': 'smallFont'}): - write( 'Top {}'.format(len(r[3].pointStructure)) ) - with tag(html, 'tbody'): - for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): - with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): - with tag(html, 'td', {'class':'rightAlign'}): - write( '{}'.format(pos+1) ) - with tag(html, 'td'): - write( '{}'.format(name or '') ) - with tag(html, 'td', {'class':'noprint'}): - if licenseLinkTemplate and license: - with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): - write( '{}'.format(license or '') ) - else: - write( '{}'.format(license or '') ) - with tag(html, 'td', {'class':'noprint'}): - write( '{}'.format(',
'.join(machines) or '') ) - with tag(html, 'td'): - write( '{}'.format(team or '') ) - with tag(html, 'td', {'class':'rightAlign'}): - write( '{}'.format(points or '') ) - with tag(html, 'td', {'class':'rightAlign noprint'}): - write( '{}'.format(gap or '') ) - for rPoints, rRank, rPrimePoints, rTimeBonus in racePoints: - if rPoints: - with tag(html, 'td', {'class':'leftBorder rightAlign noprint' + (' ignored' if '**' in '{}'.format(rPoints) else '')}): - write( '{}'.format(rPoints).replace('[','').replace(']','').replace(' ', ' ') ) - else: - with tag(html, 'td', {'class':'leftBorder noprint'}): - pass - - if rRank: - if rPrimePoints: - with tag(html, 'td', {'class':'rank noprint'}): - write( '({}) +{}'.format(Utils.ordinal(rRank).replace(' ', ' '), rPrimePoints) ) - elif rTimeBonus: - with tag(html, 'td', {'class':'rank noprint'}): - write( '({}) -{}'.format( - Utils.ordinal(rRank).replace(' ', ' '), - Utils.formatTime(rTimeBonus, twoDigitMinutes=False)), - ) - else: - with tag(html, 'td', {'class':'rank noprint'}): - write( '({})'.format(Utils.ordinal(rRank).replace(' ', ' ')) ) - else: - with tag(html, 'td', {'class':'noprint'}): - pass - - #----------------------------------------------------------------------------- - if considerPrimePointsOrTimeBonus: - with tag(html, 'p', {'class':'noprint'}): - if scoreByTime: - with tag(html, 'strong'): - with tag(html, 'span', {'style':'font-style: italic;'}): - write( '-MM:SS' ) - write( ' - {}'.format( 'Time Bonus subtracted from Finish Time.') ) - elif not scoreByTime and not scoreByPercent and not scoreByTrueSkill: - with tag(html, 'strong'): - with tag(html, 'span', {'style':'font-style: italic;'}): - write( '+N' ) - write( ' - {}'.format( 'Bonus Points added to Points for Place.') ) - - if bestResultsToConsider > 0 and not scoreByTrueSkill: - with tag(html, 'p', {'class':'noprint'}): - with tag(html, 'strong'): - write( '**' ) - write( ' - {}'.format( 'Result not considered. Not in best of {} scores.'.format(bestResultsToConsider) ) ) - - if hasUpgrades: - with tag(html, 'p', {'class':'noprint'}): - with tag(html, 'strong'): - write( 'pre-upg' ) - write( ' - {}'.format( 'Points carried forward from pre-upgrade category results (see Upgrades Progression below).' ) ) - - if mustHaveCompleted > 0: - with tag(html, 'p', {'class':'noprint'}): - write( 'Participants completing fewer than {} events are not shown.'.format(mustHaveCompleted) ) - - #----------------------------------------------------------------------------- - if scoreByTrueSkill: - with tag(html, 'div', {'class':'noprint'} ): - with tag(html, 'p'): - pass - with tag(html, 'hr'): - pass - - with tag(html, 'p'): - with tag(html, 'h2'): - write( 'TrueSkill' ) - with tag(html, 'p'): - write( u"TrueSkill is a ranking method developed by Microsoft Research for the XBox. ") - write( u"TrueSkill maintains an estimation of the skill of each competitor. Every time a competitor races, the system accordingly changes the perceived skill of the competitor and acquires more confidence about this perception. This is unlike a regular points system where a points can be accumulated through regular participation: not necessarily representing overall racing ability. ") - with tag(html, 'p'): - write( u"Results are shown above in the form RR (MM,VV). Competitor skill is represented by a normally distributed random variable with estimated mean (MM) and variance (VV). The mean is an estimation of the skill of the competitor and the variance represents how unsure the system is about it (bigger variance = more unsure). Competitors all start with mean = 25 and variance = 25/3 which corresponds to a zero ranking (see below). ") - with tag(html, 'p'): - write( u"The parameters of each distribution are updated based on the results from each race using a Bayesian approach. The extent of updates depends on each player's variance and on how 'surprising' the outcome is to the system. Changes to scores are negligible when outcomes are expected, but can be large when favorites surprisingly do poorly or underdogs surprisingly do well. ") - with tag(html, 'p'): - write( u"RR is the skill ranking defined by RR = MM - 3 * VV. This is a conservative estimate of the 'actual skill', which is expected to be higher than the estimate 99.7% of the time. " ) - write( u"There is no meaning to positive or negative skill levels which are a result of the underlying mathematics. The numbers are only meaningful relative to each other. ") - with tag(html, 'p'): - write( u"The TrueSkill score can be improved by 'consistently' (say, 2-3 times in a row) finishing ahead of higher ranked competitors. ") - write( u"Repeatedly finishing with similarly ranked competitors will not change the score much as it isn't evidence of improvement. ") - with tag(html, 'p'): - write("Full details ") - with tag(html, 'a', {'href': 'https://www.microsoft.com/en-us/research/publication/trueskilltm-a-bayesian-skill-rating-system/'} ): - write('here.') - - if not scoreByTime and not scoreByPercent and not scoreByTrueSkill: - with tag(html, 'div', {'class':'noprint'} ): - with tag(html, 'p'): - pass - with tag(html, 'hr'): - pass - - with tag(html, 'h2'): - write( 'Point Structures' ) - with tag(html, 'table' ): - for ps in pointsStructuresList: - with tag(html, 'tr'): - for header in [ps.name, 'Races Scored with {}'.format(ps.name)]: - with tag(html, 'th'): - write( header ) - - with tag(html, 'tr'): - with tag(html, 'td', {'class': 'topAlign'}): - write( ps.getHtml() ) - with tag(html, 'td', {'class': 'topAlign'}): - with tag(html, 'ul'): - for r in pointsStructures[ps]: - with tag(html, 'li'): - write( r.getRaceName() ) - - with tag(html, 'tr'): - with tag(html, 'td'): - pass - with tag(html, 'td'): - pass - - #----------------------------------------------------------------------------- - - with tag(html, 'p'): - pass - with tag(html, 'hr'): - pass - - with tag(html, 'h2'): - write( 'Tie Breaking Rules' ) - - with tag(html, 'p'): - write( u"If two or more riders are tied on points, the following rules are applied in sequence until the tie is broken:" ) - isFirst = True - tieLink = u"if still a tie, use " - with tag(html, 'ol'): - if model.useMostEventsCompleted: - with tag(html, 'li'): - write( u"{}number of events completed".format( tieLink if not isFirst else "" ) ) - isFirst = False - if model.numPlacesTieBreaker != 0: - finishOrdinals = [Utils.ordinal(p+1) for p in range(model.numPlacesTieBreaker)] - if model.numPlacesTieBreaker == 1: - finishStr = finishOrdinals[0] - else: - finishStr = ', '.join(finishOrdinals[:-1]) + ' then ' + finishOrdinals[-1] - with tag(html, 'li'): - write( u"{}number of {} place finishes".format( tieLink if not isFirst else "", - finishStr, - ) ) - isFirst = False - with tag(html, 'li'): - write( u"{}finish position in most recent event".format(tieLink if not isFirst else "") ) - isFirst = False - - if hasUpgrades: - with tag(html, 'p'): - pass - with tag(html, 'hr'): - pass - with tag(html, 'h2'): - write( u"Upgrades Progression" ) - with tag(html, 'ol'): - for i in range(len(model.upgradePaths)): - with tag(html, 'li'): - write( u"{}: {:.2f} points in pre-upgrade category carried forward".format(model.upgradePaths[i], model.upgradeFactors[i]) ) - #----------------------------------------------------------------------------- - with tag(html, 'p'): - with tag(html, 'a', dict(href='http://sites.google.com/site/crossmgrsoftware')): - write( 'Powered by CrossMgr' ) - - html.close() - -brandText = 'Powered by CrossMgr (sites.google.com/site/crossmgrsoftware)' - -textStyle = xlwt.easyxf( - "alignment: horizontal left;" - "borders: bottom thin;" -) -numberStyle = xlwt.easyxf( - "alignment: horizontal right;" - "borders: bottom thin;" -) -centerStyle = xlwt.easyxf( - "alignment: horizontal center;" - "borders: bottom thin;" -) -labelStyle = xlwt.easyxf( - "alignment: horizontal center;" - "borders: bottom medium;" -) +Arrow = '\u2192' class Results(wx.Panel): #---------------------------------------------------------------------- def __init__(self, parent): """Constructor""" - wx.Panel.__init__(self, parent) + super().__init__(parent) - self.categoryLabel = wx.StaticText( self, label='Category:' ) - self.categoryChoice = wx.Choice( self, choices = ['No Categories'] ) - self.categoryChoice.SetSelection( 0 ) - self.categoryChoice.Bind( wx.EVT_CHOICE, self.onCategoryChoice ) - self.statsLabel = wx.StaticText( self, label=' / ' ) - self.refreshButton = wx.Button( self, label='Refresh' ) - self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) - self.publishToHtml = wx.Button( self, label='Publish to Html' ) - self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) - self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) - self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) - self.publishToExcel = wx.Button( self, label='Publish to Excel' ) - self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) - - self.postPublishCmdLabel = wx.StaticText( self, label='Post Publish Cmd:' ) - self.postPublishCmd = wx.TextCtrl( self, size=(300,-1) ) - self.postPublishExplain = wx.StaticText( self, label='Command to run after publish. Use %* for all filenames (eg. "copy %* dirname")' ) - - hs = wx.BoxSizer( wx.HORIZONTAL ) - hs.Add( self.categoryLabel, flag=wx.TOP, border=4 ) - hs.Add( self.categoryChoice ) - hs.AddSpacer( 4 ) - hs.Add( self.statsLabel, flag=wx.TOP|wx.LEFT|wx.RIGHT, border=4 ) - hs.AddStretchSpacer() - hs.Add( self.refreshButton ) - hs.Add( self.publishToHtml, flag=wx.LEFT, border=48 ) - hs.Add( self.publishToFtp, flag=wx.LEFT, border=4 ) - hs.Add( self.publishToExcel, flag=wx.LEFT, border=4 ) - - hs2 = wx.BoxSizer( wx.HORIZONTAL ) - hs2.Add( self.postPublishCmdLabel, flag=wx.ALIGN_CENTRE_VERTICAL ) - hs2.Add( self.postPublishCmd, flag=wx.ALIGN_CENTRE_VERTICAL ) - hs2.Add( self.postPublishExplain, flag=wx.ALIGN_CENTRE_VERTICAL|wx.LEFT, border=4 ) + self.font = wx.Font( (0,FontSize), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL ) + + self.showResultsLabel = wx.StaticText( self, label='Show:' ) + self.showResultsLabel.SetFont( self.font ) + self.showResults = wx.Choice( self, choices=['Qualifiers'] ) + self.showResults.SetFont( self.font ) + self.showResults.SetSelection( 0 ) + + self.communiqueLabel = wx.StaticText( self, label='Communiqu\u00E9:' ) + self.communiqueLabel.SetFont( self.font ) + self.communiqueNumber = wx.TextCtrl( self, value='', size=(80,-1) ) + self.communiqueNumber.SetFont( self.font ) + + self.showResults.Bind( wx.EVT_LEFT_DOWN, self.onClickResults ) + self.showResults.Bind( wx.EVT_CHOICE, self.onShowResults ) + self.showNames = wx.ToggleButton( self, label='Show Names' ) + self.showNames.SetFont( self.font ) + self.showNames.Bind( wx.EVT_TOGGLEBUTTON, self.onToggleShow ) + self.showTeams = wx.ToggleButton( self, label='Show Teams' ) + self.showTeams.SetFont( self.font ) + self.showTeams.Bind( wx.EVT_TOGGLEBUTTON, self.onToggleShow ) + self.competitionTime = wx.StaticText( self ) + + self.headerNames = ['Pos', 'Bib', 'Rider', 'Team', 'UCIID'] self.grid = ReorderableGrid( self, style = wx.BORDER_SUNKEN ) self.grid.DisableDragRowSize() self.grid.SetRowLabelSize( 64 ) - self.grid.CreateGrid( 0, len(HeaderNamesTemplate)+1 ) + self.grid.CreateGrid( 0, len(self.headerNames) ) self.grid.SetRowLabelSize( 0 ) self.grid.EnableReorderRows( False ) - self.grid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) - self.grid.Bind( wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.doCellClick ) - self.grid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.onResultsColumnRightClick ) - self.sortCol = None + self.setColNames() - self.setColNames(getHeaderNames()) - - sizer = wx.BoxSizer( wx.VERTICAL ) + sizer = wx.BoxSizer(wx.VERTICAL) + + hs = wx.BoxSizer(wx.HORIZONTAL) + hs.Add( self.showResultsLabel, 0, flag=wx.ALIGN_CENTRE_VERTICAL, border = 4 ) + hs.Add( self.showResults, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) + hs.Add( self.communiqueLabel, 0, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) + hs.Add( self.communiqueNumber, 0, flag=wx.ALL|wx.EXPAND, border = 4 ) + hs.Add( self.showNames, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) + hs.Add( self.showTeams, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) + hs.Add( self.competitionTime, flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border = 4 ) - sizer.Add(hs, flag=wx.TOP|wx.LEFT|wx.RIGHT, border = 4 ) - sizer.Add(hs2, flag=wx.ALIGN_RIGHT|wx.TOP|wx.LEFT|wx.RIGHT, border = 4 ) - sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.TOP|wx.ALL, border = 4) + sizer.Add(hs, 0, flag=wx.EXPAND|wx.TOP|wx.LEFT|wx.RIGHT, border = 6 ) + sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.ALL, border = 6) self.SetSizer(sizer) - def onRefresh( self, event ): - SeriesModel.model.clearCache() + def onToggleShow( self, e ): + model = Model.model + model.resultsShowNames = self.showNames.GetValue() + model.resultsShowTeams = self.showTeams.GetValue() self.refresh() - def onCategoryChoice( self, event ): - try: - Utils.getMainWin().teamResults.setCategory( self.categoryChoice.GetString(self.categoryChoice.GetSelection()) ) - except AttributeError: - pass - wx.CallAfter( self.refresh ) - - def setCategory( self, catName ): - self.fixCategories() - model = SeriesModel.model - categoryNames = model.getCategoryNamesSortedPublish() - for i, n in enumerate(categoryNames): - if n == catName: - self.categoryChoice.SetSelection( i ) - break - - def readReset( self ): - self.sortCol = None - - def doLabelClick( self, event ): - col = event.GetCol() - label = self.grid.GetColLabelValue( col ) - if self.sortCol == col: - self.sortCol = -self.sortCol - elif self.sortCol == -col: - self.sortCol = None - else: - self.sortCol = col - - if not self.sortCol: - self.sortCol = None - wx.CallAfter( self.refresh ) - - def doCellClick( self, event ): - if not hasattr(self, 'popupInfo'): - self.popupInfo = [ - ('{}...'.format(_('Copy Name to Clipboard')), wx.NewId(), self.onCopyName), - ('{}...'.format(_('Copy License to Clipboard')), wx.NewId(), self.onCopyLicense), - ('{}...'.format(_('Copy Machine to Clipboard')), wx.NewId(), self.onCopyMachine), - ('{}...'.format(_('Copy Team to Clipboard')), wx.NewId(), self.onCopyTeam), - ] - for p in self.popupInfo: - if p[2]: - self.Bind( wx.EVT_MENU, p[2], id=p[1] ) - - menu = wx.Menu() - for i, p in enumerate(self.popupInfo): - if p[2]: - menu.Append( p[1], p[0] ) - else: - menu.AppendSeparator() - - self.rowCur, self.colCur = event.GetRow(), event.GetCol() - self.PopupMenu( menu ) - menu.Destroy() - - def copyCellToClipboard( self, r, c ): - if wx.TheClipboard.Open(): - # Create a wx.TextDataObject - do = wx.TextDataObject() - do.SetText( self.grid.GetCellValue(r, c) ) - - # Add the data to the clipboard - wx.TheClipboard.SetData(do) - # Close the clipboard - wx.TheClipboard.Close() - else: - wx.MessageBox(u"Unable to open the clipboard", u"Error") - - def onCopyName( self, event ): - self.copyCellToClipboard( self.rowCur, 1 ) - - def onCopyLicense( self, event ): - self.copyCellToClipboard( self.rowCur, 2 ) - - def onCopyMachine( self, event ): - self.copyCellToClipboard( self.rowCur, 3 ) - - def onCopyTeam( self, event ): - self.copyCellToClipboard( self.rowCur, 4 ) - - def setColNames( self, headerNames ): - for col, headerName in enumerate(headerNames): + def setColNames( self ): + self.grid.SetLabelFont( self.font ) + for col, headerName in enumerate(self.headerNames): self.grid.SetColLabelValue( col, headerName ) + attr = gridlib.GridCellAttr() - if headerName in ('Name', 'Team', 'License', 'Machine'): - attr.SetAlignment( wx.ALIGN_LEFT, wx.ALIGN_TOP ) - elif headerName in ('Pos', 'Points', 'Gap'): + attr.SetFont( self.font ) + if self.headerNames[col] in {'Bib', 'Event'}: + attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP ) + elif 'Time' in self.headerNames[col]: attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP ) - else: + elif self.headerNames[col] == 'Pos': + attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP ) + elif Arrow in self.headerNames[col]: + attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_VERTICAL_CENTRE ) + elif self.headerNames[col].startswith( 'H' ): attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP ) - attr.SetReadOnly( True ) self.grid.SetColAttr( col, attr ) + def getResultChoices( self ): + model = Model.model + competition = model.competition + choices = ['Seeding' if competition.isKeirin else 'Qualifiers'] + [system.name for system in competition.systems] + choices.append( 'Final Classification' ) + return choices + + def fixShowResults( self ): + model = Model.model + competition = model.competition + + choices = self.getResultChoices() + self.showResults.SetItems( choices ) + + if model.showResults >= len(choices): + model.showResults = 0 + self.showResults.SetSelection( model.showResults ) + + def getHideCols( self, headerNames ): + model = Model.model + toHide = set() + for col, h in enumerate(headerNames): + if h == 'Name' and not getattr(model, 'resultsShowNames', True): + toHide.add( col ) + elif h == 'Team' and not getattr(model, 'resultsShowTeams', True): + toHide.add( col ) + return toHide + def getGrid( self ): return self.grid - + + def getPhase( self, num = None ): + if num is None: + num = self.showResults.GetSelection() + choices = self.getResultChoices() + return choices[num] + def getTitle( self ): - return self.showResults.GetStringSelection() + ' Series Results' + phase = self.getPhase() + title = 'Communiqu\u00E9: {}\n{} {} '.format( + self.communiqueNumber.GetValue(), + phase, + '' if phase.startswith('Final') or phase.startswith('Time') else 'Draw Sheet/Intermediate Results' ) + return title + + def onClickResults( self, event ): + self.commit() + event.Skip() + + def onShowResults( self, event ): + Model.model.showResults = self.showResults.GetSelection() + self.refresh() - def fixCategories( self ): - model = SeriesModel.model - categoryNames = model.getCategoryNamesSortedPublish() - lastSelection = self.categoryChoice.GetStringSelection() - self.categoryChoice.SetItems( categoryNames ) - iCurSelection = 0 - for i, n in enumerate(categoryNames): - if n == lastSelection: - iCurSelection = i - break - self.categoryChoice.SetSelection( iCurSelection ) - self.GetSizer().Layout() - def refresh( self ): - model = SeriesModel.model - scoreByTime = model.scoreByTime - scoreByPercent = model.scoreByPercent - scoreByTrueSkill = model.scoreByTrueSkill - HeaderNames = getHeaderNames() - - model = SeriesModel.model - self.postPublishCmd.SetValue( model.postPublishCmd ) - - with wx.BusyCursor() as wait: - self.raceResults = model.extractAllRaceResults() - - self.fixCategories() + self.fixShowResults() self.grid.ClearGrid() - categoryName = self.categoryChoice.GetStringSelection() - if not categoryName: - return - - pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } - - results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( - categoryName, - self.raceResults, - pointsForRank, - useMostEventsCompleted=model.useMostEventsCompleted, - numPlacesTieBreaker=model.numPlacesTieBreaker, - ) - - results = [rr for rr in results if toFloat(rr[4]) > 0.0] + model = Model.model + competition = model.competition - headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] + self.showNames.SetValue( getattr(model, 'resultsShowNames', True) ) + self.showTeams.SetValue( getattr(model, 'resultsShowTeams', True) ) - Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) - self.setColNames( headerNames ) - #List of columns that can be hidden if empty - emptyCols = ['Name', 'License', 'Machine', 'Team'] + self.communiqueNumber.SetValue( model.communique_number.get(self.getPhase(), '') ) - for row, (name, license, machines, team, points, gap, racePoints) in enumerate(results): - self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) - self.grid.SetCellValue( row, 1, '{}'.format(name or '') ) - self.grid.SetCellBackgroundColour( row, 1, wx.Colour(255,255,0) if name in potentialDuplicates else wx.Colour(255,255,255) ) - self.grid.SetCellValue( row, 2, '{}'.format(license or '') ) - self.grid.SetCellValue( row, 3, '{}'.format(',\n'.join(machines) or '') ) - self.grid.SetCellValue( row, 4, '{}'.format(team or '') ) - self.grid.SetCellValue( row, 5, '{}'.format(points) ) - self.grid.SetCellValue( row, 6, '{}'.format(gap) ) - for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): - self.grid.SetCellValue( row, 7 + q, - '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints - else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus - else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints - else '({})'.format(Utils.ordinal(rRank)) if rRank - else '' - ) - for c in range( len(headerNames) ): - self.grid.SetCellBackgroundColour( row, c, wx.WHITE ) - self.grid.SetCellTextColour( row, c, wx.BLACK ) - #Remove columns from emptyCols as soon as we see some data - if name is not (None or ''): - if 'Name' in emptyCols: emptyCols.remove('Name') - if license is not (None or ''): - if 'License' in emptyCols: emptyCols.remove('License') - if ''.join(machines) is not (None or ''): - if 'Machine' in emptyCols: emptyCols.remove('Machine') - if team is not (None or ''): - if 'Team' in emptyCols: emptyCols.remove('Team') + resultName = self.showResults.GetStringSelection() - if self.sortCol is not None: - def getBracketedNumber( v ): - numberMax = 99999 - if not v: - return numberMax - try: - return int(reNoDigits.sub('', v.split('(')[1])) - except (IndexError, ValueError): - return numberMax - - data = [] - for r in range(self.grid.GetNumberRows()): - rowOrig = [self.grid.GetCellValue(r, c) for c in range(0, self.grid.GetNumberCols())] - rowCmp = rowOrig[:] - rowCmp[0] = int(rowCmp[0]) - rowCmp[4] = Utils.StrToSeconds(rowCmp[4]) - rowCmp[5:] = [getBracketedNumber(v) for v in rowCmp[5:]] - rowCmp.extend( rowOrig ) - data.append( rowCmp ) + if 'Qualifiers' in resultName: + starters = competition.starters - if self.sortCol > 0: - fg = wx.WHITE - bg = wx.Colour(0,100,0) - else: - fg = wx.BLACK - bg = wx.Colour(255,165,0) - - iCol = abs(self.sortCol) - data.sort( key = lambda x: x[iCol], reverse = (self.sortCol < 0) ) - for r, row in enumerate(data): - for c, v in enumerate(row[self.grid.GetNumberCols():]): - self.grid.SetCellValue( r, c, v ) - if c == iCol: - self.grid.SetCellTextColour( r, c, fg ) - self.grid.SetCellBackgroundColour( r, c, bg ) - if c < 4: - halign = wx.ALIGN_LEFT - elif c == 4 or c == 5: - halign = wx.ALIGN_RIGHT - else: - halign = wx.ALIGN_CENTRE - self.grid.SetCellAlignment( r, c, halign, wx.ALIGN_TOP ) - - self.statsLabel.SetLabel( '{} / {}'.format(self.grid.GetNumberRows(), GetModelInfo.GetTotalUniqueParticipants(self.raceResults)) ) - - #Hide the empty columns - for c in range(self.grid.GetNumberCols()): - if self.grid.GetColLabelValue(c) in emptyCols: - self.grid.HideCol(c) - else: - self.grid.ShowCol(c) - - self.grid.AutoSizeColumns( False ) - self.grid.AutoSizeRows( False ) - - self.GetSizer().Layout() - - def onResultsColumnRightClick( self, event ): - # Create and display a popup menu of columns on right-click event - menu = wx.Menu() - menu.SetTitle( 'Show/Hide columns' ) - for c in range(self.grid.GetNumberCols()): - menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c) ) - self.Bind(wx.EVT_MENU, self.onToggleResultsColumn) - if self.grid.IsColShown(c): - menu.Check( menuItem.GetId(), True ) - self.PopupMenu(menu) - menu.Destroy() - - def onToggleResultsColumn( self, event ): - #find the column number - colLabels = [] - for c in range(self.grid.GetNumberCols()): - colLabels.append(self.grid.GetColLabelValue(c)) - label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() - c = colLabels.index(label) - if self.grid.IsColShown(c): - self.grid.HideCol(c) - else: - self.grid.ShowCol(c) - - def onPublishToExcel( self, event ): - model = SeriesModel.model - - scoreByTime = model.scoreByTime - scoreByPercent = model.scoreByPercent - scoreByTrueSkill = model.scoreByTrueSkill - HeaderNames = getHeaderNames() - - if Utils.mainWin: - if not Utils.mainWin.fileName: - Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) - return - - self.raceResults = model.extractAllRaceResults() - - categoryNames = model.getCategoryNamesSortedPublish() - if not categoryNames: - return + self.headerNames = ['Pos', 'Bib', 'Name', 'Team', 'Time'] + hideCols = self.getHideCols( self.headerNames ) + self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] - pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } - - wb = xlwt.Workbook() - - for categoryName in categoryNames: - results, races, potentialDuplicates = GetModelInfo.GetCategoryResults( - categoryName, - self.raceResults, - pointsForRank, - useMostEventsCompleted=model.useMostEventsCompleted, - numPlacesTieBreaker=model.numPlacesTieBreaker, - ) - results = [rr for rr in results if toFloat(rr[4]) > 0.0] + riders = sorted( model.riders, key = Model.Rider.getKeyQualifying(competition.isKeirin) ) + for row, r in enumerate(riders): + if row >= starters or r.status == 'DNQ': + riders[row:] = sorted( riders[row:], key=attrgetter('iSeeding') ) + break + Utils.AdjustGridSize( self.grid, rowsRequired = len(riders), colsRequired = len(self.headerNames) ) + Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) + self.setColNames() + for row, r in enumerate(riders): + if row < starters and r.status != 'DNQ': + pos = '{}'.format(row + 1) + for col in range(self.grid.GetNumberCols()): + self.grid.SetCellBackgroundColour( row, col, wx.WHITE ) + else: + pos = 'DNQ' + for col in range(self.grid.GetNumberCols()): + self.grid.SetCellBackgroundColour( row, col, wx.Colour(200,200,200) ) + + writeCell = WriteCell( self.grid, row ) + for col, value in enumerate([pos,' {}'.format(r.bib), r.full_name, r.team, r.qualifying_time_text]): + if col not in hideCols: + writeCell( value ) + + competitionTime = model.qualifyingCompetitionTime + self.competitionTime.SetLabel( '{}: {}'.format(_('Est. Competition Time'), Utils.formatTime(competitionTime)) + if competitionTime else '' ) + + elif 'Final Classification' in resultName: + self.headerNames = ['Pos', 'Bib', 'Name', 'Team', 'UCIID'] + hideCols = self.getHideCols( self.headerNames ) + self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] - headerNames = HeaderNames + [r[1] for r in races] + results, dnfs, dqs = competition.getResults() + Utils.AdjustGridSize( self.grid, rowsRequired = len(results), colsRequired = len(self.headerNames) ) + Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) + + self.setColNames() + for row, (classification, r) in enumerate(results): + writeCell = WriteCell( self.grid, row ) + if not r: + for col in range(self.grid.GetNumberCols()): + writeCell( '' ) + else: + for col, value in enumerate([classification, r.bib or '', r.full_name, r.team, r.uci_id]): + if col not in hideCols: + writeCell(' {}'.format(value) ) + self.competitionTime.SetLabel( '' ) + else: + # Find the System selected. + for system in competition.systems: + name = system.name + if name == resultName: + break - ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) - wsFit = FitSheetWrapper( ws ) - - fnt = xlwt.Font() - fnt.name = 'Arial' - fnt.bold = True - fnt.height = int(fnt.height * 1.5) + heatsMax = max( event.heatsMax for event in system.events ) + if heatsMax == 1: + self.headerNames = ['Event','Bib','Name','Note','Team',' ','Pos','Bib','Name','Note','Team','Time'] + else: + self.headerNames = ['Event','Bib','Name','Note','Team','H1','H2','H3',' ','Pos','Bib','Name','Note','Team','Time'] + hideCols = self.getHideCols( self.headerNames ) + self.headerNames = [h for c, h in enumerate(self.headerNames) if c not in hideCols] - headerStyle = xlwt.XFStyle() - headerStyle.font = fnt + Utils.AdjustGridSize( self.grid, rowsRequired = len(system.events), colsRequired = len(self.headerNames) ) + Utils.SetGridCellBackgroundColour( self.grid, wx.WHITE ) + + self.setColNames() + state = competition.state - rowCur = 0 - ws.write_merge( rowCur, rowCur, 0, 8, model.name, headerStyle ) - rowCur += 1 - if model.organizer: - ws.write_merge( rowCur, rowCur, 0, 8, 'by {}'.format(model.organizer), headerStyle ) - rowCur += 1 - rowCur += 1 - colCur = 0 - ws.write_merge( rowCur, rowCur, colCur, colCur + 4, categoryName, xlwt.easyxf( - "font: name Arial, bold on;" - ) ); + for row, event in enumerate(system.events): + writeCell = WriteCell( self.grid, row ) - rowCur += 2 - for c, headerName in enumerate(headerNames): - wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) - rowCur += 1 - - for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): - wsFit.write( rowCur, 0, pos+1, numberStyle ) - wsFit.write( rowCur, 1, name, textStyle ) - wsFit.write( rowCur, 2, license, textStyle ) - wsFit.write( rowCur, 3, ', '.join(machines), textStyle ) - wsFit.write( rowCur, 4, team, textStyle ) - wsFit.write( rowCur, 5, points, numberStyle ) - wsFit.write( rowCur, 6, gap, numberStyle ) - for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): - wsFit.write( rowCur, 7 + q, - '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints - else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus - else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints - else '({})'.format(Utils.ordinal(rRank)) if rRank - else '', - centerStyle - ) - rowCur += 1 - - # Add branding at the bottom of the sheet. - style = xlwt.XFStyle() - style.alignment.horz = xlwt.Alignment.HORZ_LEFT - ws.write( rowCur + 2, 0, brandText, style ) - - if Utils.mainWin: - xlfileName = os.path.splitext(Utils.mainWin.fileName)[0] + '.xls' - else: - xlfileName = 'ResultsTest.xls' + writeCell( '{}'.format(row+1) ) + + riders = [state.labels.get(c, None) for c in event.composition] + writeCell( '\n'.join(['{}'.format(rider.bib) if rider and rider.bib else '' for rider in riders]) ) + if getattr(model, 'resultsShowNames', True): + writeCell( '\n'.join([rider.full_name if rider else '' for rider in riders]) ) + writeCell( '\n'.join([competition.getRelegationsWarningsStr(rider.bib, event, True) if rider else '' for rider in riders]) ) + if getattr(model, 'resultsShowTeams', True): + writeCell( '\n'.join([rider.team if rider else '' for rider in riders]) ) + + if heatsMax != 1: + for heat in range(heatsMax): + if event.heatsMax != 1: + writeCell( '\n'.join(event.getHeatPlaces(heat+1)) ) + else: + writeCell( '' ) + + #writeCell( ' ===> ', vert=wx.ALIGN_CENTRE ) + writeCell( ' '.join(['',Arrow,'']), vert=wx.ALIGN_CENTRE ) + + out = [event.winner] + event.others + riders = [state.labels.get(c, None) for c in out] + writeCell( '\n'.join( '{}'.format(i+1) for i in range(len(riders))) ) + writeCell( '\n'.join(['{}'.format(rider.bib if rider.bib else '') if rider else '' for rider in riders]) ) + if getattr(model, 'resultsShowNames', True): + writeCell( '\n'.join([rider.full_name if rider else '' for rider in riders]) ) + writeCell( '\n'.join([competition.getRelegationsWarningsStr(rider.bib, event, False) if rider else '' for rider in riders]) ) + if getattr(model, 'resultsShowTeams', True): + writeCell( '\n'.join([rider.team if rider else '' for rider in riders]) ) + if event.winner in state.labels: + try: + value = '{:.3f}'.format(event.starts[-1].times[1]) + except (KeyError, IndexError, ValueError): + value = '' + writeCell( value ) - try: - wb.save( xlfileName ) - webbrowser.open( xlfileName, new = 2, autoraise = True ) - Utils.MessageOK(self, 'Excel file written to:\n\n {}'.format(xlfileName), 'Excel Write') - self.callPostPublishCmd( xlfileName ) - except IOError: - Utils.MessageOK(self, - 'Cannot write "{}".\n\nCheck if this spreadsheet is open.\nIf so, close it, and try again.'.format(xlfileName), - 'Excel File Error', iconMask=wx.ICON_ERROR ) - - def onPublishToHtml( self, event ): - if Utils.mainWin: - if not Utils.mainWin.fileName: - Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) - return - - htmlfileName = getHtmlFileName() - model = SeriesModel.model - model.postPublishCmd = self.postPublishCmd.GetValue().strip() - - try: - getHtml( htmlfileName ) - webbrowser.open( htmlfileName, new = 2, autoraise = True ) - Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') - except IOError: - Utils.MessageOK(self, - 'Cannot write "%s".\n\nCheck if this file is open.\nIf so, close it, and try again.' % htmlfileName, - 'Html File Error', iconMask=wx.ICON_ERROR ) - self.callPostPublishCmd( htmlfileName ) - - def onPublishToFtp( self, event ): - if Utils.mainWin: - if not Utils.mainWin.fileName: - Utils.MessageOK( self, 'You must save your Series to a file first.', 'Save Series' ) - return + competitionTime = system.competitionTime + self.competitionTime.SetLabel( '{}: {}'.format(_('Est. Competition Time'), Utils.formatTime(competitionTime)) + if competitionTime else '' ) - htmlfileName = getHtmlFileName() - - try: - getHtml( htmlfileName ) - except IOError: - return + self.grid.AutoSizeColumns( False ) + self.grid.AutoSizeRows( False ) - html = io.open( htmlfileName, 'r', encoding='utf-8', newline='' ).read() - with FtpWriteFile.FtpPublishDialog( self, html=html ) as dlg: - dlg.ShowModal() - self.callPostPublishCmd( htmlfileName ) - def commit( self ): - model = SeriesModel.model - postPublishCmd = self.postPublishCmd.GetValue().strip() - if model.postPublishCmd != postPublishCmd: - model.postPublishCmd = postPublishCmd + model = Model.model + phase = self.getPhase() + cn = self.communiqueNumber.GetValue() + if cn != model.communique_number.get(phase, ''): + model.communique_number[phase] = cn model.setChanged() - - def callPostPublishCmd( self, fname ): - self.commit() - postPublishCmd = SeriesModel.model.postPublishCmd - if postPublishCmd and fname: - allFiles = [fname] - if platform.system() == 'Windows': - files = ' '.join('""{}""'.format(f) for f in allFiles) - else: - files = ' '.join('"{}"'.format(f) for f in allFiles) - - if '%*' in postPublishCmd: - cmd = postPublishCmd.replace('%*', files) - else: - cmd = ' '.join( [postPublishCmd, files] ) - - try: - subprocess.check_call( cmd, shell=True ) - except subprocess.CalledProcessError as e: - Utils.MessageOK( self, '{}\n\n {}\n{}: {}'.format('Post Publish Cmd Error', e, 'return code', e.returncode), _('Post Publish Cmd Error') ) - except Exception as e: - Utils.MessageOK( self, '{}\n\n {}'.format('Post Publish Cmd Error', e), 'Post Publish Cmd Error' ) ######################################################################## class ResultsFrame(wx.Frame): + """""" + #---------------------------------------------------------------------- def __init__(self): """Constructor""" @@ -1206,11 +306,6 @@ def __init__(self): #---------------------------------------------------------------------- if __name__ == "__main__": app = wx.App(False) - Utils.disable_stdout_buffering() - model = SeriesModel.model - files = [ - r'C:\Projects\CrossMgr\ParkAvenue2\2013-06-26-Park Ave Bike Camp Arrowhead mtb 4-r1-.cmn', - ] - model.races = [SeriesModel.Race(fileName, model.pointStructures[0]) for fileName in files] + Model.model = SetDefaultData() frame = ResultsFrame() app.MainLoop() From 9e3be6b976fc79be54db771b7a5128fdf86f9c90 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 17:30:56 +0000 Subject: [PATCH 16/53] Hiding of columns in SeriesMgr Results grid --- SeriesMgr/Results.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 585be5ffb..3c4252f04 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -743,6 +743,7 @@ def __init__(self, parent): self.grid.EnableReorderRows( False ) self.grid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) self.grid.Bind( wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.doCellClick ) + self.grid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.onResultsColumnRightClick ) self.sortCol = None self.setColNames(getHeaderNames()) @@ -908,6 +909,8 @@ def refresh( self ): Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) + #List of columns that can be hidden if empty + emptyCols = ['Name', 'License', 'Machine', 'Team'] for row, (name, license, machines, team, points, gap, racePoints) in enumerate(results): self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) @@ -926,10 +929,18 @@ def refresh( self ): else '({})'.format(Utils.ordinal(rRank)) if rRank else '' ) - for c in range( len(headerNames) ): self.grid.SetCellBackgroundColour( row, c, wx.WHITE ) self.grid.SetCellTextColour( row, c, wx.BLACK ) + #Remove columns from emptyCols as soon as we see some data + if name is not (None or ''): + if 'Name' in emptyCols: emptyCols.remove('Name') + if license is not (None or ''): + if 'License' in emptyCols: emptyCols.remove('License') + if ''.join(machines) is not (None or ''): + if 'Machine' in emptyCols: emptyCols.remove('Machine') + if team is not (None or ''): + if 'Team' in emptyCols: emptyCols.remove('Team') if self.sortCol is not None: def getBracketedNumber( v ): @@ -976,11 +987,42 @@ def getBracketedNumber( v ): self.statsLabel.SetLabel( '{} / {}'.format(self.grid.GetNumberRows(), GetModelInfo.GetTotalUniqueParticipants(self.raceResults)) ) + #Hide the empty columns + for c in range(self.grid.GetNumberCols()): + if self.grid.GetColLabelValue(c) in emptyCols: + self.grid.HideCol(c) + else: + self.grid.ShowCol(c) + self.grid.AutoSizeColumns( False ) self.grid.AutoSizeRows( False ) self.GetSizer().Layout() + def onResultsColumnRightClick( self, event ): + # Create and display a popup menu of columns on right-click event + menu = wx.Menu() + menu.SetTitle( 'Show/Hide columns' ) + for c in range(self.grid.GetNumberCols()): + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c) ) + self.Bind(wx.EVT_MENU, self.onToggleResultsColumn) + if self.grid.IsColShown(c): + menu.Check( menuItem.GetId(), True ) + self.PopupMenu(menu) + menu.Destroy() + + def onToggleResultsColumn( self, event ): + #find the column number + colLabels = [] + for c in range(self.grid.GetNumberCols()): + colLabels.append(self.grid.GetColLabelValue(c)) + label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() + c = colLabels.index(label) + if self.grid.IsColShown(c): + self.grid.HideCol(c) + else: + self.grid.ShowCol(c) + def onPublishToExcel( self, event ): model = SeriesModel.model From 8843836b40c67aa423de87ccbc99eab02b3bfa99 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 18:40:21 +0000 Subject: [PATCH 17/53] Move alias settings to sub-tabs --- SeriesMgr/Aliases.py | 151 ++++++++++++------------------------ SeriesMgr/AliasesMachine.py | 2 +- SeriesMgr/AliasesName.py | 121 +++++++++++++++++++++++++++++ SeriesMgr/MainWin.py | 8 +- SeriesMgr/Results.py | 4 +- 5 files changed, 175 insertions(+), 111 deletions(-) create mode 100644 SeriesMgr/AliasesName.py diff --git a/SeriesMgr/Aliases.py b/SeriesMgr/Aliases.py index 8ab16b6bd..7617eae72 100644 --- a/SeriesMgr/Aliases.py +++ b/SeriesMgr/Aliases.py @@ -1,9 +1,9 @@ import wx -import os -import sys -import SeriesModel -import Utils from AliasGrid import AliasGrid +from AliasesName import AliasesName +from AliasesLicense import AliasesLicense +from AliasesMachine import AliasesMachine +from AliasesTeam import AliasesTeam def normalizeText( text ): return ', '.join( [t.strip() for t in text.split(',')][:2] ) @@ -11,111 +11,60 @@ def normalizeText( text ): class Aliases(wx.Panel): def __init__(self, parent): wx.Panel.__init__(self, parent) + self.notebook = wx.Notebook( self ) + self.notebook.Bind( wx.EVT_NOTEBOOK_PAGE_CHANGED, self.onPageChanging ) - text = ( - 'Name Aliases match different name spellings to the same participant.\n' - 'This can be more convenient than editing race results when the same participant has resullts under different names.\n' - '\n' - 'To create a name Alias, first press the "Add Reference Name" button.\n' - 'The first column is the name that will appear in Results.' - 'Then, add Aliases in the next column separated by ";" (semicolons). These are the alternate spellings of the name.\n' - 'SeriesMgr will match all Aliases to the Reference Name in the Results.\n' - '\n' - 'For example, Reference Name="Bell, Robert", Aliases="Bell, Bobby; Bell, Bob". Results for the alternate spellings will appear as "Bell, Robert".\n' - 'Accents and upper/lower case are ignored.\n' - '\n' - 'You can Copy-and-Paste names from the Results without retyping them. Right-click and Copy the name in the Results page,' - 'then Paste the name into the Reference Name or Alias field.\n' - 'Aliases will not be applied until you press the "Refresh" button on the Results screen (or reload).\n' - 'This allows you to configure many Aliases without having to wait for the Results update after each change.\n' - ) + # Add all the pages to the notebook. + self.pages = [] + + def addPage( page, name ): + self.notebook.AddPage( page, name ) + self.pages.append( page ) + + self.attrClassName = [ + [ 'aliasesName', AliasesName, 'Name Aliases' ], + [ 'licenseAliases', AliasesLicense, 'License Aliases' ], + [ 'machineAliases', AliasesMachine, 'Machine Aliases' ], + [ 'teamAliases', AliasesTeam, 'Team Aliases' ], + ] - self.explain = wx.StaticText( self, label=text ) - self.explain.SetFont( wx.Font((0,15), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) ) + for i, (a, c, n) in enumerate(self.attrClassName): + setattr( self, a, c(self.notebook) ) + addPage( getattr(self, a), n ) - self.addButton = wx.Button( self, label='Add Reference Name' ) - self.addButton.Bind( wx.EVT_BUTTON, self.onAddButton ) + self.notebook.SetSelection( 0 ) - headerNames = ('Name (Last, First)','Aliases separated by ";"') - self.itemCur = None - self.grid = AliasGrid( self ) - self.grid.CreateGrid( 0, len(headerNames) ) - self.grid.SetRowLabelSize( 64 ) - for col in range(self.grid.GetNumberCols()): - self.grid.SetColLabelValue( col, headerNames[col] ) - - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(self.explain, 0, flag=wx.ALL, border=4 ) - sizer.Add(self.addButton, 0, flag=wx.ALL, border = 4) - sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.ALL, border = 4) - self.SetSizer(sizer) - - def onAddButton( self, event ): - defaultText = '' + sizer = wx.BoxSizer( wx.VERTICAL ) + sizer.Add( self.notebook, 1, flag=wx.EXPAND ) + self.SetSizer( sizer ) - # Initialize the name from the clipboard. - if wx.TheClipboard.Open(): - do = wx.TextDataObject() - if wx.TheClipboard.GetData(do): - defaultText = do.GetText() - wx.TheClipboard.Close() - - self.grid.AppendRows( 1 ) - self.grid.SetCellValue( self.grid.GetNumberRows()-1, 0, defaultText ) - self.grid.AutoSize() - self.GetSizer().Layout() - self.grid.MakeCellVisible( self.grid.GetNumberRows()-1, 0 ) - - def getName( self, s ): - name = [t.strip() for t in s.split(',')[:2]] - if not name or not any(name): - return None - name.extend( [''] * (2 - len(name)) ) - return tuple( name ) + wx.CallAfter( self.Refresh ) + def refreshCurrentPage( self ): + self.setTitle() + self.callPageRefresh( self.notebook.GetSelection() ) + def refresh( self ): - model = SeriesModel.model + self.refreshCurrentPage() + + def callPageRefresh( self, i ): + try: + self.pages[i].refresh() + except (AttributeError, IndexError) as e: + pass - Utils.AdjustGridSize( self.grid, rowsRequired=len(model.references) ) - for row, (reference, aliases) in enumerate(model.references): - self.grid.SetCellValue( row, 0, '{}, {}'.format(*reference) ) - self.grid.SetCellValue( row, 1, '; '.join('{}, {}'.format(*a) for a in aliases) ) - - self.grid.AutoSize() - self.GetSizer().Layout() - def commit( self ): - references = [] - - self.grid.SaveEditControlValue() - for row in range(self.grid.GetNumberRows()): - reference = self.getName( self.grid.GetCellValue( row, 0 ) ) - if reference: - aliases = [a.strip() for a in self.grid.GetCellValue(row, 1).split(';')] - aliases = [self.getName(a) for a in aliases if a] - aliases = [a for a in aliases if a] - references.append( (reference, aliases) ) + self.callPageCommit( self.notebook.GetSelection() ) + self.setTitle() - references.sort() + def callPageCommit( self, i ): + try: + self.pages[i].commit() + self.setTitle() + except (AttributeError, IndexError) as e: + pass - model = SeriesModel.model - model.setReferences( references ) - -#---------------------------------------------------------------------------- - -class AliasesFrame(wx.Frame): - def __init__(self): - wx.Frame.__init__(self, None, title="Aliases Test", size=(800,600) ) - self.panel = Aliases(self) - self.Show() - -if __name__ == "__main__": - app = wx.App(False) - model = SeriesModel.model - model.setReferences( [ - [('Bell', 'Robert'), [('Bell', 'Bobby'), ('Bell', 'Bob'), ('Bell', 'B')]], - [('Sitarski', 'Stephen'), [('Sitarski', 'Steve'), ('Sitarski', 'Steven')]], - ] ) - frame = AliasesFrame() - frame.panel.refresh() - app.MainLoop() + def onPageChanging( self, event ): + self.callPageCommit( event.GetOldSelection() ) + self.callPageRefresh( event.GetSelection() ) + event.Skip() # Required to properly repaint the screen. diff --git a/SeriesMgr/AliasesMachine.py b/SeriesMgr/AliasesMachine.py index 99cd37db6..0d79416b4 100644 --- a/SeriesMgr/AliasesMachine.py +++ b/SeriesMgr/AliasesMachine.py @@ -92,7 +92,7 @@ def commit( self ): class AliasesMachineFrame(wx.Frame): def __init__(self): - wx.Frame.__init__(self, None, title="AliasesMachine Test", size=(800,600) ) + wx.Frame.__init__(self, None, title="Machine Aliases Test", size=(800,600) ) self.panel = AliasesMachine(self) self.Show() diff --git a/SeriesMgr/AliasesName.py b/SeriesMgr/AliasesName.py new file mode 100644 index 000000000..d6fae4dbd --- /dev/null +++ b/SeriesMgr/AliasesName.py @@ -0,0 +1,121 @@ +import wx +import os +import sys +import SeriesModel +import Utils +from AliasGrid import AliasGrid + +def normalizeText( text ): + return ', '.join( [t.strip() for t in text.split(',')][:2] ) + +class AliasesName(wx.Panel): + def __init__(self, parent): + wx.Panel.__init__(self, parent) + + text = ( + 'Name Aliases match different name spellings to the same participant.\n' + 'This can be more convenient than editing race results when the same participant has resullts under different names.\n' + '\n' + 'To create a name Alias, first press the "Add Reference Name" button.\n' + 'The first column is the name that will appear in Results.' + 'Then, add Aliases in the next column separated by ";" (semicolons). These are the alternate spellings of the name.\n' + 'SeriesMgr will match all Aliases to the Reference Name in the Results.\n' + '\n' + 'For example, Reference Name="Bell, Robert", Aliases="Bell, Bobby; Bell, Bob". Results for the alternate spellings will appear as "Bell, Robert".\n' + 'Accents and upper/lower case are ignored.\n' + '\n' + 'You can Copy-and-Paste names from the Results without retyping them. Right-click and Copy the name in the Results page,' + 'then Paste the name into the Reference Name or Alias field.\n' + 'Aliases will not be applied until you press the "Refresh" button on the Results screen (or reload).\n' + 'This allows you to configure many Aliases without having to wait for the Results update after each change.\n' + ) + + self.explain = wx.StaticText( self, label=text ) + self.explain.SetFont( wx.Font((0,15), wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) ) + + self.addButton = wx.Button( self, label='Add Reference Name' ) + self.addButton.Bind( wx.EVT_BUTTON, self.onAddButton ) + + headerNames = ('Name (Last, First)','Aliases separated by ";"') + self.itemCur = None + self.grid = AliasGrid( self ) + self.grid.CreateGrid( 0, len(headerNames) ) + self.grid.SetRowLabelSize( 64 ) + for col in range(self.grid.GetNumberCols()): + self.grid.SetColLabelValue( col, headerNames[col] ) + + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.explain, 0, flag=wx.ALL, border=4 ) + sizer.Add(self.addButton, 0, flag=wx.ALL, border = 4) + sizer.Add(self.grid, 1, flag=wx.EXPAND|wx.ALL, border = 4) + self.SetSizer(sizer) + + def onAddButton( self, event ): + defaultText = '' + + # Initialize the name from the clipboard. + if wx.TheClipboard.Open(): + do = wx.TextDataObject() + if wx.TheClipboard.GetData(do): + defaultText = do.GetText() + wx.TheClipboard.Close() + + self.grid.AppendRows( 1 ) + self.grid.SetCellValue( self.grid.GetNumberRows()-1, 0, defaultText ) + self.grid.AutoSize() + self.GetSizer().Layout() + self.grid.MakeCellVisible( self.grid.GetNumberRows()-1, 0 ) + + def getName( self, s ): + name = [t.strip() for t in s.split(',')[:2]] + if not name or not any(name): + return None + name.extend( [''] * (2 - len(name)) ) + return tuple( name ) + + def refresh( self ): + model = SeriesModel.model + + Utils.AdjustGridSize( self.grid, rowsRequired=len(model.references) ) + for row, (reference, aliases) in enumerate(model.references): + self.grid.SetCellValue( row, 0, '{}, {}'.format(*reference) ) + self.grid.SetCellValue( row, 1, '; '.join('{}, {}'.format(*a) for a in aliases) ) + + self.grid.AutoSize() + self.GetSizer().Layout() + + def commit( self ): + references = [] + + self.grid.SaveEditControlValue() + for row in range(self.grid.GetNumberRows()): + reference = self.getName( self.grid.GetCellValue( row, 0 ) ) + if reference: + aliases = [a.strip() for a in self.grid.GetCellValue(row, 1).split(';')] + aliases = [self.getName(a) for a in aliases if a] + aliases = [a for a in aliases if a] + references.append( (reference, aliases) ) + + references.sort() + + model = SeriesModel.model + model.setReferences( references ) + +#---------------------------------------------------------------------------- + +class AliasesNameFrame(wx.Frame): + def __init__(self): + wx.Frame.__init__(self, None, title="Name Aliases Test", size=(800,600) ) + self.panel = AliasesName(self) + self.Show() + +if __name__ == "__main__": + app = wx.App(False) + model = SeriesModel.model + model.setReferences( [ + [('Bell', 'Robert'), [('Bell', 'Bobby'), ('Bell', 'Bob'), ('Bell', 'B')]], + [('Sitarski', 'Stephen'), [('Sitarski', 'Steve'), ('Sitarski', 'Steven')]], + ] ) + frame = AliasesNameFrame() + frame.panel.refresh() + app.MainLoop() diff --git a/SeriesMgr/MainWin.py b/SeriesMgr/MainWin.py index faf340b20..53d4ce02c 100644 --- a/SeriesMgr/MainWin.py +++ b/SeriesMgr/MainWin.py @@ -39,9 +39,6 @@ from TeamResults import TeamResults from CategorySequence import CategorySequence from Aliases import Aliases -from AliasesLicense import AliasesLicense -from AliasesMachine import AliasesMachine -from AliasesTeam import AliasesTeam from Options import Options from Errors import Errors from Printing import SeriesMgrPrintout @@ -264,10 +261,7 @@ def addPage( page, name ): [ 'points', Points, 'Scoring Criteria' ], [ 'categorySequence',CategorySequence, 'Category Options' ], [ 'upgrades', Upgrades, 'Upgrades' ], - [ 'aliases', Aliases, 'Name Aliases' ], - [ 'licenseAliases', AliasesLicense, 'License Aliases' ], - [ 'machineAliases', AliasesMachine, 'Machine Aliases' ], - [ 'teamAliases', AliasesTeam, 'Team Aliases' ], + [ 'aliases', Aliases, 'Aliases' ], [ 'options', Options, 'Options' ], [ 'errors', Errors, 'Errors' ], ] diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 3c4252f04..a4feb7b01 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -1004,7 +1004,7 @@ def onResultsColumnRightClick( self, event ): menu = wx.Menu() menu.SetTitle( 'Show/Hide columns' ) for c in range(self.grid.GetNumberCols()): - menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c) ) + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c).strip() ) self.Bind(wx.EVT_MENU, self.onToggleResultsColumn) if self.grid.IsColShown(c): menu.Check( menuItem.GetId(), True ) @@ -1015,7 +1015,7 @@ def onToggleResultsColumn( self, event ): #find the column number colLabels = [] for c in range(self.grid.GetNumberCols()): - colLabels.append(self.grid.GetColLabelValue(c)) + colLabels.append(self.grid.GetColLabelValue(c).strip()) label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() c = colLabels.index(label) if self.grid.IsColShown(c): From 4b6d4fa9ed6bbf46de6a35bfbbeef7556e345795 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 19:04:40 +0000 Subject: [PATCH 18/53] Update quickstart --- SeriesMgr/helptxt/QuickStart.txt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index e5bb936d1..6fd8707b6 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -179,13 +179,18 @@ In this way, SeriesMgr will purge riders from the previous category. This preve Use this screen to specify name aliases to fix misspellings in your given Race Results. This is easier than fixing each race file or spreadsheet. +# License Aliases Screen + +Use this screen to specify license aliases to fix misspellings in your given Race Results. + +# Machine Aliases Screen + +Use this screen to specify machine aliases to fix misspellings in your given Race Results. + # Team Aliases Screen Use this screen to specify team aliases to fix misspellings in your given Race Results. -# License Aliases Screen - -Use this screen to specify license aliases to fix misspellings in your given Race Results. # Results Screen @@ -195,6 +200,8 @@ The publish buttons are fairly self-explanitory. __Publish to HTML with FTP__ w The __Post Publish Cmd__ is a command that is run after the publish. This allows you to post-process the results (for example, copy them somewhere). As per the Windows shell standard, you can use %* to refer to all files created by SeriesMgr in the publish. +You can temporarily hide columns by right-clicking on the headings. Empty Name, Licence, Machine, or Team columns will be hidden by default. + # Team Results Screen Similar to individual results, but for teams. From fa77dfc4cf1e43d22db65dbcf2a43910d6c73749 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:21:46 +0000 Subject: [PATCH 19/53] Update quickstart --- CrossMgrVideo/helptxt/QuickStart.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CrossMgrVideo/helptxt/QuickStart.md b/CrossMgrVideo/helptxt/QuickStart.md index e71efb59d..443607870 100644 --- a/CrossMgrVideo/helptxt/QuickStart.md +++ b/CrossMgrVideo/helptxt/QuickStart.md @@ -211,7 +211,9 @@ Window containing the triggers. * Left-click: shows the photos associated with the trigger. * Right-click: brings up an Edit... and Delete... menu. -* Double-click: brings up the Edit window. This allows you to change the Bib, First, Last, Team, Wave, Race and Note of the trigger. +* Double-click: brings up the Edit window. This allows you to change the Bib, First, Last, Machine, Team, Wave, Race and Note of the trigger. + +Columns can be hidden or shown by right-clicking on the header row. Triggers can be created manually with __Snapshot__, __Auto Capture__, __Capture__ or when connected to CrossMgr, from CrossMgr entries. CrossMgr can be configured to create a trigger for every entry, or just the last entry in the race (see CrossMgr help for details on the Camera setup). From 3a71d5e82c3854ffd309c37ed94153f9dd165518 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 20:31:48 +0000 Subject: [PATCH 20/53] Hiding columns in TeamResults grid --- SeriesMgr/TeamResults.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index c86a3750c..a96a7bdd0 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -644,6 +644,7 @@ def __init__(self, parent): self.grid.EnableReorderRows( False ) self.grid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) self.grid.Bind( wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.doCellClick ) + self.grid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.onResultsColumnRightClick ) self.sortCol = None self.setColNames(getHeaderNames()) @@ -800,6 +801,10 @@ def refresh( self ): headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] + #Show all columns + for c in range(self.grid.GetNumberCols()): + self.grid.ShowCol(c) + Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) @@ -865,6 +870,30 @@ def getBracketedNumber( v ): self.GetSizer().Layout() + def onResultsColumnRightClick( self, event ): + # Create and display a popup menu of columns on right-click event + menu = wx.Menu() + menu.SetTitle( 'Show/Hide columns' ) + for c in range(self.grid.GetNumberCols()): + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.grid.GetColLabelValue(c).strip() ) + self.Bind(wx.EVT_MENU, self.onToggleResultsColumn) + if self.grid.IsColShown(c): + menu.Check( menuItem.GetId(), True ) + self.PopupMenu(menu) + menu.Destroy() + + def onToggleResultsColumn( self, event ): + #find the column number + colLabels = [] + for c in range(self.grid.GetNumberCols()): + colLabels.append(self.grid.GetColLabelValue(c).strip()) + label = event.GetEventObject().FindItemById(event.GetId()).GetItemLabel() + c = colLabels.index(label) + if self.grid.IsColShown(c): + self.grid.HideCol(c) + else: + self.grid.ShowCol(c) + def onPublishToExcel( self, event ): model = SeriesModel.model From 1c1e7f32e73374fa66176bc6c7681bc23c8699be Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 29 Nov 2022 22:26:06 +0000 Subject: [PATCH 21/53] Hiding columns in results label grid --- Results.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Results.py b/Results.py index 53ba84176..6b5d31c49 100644 --- a/Results.py +++ b/Results.py @@ -174,6 +174,7 @@ def __init__( self, parent, id = wx.ID_ANY ): self.labelGrid.DisableDragRowSize() # put a tooltip on the cells in a column self.labelGrid.GetGridWindow().Bind(wx.EVT_MOTION, self.onMouseOver) + # self.lapGrid = ColGrid.ColGrid( self.splitter, style=wx.BORDER_SUNKEN ) self.lapGrid.SetRowLabelSize( 0 ) @@ -195,6 +196,7 @@ def __init__( self, parent, id = wx.ID_ANY ): self.Bind( wx.grid.EVT_GRID_CELL_RIGHT_CLICK, self.doRightClick ) self.lapGrid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) self.labelGrid.Bind( wx.grid.EVT_GRID_LABEL_LEFT_CLICK, self.doLabelClick ) + self.labelGrid.Bind( wx.grid.EVT_GRID_LABEL_RIGHT_CLICK, self.onLabelColumnRightClick ) bs = wx.BoxSizer(wx.VERTICAL) #bs.Add(self.hbs) @@ -386,6 +388,31 @@ def doRightClick( self, event ): self.PopupMenu( menu ) except Exception as e: Utils.writeLog( 'Results:doRightClick: {}'.format(e) ) + + def onLabelColumnRightClick( self, event ): + # Create and display a popup menu of columns on right-click event + menu = wx.Menu() + menu.SetTitle( 'Show/Hide columns' ) + for c in range( self.labelGrid.GetNumberCols() ): + menuItem = menu.AppendCheckItem( wx.ID_ANY, self.labelGrid.GetColLabelValue( c ) ) + self.Bind( wx.EVT_MENU, self.onToggleLabelColumn ) + if self.labelGrid.IsColShown( c ): + menu.Check( menuItem.GetId(), True ) + self.PopupMenu(menu) + menu.Destroy() + + def onToggleLabelColumn( self, event ): + #find the column number + colLabels = [] + for c in range( self.labelGrid.GetNumberCols() ): + colLabels.append( self.labelGrid.GetColLabelValue( c ) ) + label = event.GetEventObject().FindItemById( event.GetId() ).GetItemLabel() + c = colLabels.index( label ) + with Model.LockRace() as race: + if self.labelGrid.IsColShown( c ): + self.labelGrid.HideCol( c ) + else: + self.labelGrid.ShowCol( c ) def OnPopupCorrect( self, event ): CorrectNumber( self, self.entry ) From f95f728b52d7612b3126e37da0dc0aed2fd835e4 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Wed, 30 Nov 2022 20:39:51 +0000 Subject: [PATCH 22/53] Tidy up --- SeriesMgr/Results.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index a4feb7b01..a234f57a3 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -909,8 +909,10 @@ def refresh( self ): Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) - #List of columns that can be hidden if empty - emptyCols = ['Name', 'License', 'Machine', 'Team'] + #These columns start off hidden + hideLicense = True + hideMachine = True + hideTeam = True for row, (name, license, machines, team, points, gap, racePoints) in enumerate(results): self.grid.SetCellValue( row, 0, '{}'.format(row+1) ) @@ -932,15 +934,13 @@ def refresh( self ): for c in range( len(headerNames) ): self.grid.SetCellBackgroundColour( row, c, wx.WHITE ) self.grid.SetCellTextColour( row, c, wx.BLACK ) - #Remove columns from emptyCols as soon as we see some data - if name is not (None or ''): - if 'Name' in emptyCols: emptyCols.remove('Name') + #Unhide label columns as soon as we see some data if license is not (None or ''): - if 'License' in emptyCols: emptyCols.remove('License') + hideLicense = False if ''.join(machines) is not (None or ''): - if 'Machine' in emptyCols: emptyCols.remove('Machine') + hideMachine = False if team is not (None or ''): - if 'Team' in emptyCols: emptyCols.remove('Team') + hideTeam = False if self.sortCol is not None: def getBracketedNumber( v ): @@ -987,12 +987,15 @@ def getBracketedNumber( v ): self.statsLabel.SetLabel( '{} / {}'.format(self.grid.GetNumberRows(), GetModelInfo.GetTotalUniqueParticipants(self.raceResults)) ) - #Hide the empty columns - for c in range(self.grid.GetNumberCols()): - if self.grid.GetColLabelValue(c) in emptyCols: - self.grid.HideCol(c) - else: - self.grid.ShowCol(c) + #Reset column visibility, hide the empty label columns + for c in range(0, self.grid.GetNumberCols()): + self.grid.ShowCol(c) + if hideLicense: + self.grid.HideCol( 2 ) + if hideMachine: + self.grid.HideCol( 3 ) + if hideTeam: + self.grid.HideCol( 4 ) self.grid.AutoSizeColumns( False ) self.grid.AutoSizeRows( False ) From eb71159e72a1931eabbf51f68cb98342fe8e7b37 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Wed, 30 Nov 2022 23:34:59 +0000 Subject: [PATCH 23/53] Make race names editable in SeriesMgr --- SeriesMgr/Races.py | 5 +++-- SeriesMgr/Results.py | 10 +++++----- SeriesMgr/SeriesModel.py | 17 +++++++++++------ SeriesMgr/TeamResults.py | 10 +++++----- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/SeriesMgr/Races.py b/SeriesMgr/Races.py index ce48d6b24..34f931304 100644 --- a/SeriesMgr/Races.py +++ b/SeriesMgr/Races.py @@ -61,7 +61,7 @@ def __init__(self, parent): self.grid.SetColAttr( self.TeamPointsCol, attr ) attr = gridlib.GridCellAttr() - attr.SetReadOnly( True ) + #attr.SetReadOnly( True ) self.grid.SetColAttr( self.RaceCol, attr ) attr = gridlib.GridCellAttr() @@ -212,11 +212,12 @@ def commit( self ): pname = self.grid.GetCellValue( row, self.PointsCol ) pteamname = self.grid.GetCellValue( row, self.TeamPointsCol ) or None grade = self.grid.GetCellValue(row, self.GradeCol).strip().upper()[:1] + raceName = self.grid.GetCellValue( row, self.RaceCol ).strip() if not (grade and ord('A') <= ord(grade) <= ord('Z')): grade = 'A' if not fileName or not pname: continue - raceList.append( (fileName, pname, pteamname, grade) ) + raceList.append( (fileName, pname, pteamname, grade, raceName) ) model = SeriesModel.model model.setRaces( raceList ) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 3ed168656..499d0b320 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -447,7 +447,7 @@ def write( s ): numPlacesTieBreaker=model.numPlacesTieBreaker ) results = [rr for rr in results if toFloat(rr[3]) > 0.0] - headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] + headerNames = HeaderNames + ['{}'.format(r[3].raceName) for r in races] with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): write( '

') @@ -477,9 +477,9 @@ def write( s ): pass if r[2]: with tag(html,'a',dict(href='{}?raceCat={}'.format(r[2], quote(categoryName.encode('utf8')))) ): - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + write( '{}'.format(escape(r[3].raceName).replace('\n', '
\n')) ) else: - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + write( '{}'.format(escape(r[3].raceName).replace('\n', '
\n')) ) if r[0]: write( '
' ) with tag(html, 'span', {'class': 'smallFont'}): @@ -898,7 +898,7 @@ def refresh( self ): results = [rr for rr in results if toFloat(rr[3]) > 0.0] - headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] + headerNames = HeaderNames + ['{}\n{}'.format(r[3].raceName,r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) @@ -1007,7 +1007,7 @@ def onPublishToExcel( self, event ): ) results = [rr for rr in results if toFloat(rr[3]) > 0.0] - headerNames = HeaderNames + [r[1] for r in races] + headerNames = HeaderNames + [r[3].raceName for r in races] ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) wsFit = FitSheetWrapper( ws ) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 8ed971c6b..c515f7903 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -156,14 +156,19 @@ class Race: pureTeam = False # True if the results are by pure teams, that is, no individual results. teamPointStructure = None # If specified, team points will be recomputed from the top individual results. - def __init__( self, fileName, pointStructure, teamPointStructure=None, grade=None ): + def __init__( self, fileName, pointStructure, teamPointStructure=None, grade=None, raceName=None ): self.fileName = fileName self.pointStructure = pointStructure self.teamPointStructure = teamPointStructure self.grade = grade or 'A' + if raceName is None: + self.raceName = RaceNameFromPath( self.fileName ) + else: + self.raceName = raceName def getRaceName( self ): - return RaceNameFromPath( self.fileName ) + #return RaceNameFromPath( self.fileName ) + return self.raceName def postReadFix( self ): if getattr( self, 'fname', None ): @@ -174,7 +179,7 @@ def getFileName( self ): return self.fileName def __repr__( self ): - return ', '.join( '{}={}'.format(a, repr(getattr(self, a))) for a in ['fileName', 'pointStructure'] ) + return ', '.join( '{}={}'.format(a, repr(getattr(self, a))) for a in ['fileName', 'pointStructure', 'raceName'] ) class Category: name = '' @@ -299,7 +304,7 @@ def setPoints( self, pointsList ): self.setChanged() def setRaces( self, raceList ): - if [(r.fileName, r.pointStructure.name, r.teamPointStructure.name if r.teamPointStructure else None, r.grade) for r in self.races] == raceList: + if [(r.fileName, r.pointStructure.name, r.teamPointStructure.name if r.teamPointStructure else None, r.grade, r.raceName) for r in self.races] == raceList: return self.setChanged() @@ -307,7 +312,7 @@ def setRaces( self, raceList ): racesSeen = set() newRaces = [] ps = { p.name:p for p in self.pointStructures } - for fileName, pname, pteamname, grade in raceList: + for fileName, pname, pteamname, grade, racename in raceList: fileName = fileName.strip() if not fileName or fileName in racesSeen: continue @@ -319,7 +324,7 @@ def setRaces( self, raceList ): except KeyError: continue pt = ps.get( pteamname, None ) - newRaces.append( Race(fileName, p, pt, grade) ) + newRaces.append( Race(fileName, p, pt, grade, racename) ) self.races = newRaces for i, r in enumerate(self.races): diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index c86a3750c..60e798c60 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -448,7 +448,7 @@ def write( s ): numPlacesTieBreaker=model.numPlacesTieBreaker ) results = [rr for rr in results if toFloat(rr[1]) > 0.0] - headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] + headerNames = HeaderNames + ['{}'.format(r[3].raceName) for r in races] with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): write( '

') @@ -477,9 +477,9 @@ def write( s ): pass if r[2]: with tag(html,'a',dict(href='{}?raceCat={}'.format(r[2], quote(categoryName.encode('utf8')))) ): - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + write( '{}'.format(escape(r[3].raceName).replace('\n', '
\n')) ) else: - write( '{}'.format(escape(r[1]).replace('\n', '
\n')) ) + write( '{}'.format(escape(r[3].raceName).replace('\n', '
\n')) ) if r[0]: write( '
' ) with tag(html, 'span', {'class': 'smallFont'}): @@ -798,7 +798,7 @@ def refresh( self ): ) results = [rr for rr in results if toFloat(rr[1]) > 0.0] - headerNames = HeaderNames + ['{}\n{}'.format(r[1],r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] + headerNames = HeaderNames + ['{}\n{}'.format(r[3].raceName,r[0].strftime('%Y-%m-%d') if r[0] else '') for r in races] Utils.AdjustGridSize( self.grid, len(results), len(headerNames) ) self.setColNames( headerNames ) @@ -901,7 +901,7 @@ def onPublishToExcel( self, event ): ) results = [rr for rr in results if toFloat(rr[1]) > 0.0] - headerNames = HeaderNames + [r[1] for r in races] + headerNames = HeaderNames + [r[3].raceName for r in races] ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) wsFit = FitSheetWrapper( ws ) From f5761a76a3b3a043ea761e07c810fbbf6900f8d0 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Wed, 30 Nov 2022 23:35:34 +0000 Subject: [PATCH 24/53] Update quickstart --- SeriesMgr/helptxt/QuickStart.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 8ef429118..dd4537363 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -55,6 +55,7 @@ In each row, add the race, either a CrossMgr race file (.cmn) or an Excel sheet You can change the order of races by dragging the row in the grey column to the left of the race name. +* The __Race__ column can be edited to change how the race name is displayed in the results. * The __Grade__ column is used to break ties (more on that later too). * The __Points__ column is the points structure to be used to score that race. This only applies if you are scoring by Points (more on that later). * The __Team Points__ column is the points structure used to score teams for that race. From e8001b157023510242bcb44c82dc3d96e574e56c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:20:46 +0000 Subject: [PATCH 25/53] Suppress hidden columns in HTML output --- SeriesMgr/Results.py | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index a234f57a3..245dbcae8 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -73,7 +73,7 @@ def getHtmlFileName(): defaultPath = os.path.dirname( modelFileName ) return os.path.join( defaultPath, fileName ) -def getHtml( htmlfileName=None, seriesFileName=None ): +def getHtml( htmlfileName=None, hideLicense=False, hideMachine=False, hideTeam=False, seriesFileName=None ): model = SeriesModel.model scoreByTime = model.scoreByTime scoreByPercent = model.scoreByPercent @@ -290,9 +290,13 @@ def write( s ): hr { clear: both; } @media print { + .hide {display:none;} .noprint { display: none; } .title { page-break-after: avoid; } } + +.hide {display:none;} + ''') with tag(html, 'script', dict( type="text/javascript")): @@ -460,8 +464,14 @@ def write( s ): with tag(html, 'tr'): for iHeader, col in enumerate(HeaderNames): colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } - if col in ('License', 'Machine', 'Gap'): + if col in ('License', 'Gap'): colAttr['class'] = 'noprint' + if hideLicense and col in 'License': + colAttr['class'] = 'hide' + if hideMachine and col in 'Machine': + colAttr['class'] = 'hide' + if hideTeam and col in 'Team': + colAttr['class'] = 'hide' with tag(html, 'th', colAttr): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): pass @@ -489,21 +499,33 @@ def write( s ): with tag(html, 'span', {'class': 'smallFont'}): write( 'Top {}'.format(len(r[3].pointStructure)) ) with tag(html, 'tbody'): + if hideLicense: + licenseClass = 'hide' + else: + licenseClass = 'noprint' + if hideMachine: + machineClass = 'hide' + else: + machineClass = 'leftAlign' + if hideTeam: + teamClass = 'hide' + else: + teamClass = 'leftAlign' for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(pos+1) ) with tag(html, 'td'): write( '{}'.format(name or '') ) - with tag(html, 'td', {'class':'noprint'}): + with tag(html, 'td', {'class': licenseClass}): if licenseLinkTemplate and license: with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): write( '{}'.format(license or '') ) else: write( '{}'.format(license or '') ) - with tag(html, 'td', {'class':'noprint'}): - write( '{}'.format(',
'.join(machines) or '') ) - with tag(html, 'td'): + with tag(html, 'td', {'class': machineClass}): + write( '{}'.format(',
'.join(list(filter(None, machines))) or '') ) + with tag(html, 'td', {'class': teamClass}): write( '{}'.format(team or '') ) with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(points or '') ) @@ -919,7 +941,7 @@ def refresh( self ): self.grid.SetCellValue( row, 1, '{}'.format(name or '') ) self.grid.SetCellBackgroundColour( row, 1, wx.Colour(255,255,0) if name in potentialDuplicates else wx.Colour(255,255,255) ) self.grid.SetCellValue( row, 2, '{}'.format(license or '') ) - self.grid.SetCellValue( row, 3, '{}'.format(',\n'.join(machines) or '') ) + self.grid.SetCellValue( row, 3, '{}'.format(',\n'.join(list(filter(None, machines))) or '') ) self.grid.SetCellValue( row, 4, '{}'.format(team or '') ) self.grid.SetCellValue( row, 5, '{}'.format(points) ) self.grid.SetCellValue( row, 6, '{}'.format(gap) ) @@ -1137,9 +1159,10 @@ def onPublishToHtml( self, event ): htmlfileName = getHtmlFileName() model = SeriesModel.model model.postPublishCmd = self.postPublishCmd.GetValue().strip() - + try: - getHtml( htmlfileName ) + #surpress currently hidden license/machine/team columns in the HTML output + getHtml( htmlfileName, not self.grid.IsColShown(2), not self.grid.IsColShown(3), not self.grid.IsColShown(4)) webbrowser.open( htmlfileName, new = 2, autoraise = True ) Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') except IOError: @@ -1157,7 +1180,8 @@ def onPublishToFtp( self, event ): htmlfileName = getHtmlFileName() try: - getHtml( htmlfileName ) + #surpress currently hidden license/machine/team columns in the HTML output + getHtml( htmlfileName, not self.grid.IsColShown(2), not self.grid.IsColShown(3), not self.grid.IsColShown(4)) except IOError: return From 808a5f0f44f5ba91bfcbb7df024685e2fb88f890 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:21:17 +0000 Subject: [PATCH 26/53] Update quickstart --- SeriesMgr/helptxt/QuickStart.txt | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 6fd8707b6..dd4537363 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -47,12 +47,6 @@ If you are interested in Team results, the Team names must be present in the Rac Blank team names, or "Independent", "Ind.", "Ind" are treated separately and are not combined into an overall result. -## Machine Identification - -For sports where the engineering of the bike (or human-powered vehicle, etc.) being ridden is an integral element of the competition. SeriesMgr keeps track of the machines used by each rider over the course of a series, and in the case of multiple machines being used presents a list for each rider in order of frequency. - -You can map variations of spelling etc. to a single name in the Machine Aliases screen. - # Races Screen At the top, add the name of your Series and Organizer. @@ -61,6 +55,7 @@ In each row, add the race, either a CrossMgr race file (.cmn) or an Excel sheet You can change the order of races by dragging the row in the grey column to the left of the race name. +* The __Race__ column can be edited to change how the race name is displayed in the results. * The __Grade__ column is used to break ties (more on that later too). * The __Points__ column is the points structure to be used to score that race. This only applies if you are scoring by Points (more on that later). * The __Team Points__ column is the points structure used to score teams for that race. @@ -179,18 +174,13 @@ In this way, SeriesMgr will purge riders from the previous category. This preve Use this screen to specify name aliases to fix misspellings in your given Race Results. This is easier than fixing each race file or spreadsheet. -# License Aliases Screen - -Use this screen to specify license aliases to fix misspellings in your given Race Results. - -# Machine Aliases Screen - -Use this screen to specify machine aliases to fix misspellings in your given Race Results. - # Team Aliases Screen Use this screen to specify team aliases to fix misspellings in your given Race Results. +# License Aliases Screen + +Use this screen to specify license aliases to fix misspellings in your given Race Results. # Results Screen @@ -200,8 +190,6 @@ The publish buttons are fairly self-explanitory. __Publish to HTML with FTP__ w The __Post Publish Cmd__ is a command that is run after the publish. This allows you to post-process the results (for example, copy them somewhere). As per the Windows shell standard, you can use %* to refer to all files created by SeriesMgr in the publish. -You can temporarily hide columns by right-clicking on the headings. Empty Name, Licence, Machine, or Team columns will be hidden by default. - # Team Results Screen Similar to individual results, but for teams. From 531da82bd7d5fe88bcfcd3f21713beaedb8abc65 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 6 Dec 2022 00:53:32 +0000 Subject: [PATCH 27/53] More efficient approach... --- SeriesMgr/Results.py | 49 +++++++++++++++++--------------------------- 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 245dbcae8..fed239897 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -89,6 +89,12 @@ def getHtml( htmlfileName=None, hideLicense=False, hideMachine=False, hideTeam=F return 'SeriesMgr: No Categories.' HeaderNames = getHeaderNames() + if hideLicense: + HeaderNames.remove('License') + if hideMachine: + HeaderNames.remove('Machine') + if hideTeam: + HeaderNames.remove('Team') pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } if not seriesFileName: @@ -290,13 +296,10 @@ def write( s ): hr { clear: both; } @media print { - .hide {display:none;} .noprint { display: none; } .title { page-break-after: avoid; } } -.hide {display:none;} - ''') with tag(html, 'script', dict( type="text/javascript")): @@ -462,16 +465,11 @@ def write( s ): with tag(html, 'table', {'class': 'results', 'id': 'idTable{}'.format(iTable)} ): with tag(html, 'thead'): with tag(html, 'tr'): + for iHeader, col in enumerate(HeaderNames): colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } if col in ('License', 'Gap'): colAttr['class'] = 'noprint' - if hideLicense and col in 'License': - colAttr['class'] = 'hide' - if hideMachine and col in 'Machine': - colAttr['class'] = 'hide' - if hideTeam and col in 'Team': - colAttr['class'] = 'hide' with tag(html, 'th', colAttr): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): pass @@ -499,34 +497,25 @@ def write( s ): with tag(html, 'span', {'class': 'smallFont'}): write( 'Top {}'.format(len(r[3].pointStructure)) ) with tag(html, 'tbody'): - if hideLicense: - licenseClass = 'hide' - else: - licenseClass = 'noprint' - if hideMachine: - machineClass = 'hide' - else: - machineClass = 'leftAlign' - if hideTeam: - teamClass = 'hide' - else: - teamClass = 'leftAlign' for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(pos+1) ) with tag(html, 'td'): write( '{}'.format(name or '') ) - with tag(html, 'td', {'class': licenseClass}): - if licenseLinkTemplate and license: - with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): + if not hideLicense: + with tag(html, 'td', {'class':'noprint'}): + if licenseLinkTemplate and license: + with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): + write( '{}'.format(license or '') ) + else: write( '{}'.format(license or '') ) - else: - write( '{}'.format(license or '') ) - with tag(html, 'td', {'class': machineClass}): - write( '{}'.format(',
'.join(list(filter(None, machines))) or '') ) - with tag(html, 'td', {'class': teamClass}): - write( '{}'.format(team or '') ) + if not hideMachine: + with tag(html, 'td'): + write( '{}'.format(',
'.join(list(filter(None, machines))) or '') ) + if not hideTeam: + with tag(html, 'td'): + write( '{}'.format(team or '') ) with tag(html, 'td', {'class':'rightAlign'}): write( '{}'.format(points or '') ) with tag(html, 'td', {'class':'rightAlign noprint'}): From 7c2a393019462ec954d1d245909d2cf42cbd62cb Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 6 Dec 2022 01:04:59 +0000 Subject: [PATCH 28/53] Reformat delimited fields on copy --- SeriesMgr/Results.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index fed239897..345fad0f5 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -830,7 +830,7 @@ def copyCellToClipboard( self, r, c ): if wx.TheClipboard.Open(): # Create a wx.TextDataObject do = wx.TextDataObject() - do.SetText( self.grid.GetCellValue(r, c) ) + do.SetText( self.grid.GetCellValue(r, c).replace(',\n', '; ') ) #reformat delimiter for compatibility with aliases # Add the data to the clipboard wx.TheClipboard.SetData(do) From 42f873a5b705a41306a7dea2362d44a619acf3fe Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Wed, 7 Dec 2022 21:44:30 +0000 Subject: [PATCH 29/53] Fix typo --- SeriesMgr/SeriesModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 6ed3428ff..c206f6c49 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -241,7 +241,7 @@ class SeriesModel: referenceTeams = [] aliasLookup = {} aliasLicenseLookup = {} - aliasMachineLoopup = {} + aliasMachineLookup = {} aliasTeamLookup = {} ftpHost = '' From e5e641bbd714e2021c6f053ae8d9588d5de2e5b6 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Thu, 8 Dec 2022 00:47:16 +0000 Subject: [PATCH 30/53] Fix refreshing of Aliases pages --- SeriesMgr/Aliases.py | 3 --- SeriesMgr/FileTrie.py | 9 ++++++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SeriesMgr/Aliases.py b/SeriesMgr/Aliases.py index 7617eae72..7fd01e905 100644 --- a/SeriesMgr/Aliases.py +++ b/SeriesMgr/Aliases.py @@ -41,7 +41,6 @@ def addPage( page, name ): wx.CallAfter( self.Refresh ) def refreshCurrentPage( self ): - self.setTitle() self.callPageRefresh( self.notebook.GetSelection() ) def refresh( self ): @@ -55,12 +54,10 @@ def callPageRefresh( self, i ): def commit( self ): self.callPageCommit( self.notebook.GetSelection() ) - self.setTitle() def callPageCommit( self, i ): try: self.pages[i].commit() - self.setTitle() except (AttributeError, IndexError) as e: pass diff --git a/SeriesMgr/FileTrie.py b/SeriesMgr/FileTrie.py index 1007d4253..eedc3ba24 100644 --- a/SeriesMgr/FileTrie.py +++ b/SeriesMgr/FileTrie.py @@ -42,7 +42,7 @@ def add( self, p ): if c not in node: node[c] = {} node = node[c] - + def best_match( self, p ): path = [] node = self.node @@ -65,14 +65,17 @@ def best_match( self, p ): path.reverse() if re.match( '^[a-zA-Z]:$', path[0] ): path[0] += '\\' - + # *nix paths should start with a '/' + if path[0] == '': + path[0] = '/' return os.path.join( *path ) if __name__ == '__main__': ft = FileTrie() for i in range(5): ft.add( r'c:\Projects\CrossMgr\SeriesMgr\test{}'.format(i) ) - ft.add( r'c:\Projects\CrossMgr\CrossMgrImpinj\test{}'.format(0) ) + ft.add( r'c:\Projects\CrossMgr\CrossMgrImpinj\test{}'.format(0) ) + ft.add( r'/home/Projects/CrossMgr/CrossMgrImpinj/test{}'.format(0) ) print( ft.best_match( '/home/Projects/CrossMgr/SeriesMgr/test2' ) ) print( ft.best_match( '/home/Projects/CrossMgr/CrossMgrImpinj/test0' ) ) print( ft.best_match( 'test4' ) ) From 8a8a0353449117205493b81b2db1f59e1c5c94ab Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Thu, 8 Dec 2022 01:06:05 +0000 Subject: [PATCH 31/53] Undo commit to wrong branch --- SeriesMgr/helptxt/QuickStart.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index dd4537363..8ef429118 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -55,7 +55,6 @@ In each row, add the race, either a CrossMgr race file (.cmn) or an Excel sheet You can change the order of races by dragging the row in the grey column to the left of the race name. -* The __Race__ column can be edited to change how the race name is displayed in the results. * The __Grade__ column is used to break ties (more on that later too). * The __Points__ column is the points structure to be used to score that race. This only applies if you are scoring by Points (more on that later). * The __Team Points__ column is the points structure used to score teams for that race. From ddc4b41197f22af082c0819240e8f50ae9bcbaf3 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:04:33 +0000 Subject: [PATCH 32/53] Hide columns in printout; make Aliases printable --- SeriesMgr/Aliases.py | 3 +++ SeriesMgr/AliasesLicense.py | 2 ++ SeriesMgr/AliasesMachine.py | 2 ++ SeriesMgr/AliasesName.py | 4 +++- SeriesMgr/AliasesTeam.py | 3 +++ SeriesMgr/Results.py | 11 ++++++++++- SeriesMgr/TeamResults.py | 13 +++++++++++-- 7 files changed, 34 insertions(+), 4 deletions(-) diff --git a/SeriesMgr/Aliases.py b/SeriesMgr/Aliases.py index 7fd01e905..18999c941 100644 --- a/SeriesMgr/Aliases.py +++ b/SeriesMgr/Aliases.py @@ -65,3 +65,6 @@ def onPageChanging( self, event ): self.callPageCommit( event.GetOldSelection() ) self.callPageRefresh( event.GetSelection() ) event.Skip() # Required to properly repaint the screen. + + def getGrid( self ): + return self.pages[self.notebook.GetSelection()].getGrid() diff --git a/SeriesMgr/AliasesLicense.py b/SeriesMgr/AliasesLicense.py index 0ac9c2d4a..bdd958eb5 100644 --- a/SeriesMgr/AliasesLicense.py +++ b/SeriesMgr/AliasesLicense.py @@ -88,6 +88,8 @@ def commit( self ): model = SeriesModel.model model.setReferenceLicenses( references ) + def getGrid( self ): + return self.grid #---------------------------------------------------------------------------- class AliasesLicenseFrame(wx.Frame): diff --git a/SeriesMgr/AliasesMachine.py b/SeriesMgr/AliasesMachine.py index 0d79416b4..8dfaacccf 100644 --- a/SeriesMgr/AliasesMachine.py +++ b/SeriesMgr/AliasesMachine.py @@ -88,6 +88,8 @@ def commit( self ): model = SeriesModel.model model.setReferenceMachines( references ) + def getGrid( self ): + return self.grid #---------------------------------------------------------------------------- class AliasesMachineFrame(wx.Frame): diff --git a/SeriesMgr/AliasesName.py b/SeriesMgr/AliasesName.py index d6fae4dbd..9bfee8e8e 100644 --- a/SeriesMgr/AliasesName.py +++ b/SeriesMgr/AliasesName.py @@ -100,7 +100,9 @@ def commit( self ): model = SeriesModel.model model.setReferences( references ) - + + def getGrid( self ): + return self.grid #---------------------------------------------------------------------------- class AliasesNameFrame(wx.Frame): diff --git a/SeriesMgr/AliasesTeam.py b/SeriesMgr/AliasesTeam.py index 553fa7f11..9b7cccb52 100644 --- a/SeriesMgr/AliasesTeam.py +++ b/SeriesMgr/AliasesTeam.py @@ -88,6 +88,9 @@ def commit( self ): model = SeriesModel.model model.setReferenceTeams( references ) + def getGrid( self ): + return self.grid + #---------------------------------------------------------------------------- class AliasesTeamFrame(wx.Frame): diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 345fad0f5..2f1811e1d 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -866,7 +866,16 @@ def setColNames( self, headerNames ): self.grid.SetColAttr( col, attr ) def getGrid( self ): - return self.grid + #Make a copy of the grid without the hidden columns for printing + grid = self.grid + delcols = [] + for c in range(grid.GetNumberCols()): + if not grid.IsColShown( c ): + delcols.append( c ) + delcols.sort(reverse=True) + for c in delcols: + grid.DeleteCols( c ) + return grid def getTitle( self ): return self.showResults.GetStringSelection() + ' Series Results' diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index a96a7bdd0..b56ccfc69 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -744,8 +744,17 @@ def setColNames( self, headerNames ): self.grid.SetColAttr( col, attr ) def getGrid( self ): - return self.grid - + #Make a copy of the grid without the hidden columns for printing + grid = self.grid + delcols = [] + for c in range(grid.GetNumberCols()): + if not grid.IsColShown( c ): + delcols.append( c ) + delcols.sort(reverse=True) + for c in delcols: + grid.DeleteCols( c ) + return grid + def getTitle( self ): return self.showResults.GetStringSelection() + ' Series Results' From 9d0f1ff46303534b6122184d3868ce92f31c6aee Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:53:46 +0000 Subject: [PATCH 33/53] More efficient approach --- SeriesMgr/ExportGrid.py | 4 ++-- SeriesMgr/Results.py | 13 +++---------- SeriesMgr/TeamResults.py | 11 +---------- 3 files changed, 6 insertions(+), 22 deletions(-) diff --git a/SeriesMgr/ExportGrid.py b/SeriesMgr/ExportGrid.py index 48c2416d1..bf2d92c48 100644 --- a/SeriesMgr/ExportGrid.py +++ b/SeriesMgr/ExportGrid.py @@ -51,8 +51,8 @@ class ExportGrid: def __init__( self, title, grid ): self.title = title self.grid = grid - self.colnames = [grid.GetColLabelValue(c) for c in range(grid.GetNumberCols())] - self.data = [ [grid.GetCellValue(r, c) for r in range(grid.GetNumberRows())] for c in range(grid.GetNumberCols()) ] + self.colnames = [grid.GetColLabelValue(c) for c in range(grid.GetNumberCols()) if grid.IsColShown(c)] + self.data = [ [grid.GetCellValue(r, c) for r in range(grid.GetNumberRows())] for c in range(grid.GetNumberCols()) if grid.IsColShown(c)] self.fontName = 'Helvetica' self.fontSize = 16 diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 2f1811e1d..63ca2ef72 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -24,6 +24,8 @@ import subprocess import platform +import copy + reNoDigits = re.compile( '[^0-9]' ) HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Machine', 'Team'] @@ -866,16 +868,7 @@ def setColNames( self, headerNames ): self.grid.SetColAttr( col, attr ) def getGrid( self ): - #Make a copy of the grid without the hidden columns for printing - grid = self.grid - delcols = [] - for c in range(grid.GetNumberCols()): - if not grid.IsColShown( c ): - delcols.append( c ) - delcols.sort(reverse=True) - for c in delcols: - grid.DeleteCols( c ) - return grid + return self.grid def getTitle( self ): return self.showResults.GetStringSelection() + ' Series Results' diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index b56ccfc69..0c8797e06 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -744,16 +744,7 @@ def setColNames( self, headerNames ): self.grid.SetColAttr( col, attr ) def getGrid( self ): - #Make a copy of the grid without the hidden columns for printing - grid = self.grid - delcols = [] - for c in range(grid.GetNumberCols()): - if not grid.IsColShown( c ): - delcols.append( c ) - delcols.sort(reverse=True) - for c in delcols: - grid.DeleteCols( c ) - return grid + return self.grid def getTitle( self ): return self.showResults.GetStringSelection() + ' Series Results' From 3ffbe5013e9bbfd1f99b3232f81a8f5312a04473 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Thu, 8 Dec 2022 23:55:26 +0000 Subject: [PATCH 34/53] tidy up --- SeriesMgr/Results.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 63ca2ef72..345fad0f5 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -24,8 +24,6 @@ import subprocess import platform -import copy - reNoDigits = re.compile( '[^0-9]' ) HeaderNamesTemplate = ['Pos', 'Name', 'License', 'Machine', 'Team'] From bdbf442872fcf8b53f5efd059d54ceaa07a7944c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 10 Dec 2022 22:21:11 +0000 Subject: [PATCH 35/53] Tidy up machines for all scoring criteria --- SeriesMgr/GetModelInfo.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index 95da44a3a..eeda67d6a 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -571,7 +571,9 @@ def TidyMachinesList (riderMachines): leaderEventsCompleted = riderEventsCompleted[leader] riderGap = { r : riderTFinish[r] - leaderTFinish if riderEventsCompleted[r] == leaderEventsCompleted else None for r in riderOrder } riderGap = { r : formatTimeGap(gap) if gap else '' for r, gap in riderGap.items() } - + + # Tidy up the machines list for display + TidyMachinesList( riderMachines ) # List of: # lastName, firstName, license, [list of machines], team, tTotalFinish, [list of (points, position) for each race in series] @@ -640,7 +642,10 @@ def TidyMachinesList (riderMachines): leaderPercentTotal = riderPercentTotal[leader] riderGap = { r : leaderPercentTotal - riderPercentTotal[r] for r in riderOrder } riderGap = { r : percentFormat.format(gap) if gap else '' for r, gap in riderGap.items() } - + + # Tidy up the machines list for display + TidyMachinesList( riderMachines ) + # List of: # lastName, firstName, license, [list of machines], team, totalPercent, [list of (percent, position) for each race in series] categoryResult = [list(riderNameLicense[rider]) + [riderMachines[rider], riderTeam[rider], percentFormat.format(riderPercentTotal[rider]), riderGap[rider]] + [riderResults[rider]] for rider in riderOrder] @@ -716,6 +721,9 @@ def formatRating( rating ): races.reverse() for results in riderResults.values(): results.reverse() + + # Tidy up the machines list for display + TidyMachinesList( riderMachines ) # List of: # lastName, firstName, license, [list of machines], team, points, [list of (points, position) for each race in series] From 06ded65a02b72ef74482919e0fa3f5ec3ad7e659 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sat, 10 Dec 2022 23:49:48 +0000 Subject: [PATCH 36/53] Greatly improved HTML column hiding Using CSS to hide columns. All columns can now be hidden. Sorting javascript no longer broken. Fixed a bug in the Team results to enable sorting. --- SeriesMgr/Results.py | 80 ++++++++++++++++++++-------------------- SeriesMgr/TeamResults.py | 40 +++++++++++++------- 2 files changed, 67 insertions(+), 53 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 345fad0f5..1bfd3d1cf 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -73,7 +73,7 @@ def getHtmlFileName(): defaultPath = os.path.dirname( modelFileName ) return os.path.join( defaultPath, fileName ) -def getHtml( htmlfileName=None, hideLicense=False, hideMachine=False, hideTeam=False, seriesFileName=None ): +def getHtml( htmlfileName=None, hideCols=[], seriesFileName=None): model = SeriesModel.model scoreByTime = model.scoreByTime scoreByPercent = model.scoreByPercent @@ -89,12 +89,7 @@ def getHtml( htmlfileName=None, hideLicense=False, hideMachine=False, hideTeam=F return 'SeriesMgr: No Categories.' HeaderNames = getHeaderNames() - if hideLicense: - HeaderNames.remove('License') - if hideMachine: - HeaderNames.remove('Machine') - if hideTeam: - HeaderNames.remove('Team') + pointsForRank = { r.getFileName(): r.pointStructure for r in model.races } if not seriesFileName: @@ -295,6 +290,10 @@ def write( s ): hr { clear: both; } +.hidden { + display: none; +} + @media print { .noprint { display: none; } .title { page-break-after: avoid; } @@ -354,10 +353,10 @@ def write( s ): if( col == 0 || col == 4 || col == 5 ) { // Pos, Points or Gap cmpFunc = cmpPos; } - else if( col >= 6 ) { // Race Points/Time and Rank + else if( col >= 7 ) { // Race Points/Time and Rank cmpFunc = function( a, b ) { - var x = parseRank( a.cells[6+(col-6)*2+1].textContent.trim() ); - var y = parseRank( b.cells[6+(col-6)*2+1].textContent.trim() ); + var x = parseRank( a.cells[7+(col-7)*2+1].textContent.trim() ); + var y = parseRank( b.cells[7+(col-7)*2+1].textContent.trim() ); return MakeCmpStable( a, b, x - y ); }; } @@ -455,6 +454,7 @@ def write( s ): results = [rr for rr in results if toFloat(rr[4]) > 0.0] headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] + hideRaces = [] with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): write( '

') @@ -465,19 +465,24 @@ def write( s ): with tag(html, 'table', {'class': 'results', 'id': 'idTable{}'.format(iTable)} ): with tag(html, 'thead'): with tag(html, 'tr'): - for iHeader, col in enumerate(HeaderNames): colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } if col in ('License', 'Gap'): colAttr['class'] = 'noprint' + if col in hideCols: + colAttr['class'] = colAttr.get('class', '') + ' hidden' with tag(html, 'th', colAttr): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): pass write( '{}'.format(escape(col).replace('\n', '
\n')) ) for iRace, r in enumerate(races): # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race + hideClass = '' + if r[1] + '\n' in hideCols: + hideRaces.append(iRace) #list of race columns to hide when rendering points rows + hideClass = ' hidden' with tag(html, 'th', { - 'class':'leftBorder centerAlign noprint', + 'class':'leftBorder centerAlign noprint' + hideClass, 'colspan': 2, 'onclick': 'sortTableId({}, {})'.format(iTable, len(HeaderNames) + iRace), } ): @@ -499,52 +504,49 @@ def write( s ): with tag(html, 'tbody'): for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): - with tag(html, 'td', {'class':'rightAlign'}): + with tag(html, 'td', {'class':'rightAlign' + (' hidden' if 'Pos' in hideCols else '')}): write( '{}'.format(pos+1) ) - with tag(html, 'td'): + with tag(html, 'td', {'class':'' + (' hidden' if 'Name' in hideCols else '')}): write( '{}'.format(name or '') ) - if not hideLicense: - with tag(html, 'td', {'class':'noprint'}): - if licenseLinkTemplate and license: - with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): - write( '{}'.format(license or '') ) - else: + with tag(html, 'td', {'class':'noprint' + (' hidden' if 'License' in hideCols else '')}): + if licenseLinkTemplate and license: + with tag(html, 'a', {'href':'{}{}'.format(licenseLinkTemplate, license), 'target':'_blank'}): write( '{}'.format(license or '') ) - if not hideMachine: - with tag(html, 'td'): - write( '{}'.format(',
'.join(list(filter(None, machines))) or '') ) - if not hideTeam: - with tag(html, 'td'): - write( '{}'.format(team or '') ) - with tag(html, 'td', {'class':'rightAlign'}): + else: + write( '{}'.format(license or '') ) + with tag(html, 'td', {'class':'' + (' hidden' if 'Machine' in hideCols else '')}): + write( '{}'.format(',
'.join(list(filter(None, machines))) or '') ) + with tag(html, 'td', {'class':'' + (' hidden' if 'Team' in hideCols else '')}): + write( '{}'.format(team or '') ) + with tag(html, 'td', {'class':'rightAlign' + (' hidden' if 'Points' in hideCols else '')}): write( '{}'.format(points or '') ) - with tag(html, 'td', {'class':'rightAlign noprint'}): + with tag(html, 'td', {'class':'rightAlign noprint' + (' hidden' if 'Gap' in hideCols else '')}): write( '{}'.format(gap or '') ) + iRace = 0 #simple iterator, is there a more pythonesque way to do this? for rPoints, rRank, rPrimePoints, rTimeBonus in racePoints: if rPoints: - with tag(html, 'td', {'class':'leftBorder rightAlign noprint' + (' ignored' if '**' in '{}'.format(rPoints) else '')}): + with tag(html, 'td', {'class':'leftBorder rightAlign noprint' + (' ignored' if '**' in '{}'.format(rPoints) else '') + (' hidden' if iRace in hideRaces else '')}): write( '{}'.format(rPoints).replace('[','').replace(']','').replace(' ', ' ') ) else: - with tag(html, 'td', {'class':'leftBorder noprint'}): + with tag(html, 'td', {'class':'leftBorder noprint' + (' hidden' if iRace in hideRaces else '')}): pass - if rRank: if rPrimePoints: - with tag(html, 'td', {'class':'rank noprint'}): + with tag(html, 'td', {'class':'rank noprint' + (' hidden' if iRace in hideRaces else '')}): write( '({}) +{}'.format(Utils.ordinal(rRank).replace(' ', ' '), rPrimePoints) ) elif rTimeBonus: - with tag(html, 'td', {'class':'rank noprint'}): + with tag(html, 'td', {'class':'rank noprint' + (' hidden' if iRace in hideRaces else '')}): write( '({}) -{}'.format( Utils.ordinal(rRank).replace(' ', ' '), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)), ) else: - with tag(html, 'td', {'class':'rank noprint'}): + with tag(html, 'td', {'class':'rank noprint' + (' hidden' if iRace in hideRaces else '')}): write( '({})'.format(Utils.ordinal(rRank).replace(' ', ' ')) ) else: - with tag(html, 'td', {'class':'noprint'}): + with tag(html, 'td', {'class':'noprint' + (' hidden' if iRace in hideRaces else '')}): pass - + iRace += 1 #----------------------------------------------------------------------------- if considerPrimePointsOrTimeBonus: with tag(html, 'p', {'class':'noprint'}): @@ -1150,8 +1152,8 @@ def onPublishToHtml( self, event ): model.postPublishCmd = self.postPublishCmd.GetValue().strip() try: - #surpress currently hidden license/machine/team columns in the HTML output - getHtml( htmlfileName, not self.grid.IsColShown(2), not self.grid.IsColShown(3), not self.grid.IsColShown(4)) + #surpress currently hidden columns in the HTML output + getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) webbrowser.open( htmlfileName, new = 2, autoraise = True ) Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') except IOError: @@ -1170,7 +1172,7 @@ def onPublishToFtp( self, event ): try: #surpress currently hidden license/machine/team columns in the HTML output - getHtml( htmlfileName, not self.grid.IsColShown(2), not self.grid.IsColShown(3), not self.grid.IsColShown(4)) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) except IOError: return diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index 0c8797e06..a59878dc7 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -79,7 +79,7 @@ def getHtmlFileName(): defaultPath = os.path.dirname( modelFileName ) return os.path.join( defaultPath, fileName ) -def getHtml( htmlfileName=None, seriesFileName=None ): +def getHtml( htmlfileName=None, hideCols=[], seriesFileName=None ): model = SeriesModel.model scoreByPoints = model.scoreByPoints scoreByTime = model.scoreByTime @@ -288,6 +288,10 @@ def write( s ): hr { clear: both; } +.hidden { + display: none; +} + @media print { .noprint { display: none; } .title { page-break-after: avoid; } @@ -343,13 +347,13 @@ def write( s ): }; var cmpFunc; - if( col == 0 || col == 4 || col == 5 ) { // Pos, Points or Gap + if( col == 0 || col == 2 || col == 3 ) { // Pos, Points or Gap cmpFunc = cmpPos; } - else if( col >= 6 ) { // Race Points/Time and Rank + else if( col >= 4 ) { // Race Points/Time and Rank cmpFunc = function( a, b ) { - var x = parseRank( a.cells[6+(col-6)*2+1].textContent.trim() ); - var y = parseRank( b.cells[6+(col-6)*2+1].textContent.trim() ); + var x = parseRank( a.cells[4+(col-4)].textContent.trim() ); + var y = parseRank( b.cells[4+(col-4)].textContent.trim() ); return MakeCmpStable( a, b, x - y ); }; } @@ -449,6 +453,7 @@ def write( s ): results = [rr for rr in results if toFloat(rr[1]) > 0.0] headerNames = HeaderNames + ['{}'.format(r[1]) for r in races] + hideRaces = [] with tag(html, 'div', {'id':'catContent{}'.format(iTable)} ): write( '

') @@ -463,15 +468,21 @@ def write( s ): colAttr = { 'onclick': 'sortTableId({}, {})'.format(iTable, iHeader) } if col in ('Gap',): colAttr['class'] = 'noprint' + if col in hideCols: + colAttr['class'] = colAttr.get('class', '') + ' hidden' with tag(html, 'th', colAttr): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,iHeader)) ): pass write( '{}'.format(escape(col).replace('\n', '
\n')) ) for iRace, r in enumerate(races): # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race + hideClass = '' + if r[1] + '\n' in hideCols: + hideRaces.append(iRace) #list of race columns to hide when rendering points rows + hideClass = ' hidden' with tag(html, 'th', { - 'class':'centerAlign noprint', - #'onclick': 'sortTableId({}, {})'.format(iTable, len(HeaderNames) + iRace), + 'class':'centerAlign noprint' + hideClass, + 'onclick': 'sortTableId({}, {})'.format(iTable, len(HeaderNames) + iRace), #I've re-enabled this and fixed the constants in sortTable() so it actually works- KW } ): with tag(html, 'span', dict(id='idUpDn{}_{}'.format(iTable,len(HeaderNames) + iRace)) ): pass @@ -487,18 +498,19 @@ def write( s ): with tag(html, 'tbody'): for pos, (team, result, gap, rrs) in enumerate(results): with tag(html, 'tr', {'class':'odd'} if pos % 2 == 1 else {} ): - with tag(html, 'td', {'class':'rightAlign'}): + with tag(html, 'td', {'class':'rightAlign' + (' hidden' if 'Pos' in hideCols else '')}): write( '{}'.format(pos+1) ) - with tag(html, 'td'): + with tag(html, 'td', {'class':'' + (' hidden' if 'Team' in hideCols else '')}): write( '{}'.format(team or '') ) - with tag(html, 'td', {'class':'rightAlign'}): + with tag(html, 'td', {'class':'rightAlign' + (' hidden' if 'Points' in hideCols else '')}): write( '{}'.format(result or '') ) - with tag(html, 'td', {'class':'rightAlign noprint'}): + with tag(html, 'td', {'class':'rightAlign noprint' + (' hidden' if 'Gap' in hideCols else '')}): write( '{}'.format(gap or '') ) + iRace = 0 #simple iterator, is there a more pythonesque way to do this? for rt in rrs: - with tag(html, 'td', {'class': 'centerAlign noprint'}): + with tag(html, 'td', {'class': 'centerAlign noprint' + (' hidden' if iRace in hideRaces else '')}): write( formatTeamResults(scoreByPoints, rt) ) - + iRace += 1 #----------------------------------------------------------------------------- if considerPrimePointsOrTimeBonus: with tag(html, 'p', {'class':'noprint'}): @@ -1000,7 +1012,7 @@ def onPublishToHtml( self, event ): model.postPublishCmd = self.postPublishCmd.GetValue().strip() try: - getHtml( htmlfileName ) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) webbrowser.open( htmlfileName, new = 2, autoraise = True ) Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') except IOError: From 5427c61d8d16c80973073ffbf7335d3cb57ca67d Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sun, 11 Dec 2022 00:37:56 +0000 Subject: [PATCH 37/53] Support hiding columns in Excel output --- SeriesMgr/Results.py | 66 ++++++++++++++++++++++++++++------------ SeriesMgr/TeamResults.py | 40 ++++++++++++++++++------ 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 1bfd3d1cf..b5e4cd6ae 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -478,7 +478,7 @@ def write( s ): for iRace, r in enumerate(races): # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race hideClass = '' - if r[1] + '\n' in hideCols: + if r[1] in hideCols: hideRaces.append(iRace) #list of race columns to hide when rendering points rows hideClass = ' hidden' with tag(html, 'th', { @@ -1046,6 +1046,7 @@ def onPublishToExcel( self, event ): scoreByPercent = model.scoreByPercent scoreByTrueSkill = model.scoreByTrueSkill HeaderNames = getHeaderNames() + hideCols = [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)] if Utils.mainWin: if not Utils.mainWin.fileName: @@ -1074,6 +1075,11 @@ def onPublishToExcel( self, event ): headerNames = HeaderNames + [r[1] for r in races] + hideRaces = [] + for iRace, r in enumerate(races): + if r[1] in hideCols: + hideRaces.append(iRace) + ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) wsFit = FitSheetWrapper( ws ) @@ -1098,27 +1104,47 @@ def onPublishToExcel( self, event ): ) ); rowCur += 2 - for c, headerName in enumerate(headerNames): - wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) + c = 0 + for headerName in headerNames: + if headerName not in hideCols: + wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) + c += 1 rowCur += 1 for pos, (name, license, machines, team, points, gap, racePoints) in enumerate(results): - wsFit.write( rowCur, 0, pos+1, numberStyle ) - wsFit.write( rowCur, 1, name, textStyle ) - wsFit.write( rowCur, 2, license, textStyle ) - wsFit.write( rowCur, 3, ', '.join(machines), textStyle ) - wsFit.write( rowCur, 4, team, textStyle ) - wsFit.write( rowCur, 5, points, numberStyle ) - wsFit.write( rowCur, 6, gap, numberStyle ) + c = 0 + if 'Pos' not in hideCols: + wsFit.write( rowCur, c, pos+1, numberStyle ) + c += 1 + if 'Name' not in hideCols: + wsFit.write( rowCur, c, name, textStyle ) + c += 1 + if 'License' not in hideCols: + wsFit.write( rowCur, c, license, textStyle ) + c += 1 + if 'Machine' not in hideCols: + wsFit.write( rowCur, c, ', '.join(machines), textStyle ) + c += 1 + if 'Team' not in hideCols: + wsFit.write( rowCur, c, team, textStyle ) + c += 1 + if 'Points' not in hideCols: + wsFit.write( rowCur, c, points, numberStyle ) + c += 1 + if 'Gap' not in hideCols: + wsFit.write( rowCur, c, gap, numberStyle ) + c += 1 for q, (rPoints, rRank, rPrimePoints, rTimeBonus) in enumerate(racePoints): - wsFit.write( rowCur, 7 + q, - '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints - else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus - else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints - else '({})'.format(Utils.ordinal(rRank)) if rRank - else '', - centerStyle - ) + if q not in hideRaces: + wsFit.write( rowCur, c, + '{} ({}) +{}'.format(rPoints, Utils.ordinal(rRank), rPrimePoints) if rPoints and rPrimePoints + else '{} ({}) -{}'.format(rPoints, Utils.ordinal(rRank), Utils.formatTime(rTimeBonus, twoDigitMinutes=False)) if rPoints and rRank and rTimeBonus + else '{} ({})'.format(rPoints, Utils.ordinal(rRank)) if rPoints + else '({})'.format(Utils.ordinal(rRank)) if rRank + else '', + centerStyle + ) + c += 1 rowCur += 1 # Add branding at the bottom of the sheet. @@ -1153,7 +1179,7 @@ def onPublishToHtml( self, event ): try: #surpress currently hidden columns in the HTML output - getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) webbrowser.open( htmlfileName, new = 2, autoraise = True ) Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') except IOError: @@ -1172,7 +1198,7 @@ def onPublishToFtp( self, event ): try: #surpress currently hidden license/machine/team columns in the HTML output - getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) except IOError: return diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index a59878dc7..23467f6f4 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -477,7 +477,7 @@ def write( s ): for iRace, r in enumerate(races): # r[0] = RaceData, r[1] = RaceName, r[2] = RaceURL, r[3] = Race hideClass = '' - if r[1] + '\n' in hideCols: + if r[1] in hideCols: hideRaces.append(iRace) #list of race columns to hide when rendering points rows hideClass = ' hidden' with tag(html, 'th', { @@ -914,6 +914,7 @@ def onPublishToExcel( self, event ): scoreByPercent = model.scoreByPercent scoreByTrueSkill = model.scoreByTrueSkill HeaderNames = getHeaderNames() + hideCols = [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)] if Utils.mainWin: if not Utils.mainWin.fileName: @@ -944,6 +945,11 @@ def onPublishToExcel( self, event ): headerNames = HeaderNames + [r[1] for r in races] + hideRaces = [] + for iRace, r in enumerate(races): + if r[1] in hideCols: + hideRaces.append(iRace) + ws = wb.add_sheet( re.sub('[:\\/?*\[\]]', ' ', categoryName) ) wsFit = FitSheetWrapper( ws ) @@ -968,17 +974,31 @@ def onPublishToExcel( self, event ): ) ); rowCur += 2 - for c, headerName in enumerate(headerNames): - wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) + c = 0 + for headerName in headerNames: + if headerName not in hideCols: + wsFit.write( rowCur, c, headerName, labelStyle, bold = True ) + c += 1 rowCur += 1 for pos, (team, points, gap, rrs) in enumerate(results): - wsFit.write( rowCur, 0, pos+1, numberStyle ) - wsFit.write( rowCur, 1, team, textStyle ) - wsFit.write( rowCur, 2, points, numberStyle ) - wsFit.write( rowCur, 3, gap, numberStyle ) + c = 0 + if 'Pos' not in hideCols: + wsFit.write( rowCur, c, pos+1, numberStyle ) + c += 1 + if 'Team' not in hideCols: + wsFit.write( rowCur, c, team, textStyle ) + c += 1 + if 'Points' not in hideCols: + wsFit.write( rowCur, c, points, numberStyle ) + c += 1 + if 'Gap' not in hideCols: + wsFit.write( rowCur, c, gap, numberStyle ) + c += 1 for q, rt in enumerate(rrs): - wsFit.write( rowCur, 4 + q, formatTeamResults(scoreByPoints, rt), centerStyle ) + if q not in hideRaces: + wsFit.write( rowCur, c, formatTeamResults(scoreByPoints, rt), centerStyle ) + c += 1 rowCur += 1 # Add branding at the bottom of the sheet. @@ -1012,7 +1032,7 @@ def onPublishToHtml( self, event ): model.postPublishCmd = self.postPublishCmd.GetValue().strip() try: - getHtml( htmlfileName, [self.grid.GetColLabelValue(c) for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) webbrowser.open( htmlfileName, new = 2, autoraise = True ) Utils.MessageOK(self, 'Html file written to:\n\n {}'.format(htmlfileName), 'html Write') except IOError: @@ -1030,7 +1050,7 @@ def onPublishToFtp( self, event ): htmlfileName = getHtmlFileName() try: - getHtml( htmlfileName ) + getHtml( htmlfileName, [self.grid.GetColLabelValue(c).strip() for c in range(self.grid.GetNumberCols()) if not self.grid.IsColShown(c)]) except IOError: return From 94515b8b097a874a03bbe6262056782b5fa2b19a Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Sun, 11 Dec 2022 21:22:40 +0000 Subject: [PATCH 38/53] More robust 'Name' field parsing Excel input If there is only a 'Name' field, and the contents aren't comma-separated, split on the last space in the name if there is one, or failing that, treat the entire thing as LastName. A minor convenience for working with Excel files that have come from somewhere other than CrossMgr. This also allows SeriesMgr to reliably read its own Excel output, where the names aren't always comma-separated. --- SeriesMgr/GetModelInfo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index fd4279564..c9928c67e 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -227,7 +227,12 @@ def ExtractRaceResultsExcel( raceInSeries ): try: info['lastName'], info['firstName'] = name.split(',',1) except: - pass + # Failing that, split on the last space character. + try: + info['firstName'], info['lastName'] = name.rsplit(' ',1) + except: + # If there are no spaces to split on, treat the entire name as lastName. This is fine. + info['lastName'] = name if not info['firstName'] and not info['lastName']: continue From 7093716fad002ff45afd29b95a55ea7bc03b9712 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:23:43 +0000 Subject: [PATCH 39/53] Add SFTP support to SeriesMgr Also fixes a bug where team results would be uploaded to the same filename as individual results. --- SeriesMgr/FtpWriteFile.py | 83 ++++++++++++++++++++++++++++----------- SeriesMgr/SeriesModel.py | 1 + SeriesMgr/TeamResults.py | 2 +- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index a592bdbbf..b6bf90bd7 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -3,6 +3,7 @@ import os import sys import ftplib +import paramiko import datetime import threading import webbrowser @@ -13,25 +14,51 @@ def lineno(): """Returns the current line number in our program.""" return inspect.currentframe().f_back.f_lineno - -def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None ): - ftp = ftplib.FTP( host, timeout = timeout ) - ftp.login( user, passwd ) - if serverPath and serverPath != '.': - ftp.cwd( serverPath ) - fileOpened = False - if file is None: - file = open(fileName, 'rb') - fileOpened = True - ftp.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) - ftp.quit() - if fileOpened: - file.close() -def FtpWriteHtml( html_in ): +class CallCloseOnExit: + def __init__(self, obj): + self.obj = obj + def __enter__(self): + return self.obj + def __exit__(self, exc_type, exc_val, exc_tb): + self.obj.close() + +def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False, sftpPort = 22): + if useSftp: + with CallCloseOnExit(paramiko.SSHClient()) as ssh: + ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) + ssh.load_system_host_keys() + ssh.connect( host, sftpPort, user, passwd ) + + with CallCloseOnExit(ssh.open_sftp()) as sftp: + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + sftp.putfo( + file, + serverPath + os.path.basename(fileName) + ) + if fileOpened: + file.close() + else: + ftp = ftplib.FTP( host, timeout = timeout ) + ftp.login( user, passwd ) + if serverPath and serverPath != '.': + ftp.cwd( serverPath ) + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + ftp.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + ftp.quit() + if fileOpened: + file.close() + +def FtpWriteHtml( html_in, team = False ): Utils.writeLog( 'FtpWriteHtml: called.' ) modelFileName = Utils.getFileName() if Utils.getFileName() else 'Test.smn' - fileName = os.path.basename( os.path.splitext(modelFileName)[0] + '.html' ) + fileName = os.path.basename( os.path.splitext(modelFileName)[0] + ('-Team.html' if team else '.html') ) defaultPath = os.path.dirname( modelFileName ) with open(os.path.join(defaultPath, fileName), 'w') as fp: fp.write( html_in ) @@ -41,6 +68,7 @@ def FtpWriteHtml( html_in ): user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) + useSftp = getattr( model, 'useSftp', False ) with open( os.path.join(defaultPath, fileName), 'rb') as file: try: @@ -49,7 +77,8 @@ def FtpWriteHtml( html_in ): passwd = passwd, serverPath = serverPath, fileName = fileName, - file = file ) + file = file, + useSftp = useSftp) except Exception as e: Utils.writeLog( 'FtpWriteHtml Error: {}'.format(e) ) return e @@ -59,16 +88,19 @@ def FtpWriteHtml( html_in ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] - defaults = ['', '', 'anonymous', 'anonymous@' 'http://'] + fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] + defaults = ['', '', 'anonymous', 'anonymous@', 'http://', False] + team = False - def __init__( self, parent, html, id = wx.ID_ANY ): + def __init__( self, parent, html, team = False, id = wx.ID_ANY ): super().__init__( parent, id, "Ftp Publish Results", style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) self.html = html + self.team = team bs = wx.GridBagSizer(vgap=0, hgap=4) + self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUser = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) @@ -79,8 +111,15 @@ def __init__( self, parent, html, id = wx.ID_ANY ): self.refresh() + + row = 0 border = 8 + + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( wx.StaticText( self, label=_("Ftp Host Name:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) bs.Add( self.ftpHost, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) @@ -130,7 +169,7 @@ def urlPathChanged( self, event = None ): else: if not url.endswith( '/' ): url += '/' - fileName = os.path.basename( os.path.splitext(fileName)[0] + '.html' ) + fileName = os.path.basename( os.path.splitext(fileName)[0] + ( '-Team.html' if self.team else '.html') ) url += fileName self.urlFull.SetLabel( url ) @@ -156,7 +195,7 @@ def setModelAttr( self ): def onOK( self, event ): self.setModelAttr() - e = FtpWriteHtml( self.html ) + e = FtpWriteHtml( self.html, self.team ) if e: Utils.MessageOK( self, 'FTP Publish: {}'.format(e), 'FTP Publish Error' ) else: diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 8ed971c6b..aced150bc 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -247,6 +247,7 @@ class SeriesModel: ftpUser = '' ftpPassword = '' urlPath = '' + useSftp = False @property def scoreByPoints( self ): diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index d4adf426a..adbdbbd47 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -1013,7 +1013,7 @@ def onPublishToFtp( self, event ): return html = io.open( htmlfileName, 'r', encoding='utf-8', newline='' ).read() - with FtpWriteFile.FtpPublishDialog( self, html=html ) as dlg: + with FtpWriteFile.FtpPublishDialog( self, html=html, team=True ) as dlg: dlg.ShowModal() self.callPostPublishCmd( htmlfileName ) From a493282991585c91db6ebb4ea19c2e0ce3786705 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Fri, 23 Dec 2022 22:40:15 +0000 Subject: [PATCH 40/53] Fix bugs in CrossMgr SFTP --- FtpWriteFile.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 553ef79bf..99d041fe1 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -21,10 +21,10 @@ def lineno(): return inspect.currentframe().f_back.f_lineno class CallCloseOnExit: - def __enter__(self, obj): + def __init__(self, obj): self.obj = obj - return obj - + def __enter__(self): + return self.obj def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() @@ -89,13 +89,13 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, username, passwd ) + ssh.connect( host, sftpPort, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: sftp_mkdir_p( sftp, serverPath ) for i, f in enumerate(fname): sftp.put( - filePath, + f, serverPath + '/' + os.path.basename(f), SftpCallback( callback, f, i ) if callback else None ) From 3a3259a36cf17bcd75c5ba7eb4f3ad67728acc5b Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:16:59 +0000 Subject: [PATCH 41/53] Selectable FTP port in CrossMgr --- FtpWriteFile.py | 42 ++++++++++++++++++++++++++++++++++-------- Properties.py | 8 ++++---- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 99d041fe1..774402f71 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -4,6 +4,7 @@ import os import sys import webbrowser +import ftplib import ftputil import paramiko from urllib.parse import quote @@ -55,8 +56,15 @@ def sftp_mkdir_p( sftp, remote_directory ): # Create new dirs starting from the last one that existed. for i in range( i_dir_last, len(dirs_exist) ): sftp.mkdir( '/'.join(dirs_exist[:i+1]) ) - -def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, sftpPort=22, callback=None ): + +class FtpWithPort(ftplib.FTP): + def __init__(self, host, user, passwd, port): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + self.connect(host, port) + self.login(user, passwd) + +def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, callback=None ): if isinstance(fname, str): fname = [fname] @@ -89,7 +97,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, user, passwd ) + ssh.connect( host, port, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: sftp_mkdir_p( sftp, serverPath ) @@ -100,7 +108,7 @@ def FtpWriteFile( host, user='anonymous', passwd='anonymous@', timeout=30, serve SftpCallback( callback, f, i ) if callback else None ) else: - with ftputil.FTPHost( host, user, passwd ) as ftp_host: + with ftputil.FTPHost(host, user, passwd, port, session_factory=FtpWithPort) as ftp_host: ftp_host.makedirs( serverPath, exist_ok=True ) for i, f in enumerate(fname): ftp_host.upload_if_newer( @@ -126,6 +134,7 @@ def FtpUploadFile( fname=None, callback=None ): params = { 'host': getattr(race, 'ftpHost', '').strip().strip('\t'), # Fix cut and paste problems. + 'port': getattr(race, 'ftpPort', 21), 'user': getattr(race, 'ftpUser', ''), 'passwd': getattr(race, 'ftpPassword', ''), 'serverPath': getattr(race, 'ftpPath', ''), @@ -350,8 +359,8 @@ def getTitleTextSize( font ): #------------------------------------------------------------------------------------------------ -ftpFields = ['ftpHost', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] -ftpDefaults = ['', '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] +ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] +ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] def GetFtpPublish( isDialog=True ): ParentClass = wx.Dialog if isDialog else wx.Panel @@ -367,8 +376,11 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs = wx.FlexGridSizer(vgap=4, hgap=4, rows=0, cols=2) fgs.AddGrowableCol( 1, 1 ) - self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) + self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUploadPhotos = wx.CheckBox( self, label=_("Upload Photos to Path") ) self.ftpUploadPhotos.Bind( wx.EVT_CHECKBOX, self.ftpUploadPhotosChanged ) @@ -393,12 +405,18 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): self.cancelBtn = wx.Button( self, wx.ID_CANCEL ) self.Bind( wx.EVT_BUTTON, self.onCancel, self.cancelBtn ) + fgs.Add( wx.StaticText( self, label = _("Protocol")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + fgs.Add( self.useFtp, 1, flag=wx.TOP|wx.ALIGN_LEFT) fgs.AddSpacer( 16 ) - fgs.Add( self.useSftp ) + fgs.Add( self.useSftp, 1, flag=wx.TOP|wx.ALIGN_LEFT) + fgs.Add( wx.StaticText( self, label = _("Host Name")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.ftpHost, 1, flag=wx.TOP|wx.ALIGN_LEFT|wx.EXPAND ) + fgs.Add( wx.StaticText( self, label = _("Port")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + fgs.Add( self.ftpPort, 1, flag=wx.TOP|wx.ALIGN_LEFT|wx.EXPAND ) + fgs.Add( wx.StaticText( self, label = _("Upload files to Path")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.ftpPath, 1, flag=wx.EXPAND ) @@ -459,6 +477,14 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs.AddSpacer( 4 ) self.SetSizerAndFit( fgs ) fgs.Fit( self ) + + def onSelectProtocol( self, event ): + if self.useSftp.GetValue(): + self.useFtp.SetValue(False) + self.ftpPort.SetValue(22) + else: + self.useFtp.SetValue(True) + self.ftpPort.SetValue(21) def onFtpTest( self, event ): self.commit() diff --git a/Properties.py b/Properties.py index 423dfc056..dc1e950d3 100644 --- a/Properties.py +++ b/Properties.py @@ -817,13 +817,13 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): self.ftpCallback = ftpCallback if ftpCallback: - ftpBtn = wx.ToggleButton( self, label=_('Configure Ftp') ) + ftpBtn = wx.ToggleButton( self, label=_('Configure FTP') ) ftpBtn.Bind( wx.EVT_TOGGLEBUTTON, ftpCallback ) else: ftpBtn = None explain = [ - wx.StaticText(self,label=_('Choose File Formats to Publish. Select Ftp option to upload files to Ftp server.')), + wx.StaticText(self,label=_('Choose File Formats to Publish. Select FTP option to upload files to (S)FTP server.')), ] font = explain[0].GetFont() fontUnderline = wx.FFont( font.GetPointSize(), font.GetFamily(), flags=wx.FONTFLAG_BOLD ) @@ -831,7 +831,7 @@ def __init__( self, parent, id=wx.ID_ANY, testCallback=None, ftpCallback=None ): fgs = wx.FlexGridSizer( cols=4, rows=0, hgap=0, vgap=1 ) self.widget = [] - headers = [_('Format'), _('Ftp'), _('Note'), ''] + headers = [_('Format'), _('FTP'), _('Note'), ''] for h in headers: st = wx.StaticText(self, label=h) st.SetFont( fontUnderline ) @@ -1265,7 +1265,7 @@ def __init__( self, parent, id=wx.ID_ANY, addEditButton=True ): ('raceOptionsProperties', RaceOptionsProperties, _('Race Options') ), ('rfidProperties', RfidProperties, _('RFID') ), ('webProperties', WebProperties, _('Web') ), - ('ftpProperties', FtpProperties, _('FTP') ), + ('ftpProperties', FtpProperties, _('(S)FTP') ), ('batchPublishProperties', BatchPublishProperties, _('Batch Publish') ), ('gpxProperties', GPXProperties, _('GPX') ), ('notesProperties', NotesProperties, _('Notes') ), From 1b934346bb13d1047bff3c0fef9a3a87b6bde4a6 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:17:26 +0000 Subject: [PATCH 42/53] Update help --- helptxt/Properties.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helptxt/Properties.md b/helptxt/Properties.md index 89b4fe5b2..da085a4ab 100644 --- a/helptxt/Properties.md +++ b/helptxt/Properties.md @@ -289,7 +289,8 @@ Options for SFTP and FTP upload: Option|Description :-------|:---------- Use SFTP|Check this if you wish to use the SFTP protocol. Otherwise, FTP protocol will be used. -Host Name:Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). +Host Name|Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). +Port|Port of the FTP/SFTP host to upload to (resets to default after switching between FTP and SFTP). Upload files to Path|The directory path on the host you wish to upload the files into. If blank, files will be uploaded into the root directory. User|FTP/SFTP User name Password|FTP/SFTP Password From 7067c361f1cec24ca0fac13b0bf754c6ffb06770 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:18:30 +0000 Subject: [PATCH 43/53] Selectable FTP port in SeriesMgr --- SeriesMgr/FtpWriteFile.py | 52 +++++++++++++++++++++++++++++++-------- SeriesMgr/Results.py | 2 +- SeriesMgr/SeriesModel.py | 1 + SeriesMgr/TeamResults.py | 2 +- 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index b6bf90bd7..07b1c6981 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -23,12 +23,19 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() -def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False, sftpPort = 22): +class FtpWithPort(ftplib.FTP): + def __init__(self, host, user, passwd, port): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + self.connect(host, port) + self.login(user, passwd) + +def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False): if useSftp: with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() - ssh.connect( host, sftpPort, user, passwd ) + ssh.connect( host, port, user, passwd ) with CallCloseOnExit(ssh.open_sftp()) as sftp: fileOpened = False @@ -42,7 +49,8 @@ def FtpWriteFile( host, user = 'anonymous', passwd = 'anonymous@', timeout = 30, if fileOpened: file.close() else: - ftp = ftplib.FTP( host, timeout = timeout ) + ftp = ftplib.FTP() + ftp.connect( host, port, timeout = timeout ) ftp.login( user, passwd ) if serverPath and serverPath != '.': ftp.cwd( serverPath ) @@ -65,6 +73,7 @@ def FtpWriteHtml( html_in, team = False ): model = SeriesModel.model host = getattr( model, 'ftpHost', '' ) + port = getattr( model, 'ftpPort', 21 ) user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) @@ -73,6 +82,7 @@ def FtpWriteHtml( html_in, team = False ): with open( os.path.join(defaultPath, fileName), 'rb') as file: try: FtpWriteFile( host = host, + port = port, user = user, passwd = passwd, serverPath = serverPath, @@ -88,20 +98,23 @@ def FtpWriteHtml( html_in, team = False ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] - defaults = ['', '', 'anonymous', 'anonymous@', 'http://', False] + fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] + defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://', False] team = False def __init__( self, parent, html, team = False, id = wx.ID_ANY ): - super().__init__( parent, id, "Ftp Publish Results", + super().__init__( parent, id, "(S)FTP Publish Results", style=wx.DEFAULT_DIALOG_STYLE|wx.TAB_TRAVERSAL ) self.html = html self.team = team bs = wx.GridBagSizer(vgap=0, hgap=4) - self.useSftp = wx.CheckBox( self, label=_("Use SFTP Protocol (on port 22)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) + self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) self.ftpPath = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpUser = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPassword = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER|wx.TE_PASSWORD, value='' ) @@ -111,19 +124,30 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.refresh() - - row = 0 border = 8 + bs.Add( wx.StaticText( self, label=_("Protocol:")), pos=(row,0), span=(1,1), border = border, + flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + bs.Add( self.useFtp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) row += 1 - bs.Add( wx.StaticText( self, label=_("Ftp Host Name:")), pos=(row,0), span=(1,1), border = border, + bs.Add( wx.StaticText( self, label=_("Host Name:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) bs.Add( self.ftpHost, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + + bs.Add( wx.StaticText( self, label=_("Port:")), pos=(row,0), span=(1,1), border = border, + flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) + bs.Add( self.ftpPort, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + row += 1 bs.Add( wx.StaticText( self, label=_("Path on Host to Write HTML:")), pos=(row,0), span=(1,1), border = border, flag=wx.LEFT|wx.TOP|wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) @@ -161,6 +185,14 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.CentreOnParent(wx.BOTH) self.SetFocus() + def onSelectProtocol( self, event ): + if self.useSftp.GetValue(): + self.useFtp.SetValue(False) + self.ftpPort.SetValue(22) + else: + self.useFtp.SetValue(True) + self.ftpPort.SetValue(21) + def urlPathChanged( self, event = None ): url = self.urlPath.GetValue() fileName = Utils.getFileName() diff --git a/SeriesMgr/Results.py b/SeriesMgr/Results.py index 3ed168656..b798e6e25 100644 --- a/SeriesMgr/Results.py +++ b/SeriesMgr/Results.py @@ -708,7 +708,7 @@ def __init__(self, parent): self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) self.publishToHtml = wx.Button( self, label='Publish to Html' ) self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) - self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) + self.publishToFtp = wx.Button( self, label='Publish to Html with (S)FTP' ) self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) self.publishToExcel = wx.Button( self, label='Publish to Excel' ) self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index aced150bc..7c8e98341 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -243,6 +243,7 @@ class SeriesModel: aliasTeamLookup = {} ftpHost = '' + ftpPort = 21 ftpPath = '' ftpUser = '' ftpPassword = '' diff --git a/SeriesMgr/TeamResults.py b/SeriesMgr/TeamResults.py index adbdbbd47..1570d3c02 100644 --- a/SeriesMgr/TeamResults.py +++ b/SeriesMgr/TeamResults.py @@ -628,7 +628,7 @@ def __init__(self, parent): self.refreshButton.Bind( wx.EVT_BUTTON, self.onRefresh ) self.publishToHtml = wx.Button( self, label='Publish to Html' ) self.publishToHtml.Bind( wx.EVT_BUTTON, self.onPublishToHtml ) - self.publishToFtp = wx.Button( self, label='Publish to Html with FTP' ) + self.publishToFtp = wx.Button( self, label='Publish to Html with (S)FTP' ) self.publishToFtp.Bind( wx.EVT_BUTTON, self.onPublishToFtp ) self.publishToExcel = wx.Button( self, label='Publish to Excel' ) self.publishToExcel.Bind( wx.EVT_BUTTON, self.onPublishToExcel ) From ccbc846eb7b7db1eba4d70bef212e941e01ef047 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 18:18:59 +0000 Subject: [PATCH 44/53] Update Quickstart --- SeriesMgr/helptxt/QuickStart.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/helptxt/QuickStart.txt b/SeriesMgr/helptxt/QuickStart.txt index 8ef429118..8826387c8 100644 --- a/SeriesMgr/helptxt/QuickStart.txt +++ b/SeriesMgr/helptxt/QuickStart.txt @@ -185,7 +185,7 @@ Use this screen to specify license aliases to fix misspellings in your given Rac Shows the individual SeriesResult for each category. The Refresh button will recompute the results and is necessary if you have changed one of the Race files. -The publish buttons are fairly self-explanitory. __Publish to HTML with FTP__ will use the FTP site and password in the last CrossMgr race file. +The publish buttons are fairly self-explanitory. __Publish to HTML with (S)FTP__ will open a dialog where you can enter FTP/SFTP server details. The __Post Publish Cmd__ is a command that is run after the publish. This allows you to post-process the results (for example, copy them somewhere). As per the Windows shell standard, you can use %* to refer to all files created by SeriesMgr in the publish. From d75b8f1922e013a7ff89d6985f083ad513a2db5c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:29:30 +0000 Subject: [PATCH 45/53] Add FTPS support to CrossMgr --- FtpWriteFile.py | 79 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 18 deletions(-) diff --git a/FtpWriteFile.py b/FtpWriteFile.py index 774402f71..2a5c554df 100644 --- a/FtpWriteFile.py +++ b/FtpWriteFile.py @@ -6,6 +6,7 @@ import webbrowser import ftplib import ftputil +import ftputil.session import paramiko from urllib.parse import quote import datetime @@ -57,14 +58,34 @@ def sftp_mkdir_p( sftp, remote_directory ): for i in range( i_dir_last, len(dirs_exist) ): sftp.mkdir( '/'.join(dirs_exist[:i+1]) ) + class FtpWithPort(ftplib.FTP): - def __init__(self, host, user, passwd, port): - #Act like ftplib.FTP's constructor but connect to another port. - ftplib.FTP.__init__(self) - self.connect(host, port) - self.login(user, passwd) - -def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', useSftp=False, callback=None ): + def __init__(self, host, user, passwd, port, timeout): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.login(user, passwd) + +class FtpsWithPort(ftplib.FTP_TLS): + def __init__(self, host, user, passwd, port, timeout): + ftplib.FTP_TLS.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.auth() + self.login(user, passwd) + #Switch to secure data connection. + self.prot_p() + + def ntransfercmd(self, cmd, rest=None): + conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, + server_hostname=self.host, + session=self.sock.session) #reuse ssl session + return conn, size + +def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath='.', fname='', protocol='FTP', callback=None ): if isinstance(fname, str): fname = [fname] @@ -72,7 +93,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, # Normalize serverPath. serverPath = serverPath.strip().replace('\\', '/').rstrip('/') - if not useSftp: + if protocol != 'SFTP': # Stops ftputils from going into an infinite loop by removing leading slashes.. serverPath = serverPath.lstrip('/').lstrip('\\') @@ -93,7 +114,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, return ''' - if useSftp: + if protocol == 'SFTP': with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() @@ -107,8 +128,19 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath + '/' + os.path.basename(f), SftpCallback( callback, f, i ) if callback else None ) - else: - with ftputil.FTPHost(host, user, passwd, port, session_factory=FtpWithPort) as ftp_host: + elif protocol == 'FTPS': + with ftputil.FTPHost(host, user, passwd, port, timeout, session_factory=FtpsWithPort) as ftp_host: + ftp_host.makedirs( serverPath, exist_ok=True ) + for i, f in enumerate(fname): + ftp_host.upload_if_newer( + f, + serverPath + '/' + os.path.basename(f), + (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None + ) + ftp_host.close() + + else: #default to unencrypted FTP + with ftputil.FTPHost(host, user, passwd, port, timeout, session_factory=FtpWithPort) as ftp_host: ftp_host.makedirs( serverPath, exist_ok=True ) for i, f in enumerate(fname): ftp_host.upload_if_newer( @@ -116,6 +148,7 @@ def FtpWriteFile( host, port, user='anonymous', passwd='anonymous@', timeout=30, serverPath + '/' + os.path.basename(f), (lambda byteStr, fname=f, i=i: callback(byteStr, fname, i)) if callback else None ) + ftp_host.close() def FtpIsConfigured(): with Model.LockRace() as race: @@ -138,7 +171,7 @@ def FtpUploadFile( fname=None, callback=None ): 'user': getattr(race, 'ftpUser', ''), 'passwd': getattr(race, 'ftpPassword', ''), 'serverPath': getattr(race, 'ftpPath', ''), - 'useSftp': getattr(race, 'useSftp', False), + 'protocol': getattr(race, 'ftpProtocol', 'FTP'), 'fname': fname or [], 'callback': callback, } @@ -359,8 +392,8 @@ def getTitleTextSize( font ): #------------------------------------------------------------------------------------------------ -ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'useSftp', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] -ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, False, 'http://', False] +ftpFields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpPhotoPath', 'ftpUser', 'ftpPassword', 'ftpUploadDuringRace', 'urlPath', 'ftpUploadPhotos'] +ftpDefaults = ['', 21, '', '', 'anonymous', 'anonymous@', False, 'http://', False] def GetFtpPublish( isDialog=True ): ParentClass = wx.Dialog if isDialog else wx.Panel @@ -373,11 +406,14 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): else: super().__init__( parent, id ) + self.protocol = 'FTP' + fgs = wx.FlexGridSizer(vgap=4, hgap=4, rows=0, cols=2) fgs.AddGrowableCol( 1, 1 ) - self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) - self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP (unencrypted)"), style = wx.RB_GROUP ) + self.useFtps = wx.RadioButton( self, label=_("FTPS (FTP with TLS)") ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH file transfer)") ) self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) @@ -408,6 +444,8 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): fgs.Add( wx.StaticText( self, label = _("Protocol")), flag=wx.ALIGN_RIGHT|wx.ALIGN_CENTRE_VERTICAL ) fgs.Add( self.useFtp, 1, flag=wx.TOP|wx.ALIGN_LEFT) fgs.AddSpacer( 16 ) + fgs.Add( self.useFtps, 1, flag=wx.TOP|wx.ALIGN_LEFT) + fgs.AddSpacer( 16 ) fgs.Add( self.useSftp, 1, flag=wx.TOP|wx.ALIGN_LEFT) @@ -480,10 +518,13 @@ def __init__( self, parent, id=wx.ID_ANY, uploadNowButton=True ): def onSelectProtocol( self, event ): if self.useSftp.GetValue(): - self.useFtp.SetValue(False) + self.protocol = 'SFTP' self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) else: - self.useFtp.SetValue(True) + self.protocol = 'FTP' self.ftpPort.SetValue(21) def onFtpTest( self, event ): @@ -557,6 +598,7 @@ def refresh( self ): else: for f, v in zip(ftpFields, ftpDefaults): getattr(self, f).SetValue( getattr(race, f, v) ) + self.protocol = getattr(race, 'ftpProtocol', '') self.urlPathChanged() self.ftpUploadPhotosChanged() @@ -566,6 +608,7 @@ def commit( self ): if race: for f in ftpFields: setattr( race, f, getattr(self, f).GetValue() ) + setattr( race, 'ftpProtocol', self.protocol) race.urlFull = self.urlFull.GetLabel() race.setChanged() From 8d502e118fb9e02e864784db78f153f1971babf7 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:30:02 +0000 Subject: [PATCH 46/53] Update help --- helptxt/Properties.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helptxt/Properties.md b/helptxt/Properties.md index da085a4ab..cec628582 100644 --- a/helptxt/Properties.md +++ b/helptxt/Properties.md @@ -288,7 +288,7 @@ Options for SFTP and FTP upload: Option|Description :-------|:---------- -Use SFTP|Check this if you wish to use the SFTP protocol. Otherwise, FTP protocol will be used. +Protocol|Select one of FTP, FTPS (FTP with SSL encryption) or SFTP (SSH File Transfer Protocol) Host Name|Name of the FTP/SFTP host to upload to. In SFTP, CrossMgr also loads hosts from the user's local hosts file (as used by OpenSSH). Port|Port of the FTP/SFTP host to upload to (resets to default after switching between FTP and SFTP). Upload files to Path|The directory path on the host you wish to upload the files into. If blank, files will be uploaded into the root directory. From 499b1099188ed17004a2f5e7434ac924316b768f Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:30:40 +0000 Subject: [PATCH 47/53] Add FTPS support to SeriesMgr --- SeriesMgr/FtpWriteFile.py | 85 +++++++++++++++++++++++++++++---------- 1 file changed, 64 insertions(+), 21 deletions(-) diff --git a/SeriesMgr/FtpWriteFile.py b/SeriesMgr/FtpWriteFile.py index 07b1c6981..c60f0a693 100644 --- a/SeriesMgr/FtpWriteFile.py +++ b/SeriesMgr/FtpWriteFile.py @@ -22,16 +22,35 @@ def __enter__(self): return self.obj def __exit__(self, exc_type, exc_val, exc_tb): self.obj.close() - + class FtpWithPort(ftplib.FTP): - def __init__(self, host, user, passwd, port): - #Act like ftplib.FTP's constructor but connect to another port. - ftplib.FTP.__init__(self) - self.connect(host, port) - self.login(user, passwd) + def __init__(self, host, user, passwd, port, timeout): + #Act like ftplib.FTP's constructor but connect to another port. + ftplib.FTP.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.login(user, passwd) + +class FtpsWithPort(ftplib.FTP_TLS): + def __init__(self, host, user, passwd, port, timeout): + ftplib.FTP_TLS.__init__(self) + #self.set_debuglevel(2) + self.connect(host, port, timeout) + self.auth() + self.login(user, passwd) + #Switch to secure data connection. + self.prot_p() + + def ntransfercmd(self, cmd, rest=None): + conn, size = ftplib.FTP.ntransfercmd(self, cmd, rest) + if self._prot_p: + conn = self.context.wrap_socket(conn, + server_hostname=self.host, + session=self.sock.session) # reuse ssl session + return conn, size -def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, useSftp = False): - if useSftp: +def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout = 30, serverPath = '.', fileName = '', file = None, protocol='FTP'): + if protocol == 'SFTP': with CallCloseOnExit(paramiko.SSHClient()) as ssh: ssh.set_missing_host_key_policy( paramiko.AutoAddPolicy() ) ssh.load_system_host_keys() @@ -48,10 +67,20 @@ def FtpWriteFile( host, port, user = 'anonymous', passwd = 'anonymous@', timeout ) if fileOpened: file.close() - else: - ftp = ftplib.FTP() - ftp.connect( host, port, timeout = timeout ) - ftp.login( user, passwd ) + elif protocol == 'FTPS': + ftps = FtpsWithPort( host, user, passwd, port, timeout) + if serverPath and serverPath != '.': + ftps.cwd( serverPath ) + fileOpened = False + if file is None: + file = open(fileName, 'rb') + fileOpened = True + ftps.storbinary( 'STOR {}'.format(os.path.basename(fileName)), file ) + ftps.quit() + if fileOpened: + file.close() + else: #default to unencrypted FTP + ftp = FtpWithPort( host, user, passwd, port, timeout) if serverPath and serverPath != '.': ftp.cwd( serverPath ) fileOpened = False @@ -77,7 +106,7 @@ def FtpWriteHtml( html_in, team = False ): user = getattr( model, 'ftpUser', '' ) passwd = getattr( model, 'ftpPassword', '' ) serverPath = getattr( model, 'ftpPath', '' ) - useSftp = getattr( model, 'useSftp', False ) + protocol = getattr( model, 'ftpProtocol', 'FTP' ) with open( os.path.join(defaultPath, fileName), 'rb') as file: try: @@ -88,7 +117,7 @@ def FtpWriteHtml( html_in, team = False ): serverPath = serverPath, fileName = fileName, file = file, - useSftp = useSftp) + protocol = protocol) except Exception as e: Utils.writeLog( 'FtpWriteHtml Error: {}'.format(e) ) return e @@ -98,8 +127,8 @@ def FtpWriteHtml( html_in, team = False ): #------------------------------------------------------------------------------------------------ class FtpPublishDialog( wx.Dialog ): - fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath', 'useSftp'] - defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://', False] + fields = ['ftpHost', 'ftpPort', 'ftpPath', 'ftpUser', 'ftpPassword', 'urlPath'] + defaults = ['', 21, '', 'anonymous', 'anonymous@', 'http://'] team = False def __init__( self, parent, html, team = False, id = wx.ID_ANY ): @@ -108,10 +137,13 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): self.html = html self.team = team + self.protocol = 'FTP' + bs = wx.GridBagSizer(vgap=0, hgap=4) - self.useFtp = wx.RadioButton( self, label=_("FTP"), style = wx.RB_GROUP ) - self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH)") ) + self.useFtp = wx.RadioButton( self, label=_("FTP (unencrypted)"), style = wx.RB_GROUP ) + self.useFtps = wx.RadioButton( self, label=_("FTPS (FTP with TLS)") ) + self.useSftp = wx.RadioButton( self, label=_("SFTP (SSH file transfer)") ) self.Bind( wx.EVT_RADIOBUTTON,self.onSelectProtocol ) self.ftpHost = wx.TextCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER, value='' ) self.ftpPort = wx.lib.intctrl.IntCtrl( self, size=(256,-1), style=wx.TE_PROCESS_ENTER ) @@ -133,6 +165,10 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): row += 1 + bs.Add( self.useFtps, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) + + row += 1 + bs.Add( self.useSftp, pos=(row,1), span=(1,1), border = border, flag=wx.RIGHT|wx.TOP|wx.ALIGN_LEFT ) row += 1 @@ -187,10 +223,13 @@ def __init__( self, parent, html, team = False, id = wx.ID_ANY ): def onSelectProtocol( self, event ): if self.useSftp.GetValue(): - self.useFtp.SetValue(False) + self.protocol = 'SFTP' self.ftpPort.SetValue(22) + elif self.useFtps.GetValue(): + self.protocol = 'FTPS' + self.ftpPort.SetValue(21) else: - self.useFtp.SetValue(True) + self.protocol = 'FTP' self.ftpPort.SetValue(21) def urlPathChanged( self, event = None ): @@ -213,6 +252,7 @@ def refresh( self ): else: for f, v in zip(FtpPublishDialog.fields, FtpPublishDialog.defaults): getattr(self, f).SetValue( getattr(model, f, v) ) + self.protocol = getattr(model, 'ftpProtocol', '') self.urlPathChanged() def setModelAttr( self ): @@ -220,9 +260,12 @@ def setModelAttr( self ): model = SeriesModel.model for f in FtpPublishDialog.fields: value = getattr(self, f).GetValue() - if getattr(model, f, None) != value: + if getattr( model, f, None ) != value: setattr( model, f, value ) model.setChanged() + if getattr( model, 'ftpProtocol', None ) != self.protocol: + setattr( model, 'ftpProtocol', self.protocol) + model.setChanged() model.urlFull = self.urlFull.GetLabel() def onOK( self, event ): From d65d90a58c995cee679877991253279fbefdc37c Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Tue, 27 Dec 2022 22:35:14 +0000 Subject: [PATCH 48/53] Add files via upload --- SeriesMgr/SeriesModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SeriesMgr/SeriesModel.py b/SeriesMgr/SeriesModel.py index 7c8e98341..2a3a20c47 100644 --- a/SeriesMgr/SeriesModel.py +++ b/SeriesMgr/SeriesModel.py @@ -247,8 +247,8 @@ class SeriesModel: ftpPath = '' ftpUser = '' ftpPassword = '' + ftpProtocol = '' urlPath = '' - useSftp = False @property def scoreByPoints( self ): From f841001f5052c9cba735bb26d8a19e86fa7e0496 Mon Sep 17 00:00:00 2001 From: Kim Wall <30846798+kimble4@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:00:48 +0000 Subject: [PATCH 49/53] Add files via upload --- SeriesMgr/GetModelInfo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SeriesMgr/GetModelInfo.py b/SeriesMgr/GetModelInfo.py index feefe6f9e..6ead636ad 100644 --- a/SeriesMgr/GetModelInfo.py +++ b/SeriesMgr/GetModelInfo.py @@ -75,8 +75,6 @@ def safe_upper( f ): return f class RaceResult: - rankDNF = 999999 - def __init__( self, firstName, lastName, license, machine, team, categoryName, raceName, raceDate, raceFileName, bib, rank, raceOrganizer, raceURL=None, raceInSeries=None, tFinish=None, tProjected=None, primePoints=0, timeBonus=0, laps=1, pointsInput=None ): self.firstName = str(firstName or '') From 17b18a81513d59ee94e33d92ff3394170581ebfd Mon Sep 17 00:00:00 2001 From: kimble4 Date: Mon, 3 Apr 2023 22:19:14 +0100 Subject: [PATCH 50/53] Fix accidentally commented line --- CrossMgrVideo/MainWin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CrossMgrVideo/MainWin.py b/CrossMgrVideo/MainWin.py index d0f1702e1..0a86586a8 100644 --- a/CrossMgrVideo/MainWin.py +++ b/CrossMgrVideo/MainWin.py @@ -1314,7 +1314,7 @@ def updateSnapshot( self, t, f ): 'bib': self.snapshotCount, 'first_name': '', 'last_name': 'Snapshot', -# 'machine': '', + 'machine': '', 'team': '', 'wave': '', 'race_name': '', From d68c5a4330f14362d0fcf2448919174d2c7903f6 Mon Sep 17 00:00:00 2001 From: kimble4 Date: Wed, 5 Apr 2023 18:24:21 +0100 Subject: [PATCH 51/53] Add machine data to photo trigger --- SendPhotoRequests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SendPhotoRequests.py b/SendPhotoRequests.py index 2794f904f..63417c0f3 100644 --- a/SendPhotoRequests.py +++ b/SendPhotoRequests.py @@ -41,7 +41,7 @@ def getRequest( race, dirName, bib, raceSeconds, externalInfo ): info['wave'] = category.fullname if category else '' try: riderInfo = externalInfo[bib] - for a, b in (('firstName', 'FirstName'), ('lastName','LastName'), ('team', 'Team')): + for a, b in (('firstName', 'FirstName'), ('lastName','LastName'), ('machine', 'Machine'), ('team', 'Team')): try: info[a] = riderInfo[b] except KeyError: From 94544bed8c3824a4f9ee9b43bd67a3edc499935b Mon Sep 17 00:00:00 2001 From: kimble4 Date: Thu, 6 Apr 2023 01:03:34 +0100 Subject: [PATCH 52/53] Fix order of infofields in HTML payload --- MainWin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/MainWin.py b/MainWin.py index d4f3bd783..26f3507a9 100644 --- a/MainWin.py +++ b/MainWin.py @@ -1862,8 +1862,8 @@ def getBasePayload( self, publishOnly=True ): payload = {} payload['raceName'] = os.path.basename(self.fileName or '')[:-4] - iTeam = ReportFields.index('Team') - payload['infoFields'] = ReportFields[:iTeam] + ['Name'] + ReportFields[iTeam:] + iMachine = ReportFields.index('Machine') + payload['infoFields'] = ReportFields[:iMachine] + ['Name'] + ReportFields[iMachine:] payload['organizer'] = getattr(race, 'organizer', '') payload['reverseDirection'] = getattr(race, 'reverseDirection', False) From b2d874bd82b6292d0804ba77a7f0c59fc9cf84bf Mon Sep 17 00:00:00 2001 From: kimble4 Date: Thu, 13 Apr 2023 16:32:54 +0100 Subject: [PATCH 53/53] Add machine field to CrossMgrVideo web server output --- CrossMgrVideo/CrossMgrVideoHtml/main.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CrossMgrVideo/CrossMgrVideoHtml/main.html b/CrossMgrVideo/CrossMgrVideoHtml/main.html index 5fb3f8010..09481d01b 100644 --- a/CrossMgrVideo/CrossMgrVideoHtml/main.html +++ b/CrossMgrVideo/CrossMgrVideoHtml/main.html @@ -533,9 +533,9 @@ trigger_div.scrollTop = trigger_div.scrollHeight; } -const headers = ['', 'Time', 'Bib', 'Last', 'First', 'Team', 'Wave', 'Race', 'Frames', 'View', 'Note']; -const fields = ['close', 'ts', 'bib', 'last_name', 'first_name', 'team', 'wave', 'race_name', 'frames', 'view', 'note']; -const numeric = [false, false, true, false, false, false, false, false, true, true, false]; +const headers = ['', 'Time', 'Bib', 'Last', 'First', 'Machine', 'Team', 'Wave', 'Race', 'Frames', 'View', 'Note']; +const fields = ['close', 'ts', 'bib', 'last_name', 'first_name', 'machine', 'team', 'wave', 'race_name', 'frames', 'view', 'note']; +const numeric = [false, false, true, false, false, false, false, false, false, true, true, false]; function showTriggers( triggers ) { playStop();