Exemplo n.º 1
0
class Categories(wx.Panel):
    CategoryTypeChoices = [
        _('Start Wave'), u'    ' + _('Component'),
        _('Custom')
    ]
    DistanceTypeChoices = [_('Lap'), _('Race')]

    def __init__(self, parent, id=wx.ID_ANY):
        wx.Panel.__init__(self, parent, id)

        self.state = RaceInputState()

        vs = wx.BoxSizer(wx.VERTICAL)

        self.ignoreColour = wx.Colour(80, 80, 80)
        self.inactiveColour = wx.Colour(200, 200, 200)

        border = 4
        flag = wx.ALL

        hs = wx.BoxSizer(wx.HORIZONTAL)

        self.activateAllButton = wx.Button(self,
                                           label=_('Activate All'),
                                           style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onActivateAll, self.activateAllButton)
        hs.Add(self.activateAllButton, 0, border=border, flag=flag)

        hs.AddSpacer(6)

        self.newCategoryButton = wx.Button(self,
                                           label=_('New'),
                                           style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onNewCategory, self.newCategoryButton)
        hs.Add(self.newCategoryButton, 0, border=border, flag=flag)

        self.delCategoryButton = wx.Button(self,
                                           label=_('Delete'),
                                           style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onDelCategory, self.delCategoryButton)
        hs.Add(self.delCategoryButton,
               0,
               border=border,
               flag=(flag & ~wx.LEFT))

        hs.AddSpacer(6)

        self.upCategoryButton = wx.Button(self,
                                          label=u'\u2191',
                                          style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onUpCategory, self.upCategoryButton)
        hs.Add(self.upCategoryButton, 0, border=border, flag=flag)

        self.downCategoryButton = wx.Button(self,
                                            label=u'\u2193',
                                            style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onDownCategory, self.downCategoryButton)
        hs.Add(self.downCategoryButton,
               0,
               border=border,
               flag=(flag & ~wx.LEFT))

        hs.AddSpacer(6)

        self.setGpxDistanceButton = wx.Button(self,
                                              label=_('Set Gpx Distance'),
                                              style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onSetGpxDistance,
                  self.setGpxDistanceButton)
        hs.Add(self.setGpxDistanceButton, 0, border=border, flag=flag)

        hs.AddSpacer(6)

        self.addExceptionsButton = wx.Button(self,
                                             label=_('Bib Exceptions'),
                                             style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onAddExceptions,
                  self.addExceptionsButton)
        hs.Add(self.addExceptionsButton, 0, border=border, flag=flag)

        hs.AddSpacer(6)
        '''
		self.updateStartWaveNumbersButton = wx.Button(self, label=_('Update Start Wave Bibs'), style=wx.BU_EXACTFIT)
		self.Bind( wx.EVT_BUTTON, self.onUpdateStartWaveNumbers, self.updateStartWaveNumbersButton )
		hs.Add( self.updateStartWaveNumbersButton, 0, border = border, flag = flag )
		'''

        self.normalizeButton = wx.Button(self,
                                         label=_('Normalize'),
                                         style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onNormalize, self.normalizeButton)
        hs.Add(self.normalizeButton, 0, border=border, flag=flag)

        hs.AddStretchSpacer()

        self.printButton = wx.Button(self,
                                     label=u'{}...'.format(_('Print')),
                                     style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onPrint, self.printButton)
        hs.Add(self.printButton, 0, border=border, flag=flag)

        self.excelButton = wx.Button(self,
                                     label=u'{}...'.format(_('Excel')),
                                     style=wx.BU_EXACTFIT)
        self.Bind(wx.EVT_BUTTON, self.onExcel, self.excelButton)
        hs.Add(self.excelButton, 0, border=border, flag=flag)

        self.grid = ReorderableGrid(self)
        self.colNameFields = [
            (u'', None),
            (_('Category Type'), 'catType'),
            (_('Active'), 'active'),
            (_('Name'), 'name'),
            (_('Gender'), 'gender'),
            (_('Numbers'), 'catStr'),
            (_('Start\nOffset'), 'startOffset'),
            (_('Race\nLaps'), 'numLaps'),
            (_('Race\nMinutes'), 'raceMinutes'),
            (_('Lapped\nRiders\nContinue'), 'lappedRidersMustContinue'),
            (_('Distance'), 'distance'),
            (_('Dist.\nBy'), 'distanceType'),
            (_('First\nLap\nDist.'), 'firstLapDistance'),
            (_('80%\nLap\nTime'), 'rule80Time'),
            (_('CrossMgr\nEstimated\nLaps'), 'suggestedLaps'),
            (_('Publish'), 'publishFlag'),
            (_('Upload'), 'uploadFlag'),
            (_('Series'), 'seriesFlag'),
        ]
        self.computedFields = {'rule80Time', 'suggestedLaps'}
        self.colnames = [
            colName if not colName.startswith('_') else _('Name Copy')
            for colName, fieldName in self.colNameFields
        ]
        self.iCol = {
            fieldName: i
            for i, (colName, fieldName) in enumerate(self.colNameFields)
            if fieldName and not colName.startswith('_')
        }

        self.activeColumn = self.iCol['active']
        self.genderColumn = self.iCol['gender']
        self.numbersColumn = self.iCol['catStr']
        self.grid.CreateGrid(0, len(self.colnames))
        self.grid.SetRowLabelSize(32)
        self.grid.SetMargins(0, 0)
        for col, name in enumerate(self.colnames):
            self.grid.SetColLabelValue(col, name)

        self.cb = None

        self.boolCols = set()
        self.choiceCols = set()
        self.readOnlyCols = set()
        self.dependentCols = set()

        # Set column attributes for the table.
        for col, (colName, fieldName) in enumerate(self.colNameFields):
            attr = gridlib.GridCellAttr()

            if fieldName is None:
                attr.SetRenderer(CategoryIconRenderer())
                attr.SetAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE)
                attr.SetReadOnly(True)
                self.readOnlyCols.add(col)

            elif fieldName == 'catType':
                self.catTypeWidth = 64
                attr.SetEditor(
                    gridlib.GridCellChoiceEditor(self.CategoryTypeChoices,
                                                 False))
                attr.SetAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE)
                self.choiceCols.add(col)

            elif fieldName in {
                    'active', 'lappedRidersMustContinue', 'publishFlag',
                    'uploadFlag', 'seriesFlag'
            }:
                boolEditor = gridlib.GridCellBoolEditor()
                boolEditor.UseStringValues('1', '0')
                attr.SetEditor(boolEditor)
                attr.SetRenderer(gridlib.GridCellBoolRenderer())
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.boolCols.add(col)
                if fieldName == 'lappedRidersMustContinue':
                    self.dependentCols.add(col)

            elif fieldName == 'gender':
                attr.SetEditor(
                    gridlib.GridCellChoiceEditor(
                        [_('Open'), _('Men'), _('Women')], False))
                self.choiceCols.add(col)

            elif fieldName == 'startOffset':
                attr.SetEditor(TimeEditor())
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.dependentCols.add(col)

            elif fieldName == 'numLaps':
                attr.SetEditor(wx.grid.GridCellNumberEditor())
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.dependentCols.add(col)

            elif fieldName == 'raceMinutes':
                attr.SetEditor(wx.grid.GridCellNumberEditor())
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.dependentCols.add(col)

            elif fieldName in ['rule80Time', 'suggestedLaps']:
                attr.SetReadOnly(True)
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.readOnlyCols.add(col)
                self.dependentCols.add(col)

            elif fieldName in ['distance', 'firstLapDistance']:
                attr.SetEditor(gridlib.GridCellFloatEditor(7, 3))
                attr.SetRenderer(gridlib.GridCellFloatRenderer(7, 3))
                attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
                self.dependentCols.add(col)

            elif fieldName == 'distanceType':
                attr.SetEditor(
                    gridlib.GridCellChoiceEditor(self.DistanceTypeChoices,
                                                 False))
                attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTRE)
                self.choiceCols.add(col)
                self.dependentCols.add(col)

            elif colName == '_name2':
                attr.SetAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE)
                attr.SetBackgroundColour(wx.Colour(240, 240, 240))
                attr.SetReadOnly(True)

            self.grid.SetColAttr(col, attr)

        self.Bind(gridlib.EVT_GRID_CELL_LEFT_CLICK, self.onGridLeftClick)
        self.Bind(gridlib.EVT_GRID_SELECT_CELL, self.onCellSelected)
        self.Bind(gridlib.EVT_GRID_CELL_CHANGED, self.onCellChanged)
        self.Bind(gridlib.EVT_GRID_EDITOR_CREATED, self.onEditorCreated)

        vs.Add(hs, 0, flag=wx.EXPAND | wx.ALL, border=4)
        vs.Add(self.grid, 1, flag=wx.GROW | wx.ALL | wx.EXPAND)

        self.rowCur = 0
        self.colCur = 0
        self.SetSizer(vs)

    def onPrint(self, event):
        self.commit()
        PrintCategories()

    def onExcel(self, event):
        self.commit()
        export = getExportGrid()
        xlFName = Utils.getMainWin().getFormatFilename('excel')
        xlFName = os.path.splitext(
            xlFName)[0] + '-Categories' + os.path.splitext(xlFName)[1]

        wb = xlwt.Workbook()
        sheetCur = wb.add_sheet(_('Categories'))
        export.toExcelSheet(sheetCur)
        try:
            wb.save(xlFName)
            if Utils.getMainWin().launchExcelAfterPublishingResults:
                Utils.LaunchApplication(xlFName)
            Utils.MessageOK(
                self, u'{}:\n\n   {}'.format(_('Excel file written to'),
                                             xlFName), _('Excel Write'))
        except IOError:
            Utils.MessageOK(
                self,
                u'{} "{}"\n\n{}\n{}'.format(
                    _('Cannot write'), xlFName,
                    _('Check if this spreadsheet is already open.'),
                    _('If so, close it, and try again.')),
                _('Excel File Error'),
                iconMask=wx.ICON_ERROR)

    def onSetGpxDistance(self, event):
        race = Model.race
        geoTrack = getattr(race, 'geoTrack', None)
        if not geoTrack:
            return
        if not Utils.MessageOK(self,
                               _('Set the GPX distance for all Categories?'),
                               _('Set GPX Distance'), wx.ICON_QUESTION):
            return
        distance = geoTrack.lengthKm if race.distanceUnit == Model.Race.UnitKm else geoTrack.lengthMiles
        for category in race.getCategories():
            category.distance = distance
        race.setChanged()
        self.refresh(forceRefresh=True)

    def onNormalize(self, event):
        self.commit()
        if Model.race:
            Model.race.normalizeCategories()
            self.state.reset()
            self.refresh()

    #------------------------------------------

    def onGridLeftClick(self, event):
        if event.GetCol() in self.boolCols:
            r, c = event.GetRow(), event.GetCol()
            if c == self.iCol['active']:
                active = (self.grid.GetCellValue(r,
                                                 self.iCol['active']) == u'1')
                wx.CallAfter(
                    self.fixRow, r,
                    self.CategoryTypeChoices.index(
                        self.grid.GetCellValue(r, self.iCol['catType'])),
                    not active)
            self.grid.SetCellValue(
                r, c, '1' if self.grid.GetCellValue(r, c)[:1] != '1' else '0')
        event.Skip()

    def onCellSelected(self, event):
        self.rowCur = event.GetRow()
        self.colCur = event.GetCol()
        if self.colCur in self.choiceCols or self.colCur in self.boolCols:
            wx.CallAfter(self.grid.EnableCellEditControl)
        event.Skip()

    def onCellChanged(self, event):
        self.rowCur = event.GetRow()
        self.colCur = event.GetCol()
        if self.colCur in [1, 2]:
            wx.CallAfter(self.fixCells)
        event.Skip()

    def onEditorCreated(self, event):
        if event.GetCol() == self.numbersColumn:
            ctrl = event.GetControl()
            ctrl.Bind(wx.EVT_KEY_DOWN, self.onNumbersKeyEvent)
            ctrl.Bind(wx.EVT_TEXT_PASTE, self.onPaste)
        event.Skip()

    def getCleanClipboardText(self):
        if wx.TheClipboard.Open():
            data = wx.TextDataObject()
            if wx.TheClipboard.GetData(data):
                txt = data.GetText()
                txt = re.sub('[^0-9,-]+', ',', txt)
            wx.TheClipboard.Close()
            return txt
        return None

    def isExternalChange(self):
        if not Model.race:
            return False

        categories = Model.race.getAllCategories()

        for cat in categories:
            try:
                cat.distance = float(cat.distance)
            except:
                cat.distance = None
            try:
                cat.firstLapDistance = float(cat.firstLapDistance)
            except:
                cat.firstLapDistance = None

        if self.grid.GetNumberRows() != len(categories):
            return True

        def distanceMatches(distance, cellValue):
            try:
                value = float(cellValue)
            except ValueError:
                value = None

            if not distance and not value:
                return True
            return u'{:.3f}'.format(distance or 0.0) == cellValue

        def numLapsMatches(numLaps, cellValue):
            v = u'{}'.format(numLaps if numLaps is not None else '')
            return v == cellValue

        return any((
            cat.name != self.grid.GetCellValue(r, self.iCol['name'])
            or cat.catStr != self.grid.GetCellValue(r, self.iCol['catStr'])
            or not distanceMatches(
                cat.distance, self.grid.GetCellValue(r, self.iCol['distance']))
            or not distanceMatches(
                cat.firstLapDistance,
                self.grid.GetCellValue(r, self.iCol['firstLapDistance']))
            or not numLapsMatches(
                cat.numLaps, self.grid.GetCellValue(r, self.iCol['numLaps'])))
                   for r, cat in enumerate(categories))

    def pasteFromClipboard(self, event):
        txt = self.getCleanClipboardText()
        if txt:
            event.GetEventObject().WriteText(txt)
            return True
        return False

    def onNumbersKeyEvent(self, event):
        # Handle column pastes from Excel when there are newlines.
        if event.GetModifiers() == wx.MOD_CONTROL and event.GetKeyCode() == 86:
            if self.pasteFromClipboard(event):
                return
        event.Skip()

    def onPaste(self, event):
        self.pasteFromClipboard(event)
        event.Skip()

    #------------------------------------------

    def onUpdateStartWaveNumbers(self, event):
        self.commit()
        undo.pushState()
        with Model.LockRace() as race:
            race.adjustAllCategoryWaveNumbers()
        self.state.reset()
        wx.CallAfter(self.refresh)
        wx.CallAfter(Utils.refreshForecastHistory)

    def onAddExceptions(self, event):
        with Model.LockRace() as race:
            if not race or not race.getAllCategories():
                return

        r = self.grid.GetGridCursorRow()
        if r is None or r < 0:
            Utils.MessageOK(self, _('You must select a Category first'),
                            _('Select a Category'))
            return

        with Model.LockRace() as race:
            categories = race.getAllCategories()
            category = categories[r]

        dlg = wx.TextEntryDialog(
            self, u'{}: {}'.format(
                category.name,
                _('''Add Bib Exceptions (comma separated).
This will add the given list of Bibs to this category,
and remove them from other categories.'''),
            ), _('Add Bib Exceptions'))
        good = (dlg.ShowModal() == wx.ID_OK)
        if good:
            response = dlg.GetValue()
        dlg.Destroy()
        if not good:
            return

        undo.pushState()
        response = re.sub('[^0-9,]', '', response.replace(' ', ','))
        with Model.LockRace() as race:
            for numException in response.split(','):
                race.addCategoryException(category, numException)

        self.state.reset()
        self.refresh()

    def _setRow(
        self,
        r,
        active,
        name,
        catStr,
        startOffset='00:00:00',
        numLaps=None,
        raceMinutes=None,
        lappedRidersMustContinue=False,
        distance=None,
        distanceType=None,
        firstLapDistance=None,
        gender=None,
        catType=Model.Category.CatWave,
        publishFlag=True,
        uploadFlag=True,
        seriesFlag=True,
    ):

        if len(startOffset) < len('00:00:00'):
            startOffset = '00:' + startOffset

        GetTranslation = _
        gender = gender if gender in ['Men', 'Women'] else 'Open'
        self.grid.SetRowLabelValue(r, u'')
        self.grid.SetCellValue(r, self.iCol['active'],
                               u'1' if active else u'0')
        self.grid.SetCellValue(r, self.iCol['catType'],
                               self.CategoryTypeChoices[catType])
        self.grid.SetCellValue(r, self.iCol['name'], name)
        self.grid.SetCellValue(r, self.iCol['gender'], GetTranslation(gender))
        self.grid.SetCellValue(r, self.iCol['catStr'], catStr)
        self.grid.SetCellValue(r, self.iCol['startOffset'], startOffset)
        self.grid.SetCellValue(r, self.iCol['numLaps'],
                               u'{}'.format(numLaps) if numLaps else u'')
        self.grid.SetCellValue(
            r, self.iCol['raceMinutes'],
            u'{}'.format(raceMinutes) if raceMinutes else u'')
        self.grid.SetCellValue(r, self.iCol['lappedRidersMustContinue'],
                               u'1' if lappedRidersMustContinue else u'0')
        self.grid.SetCellValue(r, self.iCol['rule80Time'], u'')
        self.grid.SetCellValue(r, self.iCol['suggestedLaps'], u'')
        self.grid.SetCellValue(r, self.iCol['distance'],
                               ('%.3f' % distance) if distance else u'')
        self.grid.SetCellValue(
            r, self.iCol['distanceType'],
            self.DistanceTypeChoices[distanceType if distanceType else 0])
        self.grid.SetCellValue(r, self.iCol['firstLapDistance'],
                               ('%.3f' %
                                firstLapDistance) if firstLapDistance else '')
        self.grid.SetCellValue(r, self.iCol['publishFlag'],
                               u'1' if publishFlag else u'0')
        self.grid.SetCellValue(r, self.iCol['uploadFlag'],
                               u'1' if uploadFlag else u'0')
        self.grid.SetCellValue(r, self.iCol['seriesFlag'],
                               u'1' if seriesFlag else u'0')

        race = Model.race
        category = race.categories.get(u'{} ({})'.format(name.strip(), gender),
                                       None) if race else None
        if not category or category.catType != Model.Category.CatWave:
            return

        # Get the 80% time cutoff.
        if not active or not Model.race:
            return

        rule80Time = race.getRule80CountdownTime(category) if race else None
        if rule80Time:
            self.grid.SetCellValue(r, self.iCol['rule80Time'],
                                   Utils.formatTime(rule80Time))

        laps = race.getCategoryRaceLaps().get(category, 0) if race else None
        if laps:
            self.grid.SetCellValue(r, self.iCol['suggestedLaps'],
                                   u'{}'.format(laps))

    def fixRow(self, row, catType, active):
        activeColour = wx.WHITE if active else self.inactiveColour
        colour = activeColour if catType == Model.Category.CatWave else self.ignoreColour
        for colName, fieldName in self.colNameFields:
            if not fieldName:
                continue
            col = self.iCol[fieldName]
            self.grid.SetCellBackgroundColour(
                row, col,
                colour if col in self.dependentCols else activeColour)

    def fixCells(self, event=None):
        for row in xrange(self.grid.GetNumberRows()):
            active = self.grid.GetCellValue(row,
                                            self.iCol['active'])[:1] in 'TtYy1'
            catType = self.CategoryTypeChoices.index(
                self.grid.GetCellValue(row, self.iCol['catType']))
            self.fixRow(row, catType, active)

    def onActivateAll(self, event):
        self.commit()
        if Model.race:
            for c in Model.race.getAllCategories():
                if not c.active:
                    c.active = True
                    Model.race.setChanged()
        self.state.reset()
        wx.CallAfter(self.refresh)

    def onDeactivateAll(self, event):
        self.commit()
        if Model.race:
            for c in Model.race.getAllCategories():
                if c.active:
                    c.active = False
                    Model.race.setChanged()
        self.state.reset()
        wx.CallAfter(self.refresh)

    def doAutosize(self):
        self.grid.AutoSizeColumns(False)
        colWidth = self.grid.GetColSize(self.iCol['catStr'])
        maxWidth = wx.GetDisplaySize().width / 3
        if colWidth > maxWidth:
            self.grid.SetColSize(self.iCol['catStr'], maxWidth)
            self.grid.ForceRefresh()

    def onNewCategory(self, event):
        self.grid.AppendRows(1)
        self._setRow(r=self.grid.GetNumberRows() - 1,
                     active=True,
                     name=u'<{}>     '.format(_('CategoryName')),
                     catStr='100-199,504,-128')
        self.doAutosize()

    def onDelCategory(self, event):
        r = self.grid.GetGridCursorRow()
        if r is None or r < 0:
            return
        if Utils.MessageOKCancel(
                self, u'{} "{} ({})"?'.format(
                    _('Delete Category'),
                    self.grid.GetCellValue(r, 3).strip(),
                    self.grid.GetCellValue(r, 4).strip(),
                ), _('Delete Category')):
            self.grid.DeleteRows(r, 1, True)

    def onUpCategory(self, event):
        self.grid.SaveEditControlValue()
        self.grid.DisableCellEditControl()
        r = self.grid.GetGridCursorRow()
        Utils.SwapGridRows(self.grid, r, r - 1)
        if r - 1 >= 0:
            self.grid.MoveCursorUp(False)
        self.grid.ClearSelection()
        self.grid.SelectRow(max(r - 1, 0), True)

    def onDownCategory(self, event):
        self.grid.SaveEditControlValue()
        self.grid.DisableCellEditControl()
        r = self.grid.GetGridCursorRow()
        Utils.SwapGridRows(self.grid, r, r + 1)
        if r + 1 < self.grid.GetNumberRows():
            self.grid.MoveCursorDown(False)
        self.grid.ClearSelection()
        self.grid.SelectRow(min(r + 1, self.grid.GetNumberRows() - 1), True)

    def refresh(self, forceRefresh=False):
        self.setGpxDistanceButton.Enable(hasattr(Model.race, 'geoTrack'))

        if not (forceRefresh or self.isExternalChange()
                or self.state.changed()):
            return

        # Fix the height of the column labels.
        dc = wx.WindowDC(self.grid)
        dc.SetFont(self.grid.GetLabelFont())
        textHeight = dc.GetTextExtent('Label')[1]
        self.colLabelHeight = textHeight * max(
            name.count('\n') + 1 for name in self.colnames) + textHeight // 4
        self.grid.SetColLabelSize(self.colLabelHeight)

        with Model.LockRace() as race:
            self.grid.ClearGrid()
            if race is None:
                return

            for c in xrange(self.grid.GetNumberCols()):
                if self.grid.GetColLabelValue(c).startswith(_('Distance')):
                    self.grid.SetColLabelValue(
                        c,
                        u'{}\n({})'.format(_('Distance'),
                                           ['km', 'miles'
                                            ][getattr(race, 'distanceUnit',
                                                      0)]))
                    break

            categories = race.getAllCategories()

            if self.grid.GetNumberRows() > 0:
                self.grid.DeleteRows(0, self.grid.GetNumberRows())
            self.grid.AppendRows(len(categories))

            for r, cat in enumerate(categories):
                self._setRow(
                    r=r,
                    active=cat.active,
                    name=cat.name,
                    gender=getattr(cat, 'gender', None),
                    catStr=cat.catStr,
                    catType=cat.catType,
                    startOffset=cat.startOffset,
                    numLaps=cat._numLaps,
                    raceMinutes=cat.raceMinutes,
                    lappedRidersMustContinue=getattr(
                        cat, 'lappedRidersMustContinue', False),
                    distance=getattr(cat, 'distance', None),
                    distanceType=getattr(cat, 'distanceType',
                                         Model.Category.DistanceByLap),
                    firstLapDistance=getattr(cat, 'firstLapDistance', None),
                    publishFlag=cat.publishFlag,
                    uploadFlag=cat.uploadFlag,
                    seriesFlag=cat.seriesFlag,
                )

            self.doAutosize()
            self.fixCells()

            # Force the grid to the correct size.
            self.grid.FitInside()
            self.GetSizer().Layout()

    def commit(self):
        undo.pushState()
        with Model.LockRace() as race:
            self.grid.SaveEditControlValue()
            self.grid.DisableCellEditControl(
            )  # Make sure the current edit is committed.
            if race is None:
                return
            numStrTuples = []
            for r in xrange(self.grid.GetNumberRows()):
                values = {
                    name: self.grid.GetCellValue(r, c)
                    for name, c in self.iCol.iteritems()
                    if name not in self.computedFields
                }
                values['catType'] = self.CategoryTypeChoices.index(
                    values['catType'])
                values['distanceType'] = self.DistanceTypeChoices.index(
                    values['distanceType'])
                numStrTuples.append(values)
            race.setCategories(numStrTuples)
            race.adjustAllCategoryWaveNumbers()
        wx.CallAfter(Utils.refreshForecastHistory)
Exemplo n.º 2
0
class Points(wx.Panel):
    #----------------------------------------------------------------------
    NameCol = 0
    OldNameCol = 1
    DepthCol = 2
    PointsCol = 3
    ParticipationCol = 4
    DNFCol = 5

    def __init__(self, parent):
        wx.Panel.__init__(self, parent)

        #--------------------------------------------------------------------------
        box = wx.StaticBox(self, -1, 'Score Competitors by')
        bsizer = wx.StaticBoxSizer(box, wx.VERTICAL)

        self.scoreByPoints = wx.RadioButton(self,
                                            label='Points',
                                            style=wx.RB_GROUP)
        self.scoreByPoints.Bind(wx.EVT_RADIOBUTTON, self.fixEnable)

        self.scoreByTime = wx.RadioButton(self, label='Time')
        self.scoreByTime.Bind(wx.EVT_RADIOBUTTON, self.fixEnable)

        self.scoreByPercent = wx.RadioButton(
            self, label='Percent Time (100 * WinnersTime / FinishTime)')
        self.scoreByPercent.Bind(wx.EVT_RADIOBUTTON, self.fixEnable)

        self.scoreByTrueSkill = wx.RadioButton(self, label='TrueSkill')
        self.scoreByTrueSkill.Bind(wx.EVT_RADIOBUTTON, self.fixEnable)

        self.scoreByPoints.SetValue(True)

        hb = wx.BoxSizer(wx.HORIZONTAL)
        hb.Add(self.scoreByPoints)
        hb.Add(self.scoreByTime, flag=wx.LEFT, border=16)
        hb.Add(self.scoreByPercent, flag=wx.LEFT, border=16)
        hb.Add(self.scoreByTrueSkill, flag=wx.LEFT, border=16)
        bsizer.Add(hb, flag=wx.ALL, border=2)

        #--------------------------------------------------------------------------
        bsizer.Add(wx.StaticLine(self), 1, flag=wx.EXPAND | wx.ALL, border=4)
        self.considerPrimePointsOrTimeBonus = wx.CheckBox(
            self, label='Consider Points or Time Bonuses from CrossMgr Primes')
        self.considerPrimePointsOrTimeBonus.SetValue(True)
        bsizer.Add(self.considerPrimePointsOrTimeBonus,
                   0,
                   flag=wx.ALL,
                   border=4)

        #--------------------------------------------------------------------------
        bsizer.Add(wx.StaticLine(self), 1, flag=wx.EXPAND | wx.ALL, border=4)

        maxOptions = 30
        self.considerLabel = wx.StaticText(self,
                                           label='{}:'.format('Consider'))
        self.bestResultsToConsider = wx.Choice(
            self,
            choices=['All Results', 'Best Result Only'] + [
                '{} {} {}'.format('Best', i, 'Results Only')
                for i in xrange(2, maxOptions + 1)
            ])

        self.participationLabel = wx.StaticText(
            self, label='{}:'.format('Must have completed'))
        self.mustHaveCompleted = wx.Choice(
            self,
            choices=[
                '{} {}'.format(i, 'or more Events')
                for i in xrange(0, maxOptions + 1)
            ])

        hb = wx.BoxSizer(wx.HORIZONTAL)
        hb.Add(self.considerLabel, flag=wx.ALIGN_CENTRE_VERTICAL)
        hb.Add(self.bestResultsToConsider)
        hb.Add(self.participationLabel,
               flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
               border=16)
        hb.Add(self.mustHaveCompleted)
        bsizer.Add(hb, flag=wx.ALL, border=2)

        #--------------------------------------------------------------------------
        bsizer.Add(wx.StaticLine(self), 1, flag=wx.EXPAND | wx.ALL, border=4)
        self.ifRidersTiedOnPoints = wx.StaticText(
            self, label='If Riders are Tied on Points:')
        bsizer.Add(self.ifRidersTiedOnPoints, flag=wx.ALL, border=2)
        self.mostEventsCompleted = wx.CheckBox(
            self, label='Consider Number of Events Completed')
        self.mostEventsCompleted.SetValue(False)
        bsizer.Add(self.mostEventsCompleted, 0, flag=wx.ALL, border=4)

        bsizer.Add(wx.StaticLine(self), 1, flag=wx.EXPAND | wx.ALL, border=4)
        self.numPlacesTieBreaker = wx.RadioBox(
            self,
            majorDimension=3,
            style=wx.RA_SPECIFY_ROWS,
            choices=[
                'Do not consider Place Finishes.',
                'Number of 1st Place Finishes',
                'Number of 1st then 2nd Place Finishes',
                'Number of 1st, 2nd then 3rd Place Finishes',
                'Number of 1st, 2nd, 3rd then 4th Place Finishes',
                'Number of 1st, 2nd, 3rd, 4th then 5th Place Finishes',
            ])
        self.numPlacesTieBreaker.SetLabel(
            u'If Riders are still Tied on Points:')
        bsizer.Add(self.numPlacesTieBreaker,
                   0,
                   flag=wx.ALL | wx.EXPAND,
                   border=2)
        self.ifRidersAreStillTiedOnPoints = wx.StaticText(
            self,
            label=u'If Riders are still Tied on Points, use most Recent Results'
        )
        bsizer.Add(self.ifRidersAreStillTiedOnPoints, flag=wx.ALL, border=4)

        self.headerNames = [
            'Name', 'OldName', 'Depth', 'Points for Position', 'Participation',
            'DNF'
        ]

        self.grid = ReorderableGrid(self, style=wx.BORDER_SUNKEN)
        self.grid.DisableDragRowSize()
        self.grid.SetRowLabelSize(64)
        self.grid.CreateGrid(50, len(self.headerNames))
        for col in xrange(self.grid.GetNumberCols()):
            self.grid.SetColLabelValue(
                col, self.headerNames[col] +
                (u'       ' if self.headerNames[col] == 'DNF' else ''))

        attr = gridlib.GridCellAttr()
        attr.SetReadOnly(True)
        attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
        self.grid.SetColAttr(self.DepthCol, attr)

        attr = gridlib.GridCellAttr()
        attr.SetEditor(PointsEditor())
        self.grid.SetColAttr(self.PointsCol, attr)

        for c in (self.ParticipationCol, self.DNFCol):
            attr = gridlib.GridCellAttr()
            attr.SetRenderer(wx.grid.GridCellNumberRenderer())
            attr.SetEditor(wx.grid.GridCellNumberEditor(0, 10000))
            self.grid.SetColAttr(c, attr)

        self.gridAutoSize()

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(bsizer, 0, flag=wx.EXPAND | wx.ALL, border=4)
        self.pointsStructures = wx.StaticText(self, wx.ID_ANY,
                                              'Points Structures:')
        sizer.Add(self.pointsStructures, 0, flag=wx.ALL, border=4)
        sizer.Add(self.grid, 1, flag=wx.EXPAND | wx.ALL, border=6)
        self.SetSizer(sizer)

        self.scoreByPointsControls = [
            self.ifRidersTiedOnPoints,
            self.mostEventsCompleted,
            self.numPlacesTieBreaker,
            self.ifRidersAreStillTiedOnPoints,
            self.pointsStructures,
            self.grid,
        ]

    def getGrid(self):
        return self.grid

    def gridAutoSize(self):
        self.grid.AutoSize()
        self.grid.SetColMinimalAcceptableWidth(0)
        self.grid.SetColMinimalWidth(self.OldNameCol, 0)
        self.grid.SetColSize(self.OldNameCol, 0)
        self.grid.EnableDragGridSize(False)
        self.grid.EnableDragColSize(False)
        self.Layout()
        self.Refresh()

    def updateDepth(self, row):
        v = self.grid.GetCellValue(row, self.PointsCol).strip()
        depth = unicode(len(v.split())) if v else u''
        self.grid.SetCellValue(row, self.DepthCol, depth)

    def onGridChange(self, event):
        row = event.GetRow()
        if row >= 0:
            self.updateDepth(row)
        wx.CallAfter(self.gridAutoSize)

    def fixEnable(self, event=None):
        enable = self.scoreByPoints.GetValue()
        for c in self.scoreByPointsControls:
            c.Enable(enable)

    def refresh(self):
        model = SeriesModel.model
        for row in xrange(self.grid.GetNumberRows()):
            for col in xrange(self.grid.GetNumberCols()):
                self.grid.SetCellValue(row, col, '')

        for row, ps in enumerate(model.pointStructures):
            self.grid.SetCellValue(row, self.NameCol, ps.name)
            self.grid.SetCellValue(row, self.OldNameCol, ps.name)
            self.grid.SetCellValue(row, self.PointsCol, ps.getStr())
            self.grid.SetCellValue(row, self.ParticipationCol,
                                   unicode(ps.participationPoints))
            self.grid.SetCellValue(row, self.DNFCol, unicode(ps.dnfPoints))
            self.updateDepth(row)

        wx.CallAfter(self.gridAutoSize)

        self.considerPrimePointsOrTimeBonus.SetValue(
            model.considerPrimePointsOrTimeBonus)
        self.bestResultsToConsider.SetSelection(model.bestResultsToConsider)
        self.mustHaveCompleted.SetSelection(model.mustHaveCompleted)

        self.mostEventsCompleted.SetValue(model.useMostEventsCompleted)
        self.numPlacesTieBreaker.SetSelection(model.numPlacesTieBreaker)

        if model.scoreByTime:
            self.scoreByTime.SetValue(True)
        elif model.scoreByPercent:
            self.scoreByPercent.SetValue(True)
        elif model.scoreByTrueSkill:
            self.scoreByTrueSkill.SetValue(True)
        else:
            self.scoreByPoints.SetValue(True)
        self.fixEnable()

    def commit(self):
        self.grid.SaveEditControlValue()
        self.grid.DisableCellEditControl(
        )  # Make sure the current edit is committed.
        pointsList = []
        for row in xrange(self.grid.GetNumberRows()):
            if (self.grid.GetCellValue(row, self.NameCol).strip()):
                pointsList.append((
                    self.grid.GetCellValue(row, self.NameCol),
                    self.grid.GetCellValue(row, self.OldNameCol),
                    self.grid.GetCellValue(row, self.PointsCol),
                    self.grid.GetCellValue(row, self.ParticipationCol),
                    self.grid.GetCellValue(row, self.DNFCol),
                ))

        model = SeriesModel.model
        model.setPoints(pointsList)

        modelUpdate = {
            'considerPrimePointsOrTimeBonus':
            self.considerPrimePointsOrTimeBonus.GetValue(),
            'bestResultsToConsider':
            self.bestResultsToConsider.GetSelection(),
            'mustHaveCompleted':
            self.mustHaveCompleted.GetSelection(),
            'useMostEventsCompleted':
            self.mostEventsCompleted.GetValue(),
            'numPlacesTieBreaker':
            self.numPlacesTieBreaker.GetSelection(),
            'scoreByTime':
            self.scoreByTime.GetValue(),
            'scoreByPercent':
            self.scoreByPercent.GetValue(),
            'scoreByTrueSkill':
            self.scoreByTrueSkill.GetValue(),
        }

        for attr, value in modelUpdate.iteritems():
            if getattr(model, attr) != value:
                setattr(model, attr, value)
                model.changed = True
Exemplo n.º 3
0
class Pulled(wx.Panel):
    def __init__(self, parent, id=wx.ID_ANY, size=wx.DefaultSize):
        super(Pulled, self).__init__(parent, id, size=size)

        self.state = RaceInputState()

        vsOverall = wx.BoxSizer(wx.VERTICAL)

        self.hbs = wx.BoxSizer(wx.HORIZONTAL)
        self.showingCategoryLabel = wx.StaticText(self,
                                                  label=u'{}:'.format(
                                                      _('Start Wave')))
        self.showingCategory = wx.StaticText(self)
        self.showingCategory.SetFont(self.showingCategory.GetFont().Bold())
        self.categoryLabel = wx.StaticText(self, label=_('Category:'))
        self.categoryChoice = wx.Choice(self)
        self.Bind(wx.EVT_CHOICE, self.doChooseCategory, self.categoryChoice)
        self.useTableToPullRidersCkBox = wx.CheckBox(
            self, label=_('Use this Table to Pull Riders'))
        self.useTableToPullRidersCkBox.SetToolTip(
            wx.ToolTip(
                _('Also requires Laps to be set in Categories screen.')))
        self.commitBtn = wx.Button(self, label=_('Commit'))
        self.commitBtn.Bind(wx.EVT_BUTTON, self.doCommit)
        self.hbs.Add(self.showingCategoryLabel,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=0)
        self.hbs.Add(self.showingCategory,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=2)
        self.hbs.Add(self.categoryLabel,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=18)
        self.hbs.Add(self.categoryChoice,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=2)
        self.hbs.Add(self.useTableToPullRidersCkBox,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=18)
        self.hbs.Add(self.commitBtn,
                     flag=wx.LEFT | wx.ALIGN_CENTRE_VERTICAL,
                     border=32)

        #---------------------------------------------------------------
        self.colNameFields = (
            (_('Laps to Go'), 'lapsToGo', 'i'),
            (u'    ' + _('Bib'), 'pulledBib', 'i'),
            (u'Name', 'pulledName', 's'),
            (u'Team', 'pulledTeam', 's'),
            (u'Component', 'pulledComponent', 's'),
            (u'Error', 'pulledError', 's'),
        )
        self.colnames = [
            colName for colName, fieldName, dataType in self.colNameFields
        ]
        self.iCol = dict((fieldName, i)
                         for i, (colName, fieldName,
                                 dataType) in enumerate(self.colNameFields)
                         if fieldName)
        self.grid = ReorderableGrid(self)
        self.grid.CreateGrid(0, len(self.colNameFields))
        GetTranslation = _
        for col, (colName, fieldName,
                  dataType) in enumerate(self.colNameFields):
            self.grid.SetColLabelValue(col, colName)
            attr = wx.grid.GridCellAttr()
            if dataType == 'i':
                attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_TOP)
                attr.SetEditor(wx.grid.GridCellFloatEditor(precision=0))
                attr.SetRenderer(wx.grid.GridCellFloatRenderer(precision=0))
            elif dataType == 'f':
                attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_TOP)
                attr.SetEditor(wx.grid.GridCellFloatEditor(precision=2))
                attr.SetRenderer(wx.grid.GridCellFloatRenderer(precision=2))
            elif dataType == 't':
                attr.SetAlignment(wx.ALIGN_RIGHT, wx.ALIGN_CENTRE)
                attr.SetEditor(TimeEditor())

            self.grid.SetColAttr(col, attr)

        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.onCellChange)
        self.grid.AutoSizeColumns(False)
        self.grid.AutoSizeRows(False)

        #---------------------------------------------------------------

        vsOverall.Add(self.hbs, 0, flag=wx.EXPAND | wx.ALL, border=4)
        vsOverall.Add(self.grid, 1, flag=wx.EXPAND | wx.ALL, border=4)
        self.SetSizer(vsOverall)

    def setCategory(self, category):
        for i, c in enumerate(
                Model.race.getCategories(
                    startWaveOnly=False) if Model.race else [], 1):
            if c == category:
                SetCategory(self.categoryChoice, c)
                Model.setCategoryChoice(i, 'resultsCategory')
                return
        SetCategory(self.categoryChoice, None)
        Model.setCategoryChoice(0, 'resultsCategory')

    def doChooseCategory(self, event):
        Model.setCategoryChoice(self.categoryChoice.GetSelection(),
                                'resultsCategory')
        self.refresh()

    def doCommit(self, event):
        self.commit()
        self.refresh()

    def getCategory(self):
        race = Model.race
        if not race:
            category = None
        else:
            category = race.getCategoryStartWave(
                FixCategories(self.categoryChoice,
                              getattr(race, 'resultsCategory', 0)))
        categoryName = category.fullname if category else u''
        if categoryName != self.showingCategory.GetLabel():
            self.showingCategory.SetLabel(categoryName)
            self.hbs.Layout()
        return category

    def getRaceInfo(self):
        race = Model.race
        if not race:
            return False, []

        category = self.getCategory()
        if not category:
            return False, []

        results = GetResults(category)
        if not results or not results[0].lapTimes:
            return False, []

        return True, [race, category, results, len(results[0].lapTimes)]

    def getError(self, bib, lapsToGo, laps):
        if not bib:
            return u''
        if not lapsToGo:
            lapsToGo = 1
        success, info = self.getRaceInfo()
        if not success:
            return u''
        race, category, results, laps = info

        if bib not in race.riders:
            return _(u'Bib not in Race')
        if race.getCategory(bib) != category:
            return _(u'Bib not in Category')
        rider = race.riders[bib]
        if rider.status not in (Model.Rider.Pulled, Model.Rider.Finisher):
            return u'{}: {}'.format(_('Bib has non-Finisher Status'),
                                    Model.Rider.statusNames[rider.status])
        if lapsToGo >= laps:
            return u'{}: {}'.format(_('Laps To Go exceeds for Race Laps'),
                                    laps)
        if lapsToGo <= 0:
            return u'{}'.format(_('Laps To Go must be >= 0'))
        return u''

    def onCellChange(self, event):
        row, col = event.GetRow(), event.GetCol()
        colName = self.colNameFields[col][1]
        GetTranslation = _

        if colName == 'pulledBib' or colName == 'lapsToGo':
            bib = int(
                '0' +
                re.sub('[^0-9]', '',
                       self.grid.GetCellValue(row, self.iCol['pulledBib'])))
            for r in range(row, -1, -1):
                lapsToGo = int(
                    '0' + self.grid.GetCellValue(r, self.iCol['lapsToGo']))
                if lapsToGo:
                    break
            if not lapsToGo:
                lapsToGo = 1

            success, info = self.getRaceInfo()
            if not success:
                return
            race, category, results, laps = info

            name, team, component = getRiderInfo(bib)
            self.grid.SetCellValue(row, self.iCol['pulledName'], name)
            self.grid.SetCellValue(row, self.iCol['pulledTeam'], team)
            self.grid.SetCellValue(row, self.iCol['pulledComponent'],
                                   component)
            self.grid.SetCellValue(row, self.iCol['pulledError'],
                                   self.getError(bib, lapsToGo, laps))

            wx.CallAfter(self.grid.AutoSizeColumns, False)

    def setRow(self, bib, lapsToGo, laps, row, updateGrid=True):
        name, team, component = getRiderInfo(bib)
        values = {
            'pulledBib': bib,
            'pulledName': name,
            'pulledTeam': team,
            'pulledComponent': component,
            'pulledError': self.getError(bib, lapsToGo, laps),
            'lapsToGo': lapsToGo
        }
        for col, (name, attr, valuesType) in enumerate(self.colNameFields):
            self.grid.SetCellValue(row, col, str(values[attr]))
        return values

    def getRow(self, row):
        values = {'row': row}
        for col, (name, attr, dataType) in enumerate(self.colNameFields):
            v = self.grid.GetCellValue(row, col).strip()
            if dataType == 'i':
                v = u''.join(c for c in v if c.isdigit())
                v = int(v or 0)
            elif dataType == 'f':
                v = u''.join(c for c in v if c.isdigit() or c == '.')
                v = float(v or 0.0)
            elif dataType == 't':
                v = Utils.StrToSeconds(v or '')

            values[attr] = v

        return values

    def updateGrid(self):
        self.grid.ClearGrid()

        success, info = self.getRaceInfo()
        if not success:
            return
        race, category, results, laps = info

        if race.isTimeTrial:
            return

        Pulled = Model.Rider.Pulled
        pulled = []
        for rr in results:
            if race.riders[rr.num].status == Pulled:
                pulled.append(
                    getPulledCmpTuple(rr, race.riders[rr.num], laps, False))
        pulled.sort()
        bibLapsToGo = {p[-1].num: abs(p[0]) for p in pulled}
        pulled = [p[-1] for p in pulled]

        Utils.AdjustGridSize(self.grid, len(pulled) + 20)
        for row, rr in enumerate(pulled):
            self.setRow(rr.num, bibLapsToGo[rr.num], laps, row)

        # Remove repeated lapsToGo entries.
        col = self.iCol['lapsToGo']
        for row in range(self.grid.GetNumberRows() - 1, 0, -1):
            if self.grid.GetCellValue(row, col) == self.grid.GetCellValue(
                    row - 1, col):
                self.grid.SetCellValue(row, col, u'')

        self.grid.AutoSizeColumns(False)  # Resize to fit the column name.
        self.grid.AutoSizeRows(False)

    def refresh(self):
        success, info = self.getRaceInfo()
        if not success:
            return self.updateGrid()
        race, category, results, laps = info
        if race.isTimeTrial:
            self.grid.SaveEditControlValue(
            )  # Make sure the current edit is committed.
            self.grid.DisableCellEditControl()
            self.grid.ClearGrid()
            return
        col = self.iCol['pulledBib']
        tableBibs = set(
            int(u'0' + self.grid.GetCellValue(row, col))
            for row in range(self.grid.GetNumberRows()))
        tableBibs.discard(0)
        if not tableBibs:
            return self.updateGrid()

        Pulled = Model.Rider.Pulled
        allBibs = set(rr.num for rr in results)
        if not allBibs >= tableBibs:
            return self.updateGrid()
        pulledBibs = set(rr.num for rr in results
                         if race.riders[rr.num].status == Pulled)
        if not tableBibs >= pulledBibs:
            return self.updateGrid()

    def commit(self):
        self.grid.SaveEditControlValue(
        )  # Make sure the current edit is committed.
        self.grid.DisableCellEditControl()

        race = Model.race
        if not race:
            return
        race.useTableToPullRiders = self.useTableToPullRidersCkBox.GetValue()
        if not race.useTableToPullRiders:
            self.grid.ClearGrid()
            Utils.AdjustGridSize(self.grid, 20)
            return

        rows = [self.getRow(r) for r in range(self.grid.GetNumberRows())]
        rows = [rv for rv in rows if rv['pulledBib']]

        # Fix any missing data lapsToGo in the table.
        lapsToGoLast = 1
        for rv in rows:
            if not rv['lapsToGo']:
                rv['lapsToGo'] = lapsToGoLast
            lapsToGoLast = rv['lapsToGo']

        success, info = self.getRaceInfo()
        if not success:
            return False
        race, category, results, laps = info
        rule80LapTime = race.getRule80LapTime(category)

        changed = False
        Finisher, Pulled = Model.Rider.Finisher, Model.Rider.Pulled
        for rr in results:
            rider = race.riders.get(rr.num, None)
            if not rider or race.getCategory(rr.num) != category:
                continue
            if rider.status == Pulled:
                rider.status = Finisher
                changed = True

        lapsToGoPulled = defaultdict(list)
        for rv in rows:
            lapsToGoPulled[rv['lapsToGo']].append(rv['pulledBib'])

        for lapsToGo, bibs in lapsToGoPulled.items():
            if lapsToGo <= 0:
                continue
            for seq, bib in enumerate(bibs):
                try:
                    rider = race.riders[bib]
                except KeyError:
                    continue

                rider.status = Pulled
                rider.pulledLapsToGo = lapsToGo
                rider.pulledSequence = seq
                changed = True

        if changed:
            race.setChanged()
        self.updateGrid()
Exemplo n.º 4
0
class Primes( wx.Panel ):
	def __init__( self, parent, id=wx.ID_ANY, size=wx.DefaultSize ):
		super(Primes, self).__init__( parent, id, size=size )
		
		self.state = RaceInputState()
		
		vsOverall = wx.BoxSizer( wx.VERTICAL )
		
		#---------------------------------------------------------------
		self.colNameFields = (
			(_('Prime For'),			'effortType',	's'),
			(_('or Custom'),			'effortCustom',	's'),
			(_('Position'),				'position',		'i'),
			(_('Laps\nTo Go'),			'lapsToGo',		'i'),
			(_('Sponsor'),				'sponsor', 		's'),
			(_('Cash'),					'cash', 		'f'),
			(_('Merchandise'),			'merchandise', 	's'),
			(_('Points'),				'points', 		'i'),
			(_('Time\nBonus'),			'timeBonus', 	't'),
			(_('Winner\nBib'),			'winnerBib',	'i'),
			(u'',						'winnerInfo',	's'),
		)
		self.colnames = [colName for colName, fieldName, dataType in self.colNameFields]
		self.iCol = dict( (fieldName, i) for i, (colName, fieldName, dataType) in enumerate(self.colNameFields) if fieldName )
		self.grid = ReorderableGrid( self )
		self.grid.CreateGrid( 0, len(self.colNameFields) )
		GetTranslation = _
		for col, (colName, fieldName, dataType) in enumerate(self.colNameFields):
			self.grid.SetColLabelValue( col, colName )
			attr = wx.grid.GridCellAttr()
			if fieldName == 'effortType':
				attr.SetEditor( wx.grid.GridCellChoiceEditor(choices=[GetTranslation(name) for code, name in EffortChoices]) )
				attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP )
			elif fieldName == 'position':
				attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_TOP )
			elif fieldName == 'winnerInfo':
				attr.SetReadOnly( True )
			elif dataType == 'i':
				attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP )
				attr.SetEditor( wx.grid.GridCellFloatEditor(precision=0) )
				attr.SetRenderer( wx.grid.GridCellFloatRenderer(precision=0) )
			elif dataType == 'f':
				attr.SetAlignment( wx.ALIGN_RIGHT, wx.ALIGN_TOP )
				attr.SetEditor( wx.grid.GridCellFloatEditor(precision=2) )
				attr.SetRenderer( wx.grid.GridCellFloatRenderer(precision=2) )
			elif dataType == 't':
				attr.SetAlignment( wx.ALIGN_CENTRE, wx.ALIGN_CENTRE )
				attr.SetEditor( TimeEditor() )
			
			self.grid.SetColAttr( col, attr )
			if fieldName == 'lapsToGo':
				self.lapsToGoCol = col
		
		self.grid.Bind( wx.grid.EVT_GRID_CELL_CHANGE, self.onCellChange )
		self.grid.AutoSizeColumns( False )
		self.grid.AutoSizeRows( False )

		#---------------------------------------------------------------
		self.photosButton = wx.Button( self, label=u'{}...'.format(_('Photos')) )
		self.photosButton.Bind( wx.EVT_BUTTON, self.onPhotos )
		self.finishStrip = wx.Button( self, label=u'{}...'.format(_('Finish Strip')) )
		self.finishStrip.Bind( wx.EVT_BUTTON, self.onFinishStrip )
		self.history = wx.Button( self, label=u'{}...'.format(_('Passings')) )
		self.history.Bind( wx.EVT_BUTTON, self.onHistory )
		
		self.newButton = wx.Button( self, id=wx.ID_NEW )
		self.newButton.SetToolTip( wx.ToolTip(_('Create a new Prime')) )
		self.newButton.Bind( wx.EVT_BUTTON, self.onNew )
		self.nextPositionButton = wx.Button( self, label=('Next Position') )
		self.nextPositionButton.SetToolTip( wx.ToolTip(_('Create a Prime from an Existing Prime for the Next Position')) )
		self.nextPositionButton.Bind( wx.EVT_BUTTON, self.onNextPosition )
		self.nextPrimeButton = wx.Button( self, label=('Next Prime') )
		self.nextPrimeButton.SetToolTip( wx.ToolTip(_('Create a Prime from an Existing Prime')) )
		self.nextPrimeButton.Bind( wx.EVT_BUTTON, self.onNextPrime )
		self.deleteButton = wx.Button( self, id=wx.ID_DELETE )
		self.deleteButton.SetToolTip( wx.ToolTip(_('Delete a Prime')) )
		self.deleteButton.Bind( wx.EVT_BUTTON, self.onDelete )
		hsButtons = wx.BoxSizer( wx.HORIZONTAL )
		hsButtons.Add( self.photosButton, flag=wx.ALL, border=4 )
		hsButtons.Add( self.finishStrip, flag=wx.ALL, border=4 )
		hsButtons.Add( self.history, flag=wx.ALL, border=4 )
		hsButtons.AddStretchSpacer()
		hsButtons.Add( self.newButton, flag=wx.ALL, border=4 )
		hsButtons.Add( self.nextPositionButton, flag=wx.ALL, border=4 )
		hsButtons.Add( self.nextPrimeButton, flag=wx.ALL, border=4 )
		hsButtons.Add( self.deleteButton, flag=wx.ALL, border=4 )
		
		#---------------------------------------------------------------
		
		vsOverall.Add( self.grid, 1, flag=wx.EXPAND|wx.ALL, border=4 )
		vsOverall.Add( hsButtons, flag=wx.EXPAND|wx.LEFT|wx.RIGHT|wx.BOTTOM, border=4 )
		self.SetSizer( vsOverall )
	
	def onCellChange( self, event ):
		row, col = event.GetRow(), event.GetCol()
		colName = self.colNameFields[col][1]
		GetTranslation = _
		if colName == 'effortCustom':
			if self.grid.GetCellValue(row, col).strip():
				self.grid.SetCellValue( row, col-1, GetTranslation('Custom') )
		elif colName == 'effortType':
			if self.grid.GetCellValue(row, col) != 'Custom':
				self.grid.SetCellValue( row, col+1, u'' )
		elif colName == 'winnerBib':
			bib = int( u''.join(c for c in self.grid.GetCellValue(row, col) if c.isdigit()) )
			self.grid.SetCellValue( row, col+1, getWinnerInfo(bib) )
		
		wx.CallAfter( self.grid.AutoSizeColumns, False )
	
	def getT( self ):
		race = Model.race
		if not race:
			return 0
		row = self.grid.GetGridCursorRow()
		if row is None or row < 0:
			return
		lapsToGo = int( u''.join(c for c in self.grid.GetCellValue(row, self.lapsToGoCol) if c.isdigit()) )
		tMax = 0.0
		for rr in GetResults(None):
			try:
				tMax = min( tMax, rr.raceTimes[-1-lapsToGo] )
			except IndexError:
				pass
		return tMax
		
	def onPhotos( self, event ):
		mainWin = Utils.getMainWin()
		if not mainWin:
			return
		mainWin.photoDialog.SetT( self.getT() )
		mainWin.photoDialog.Show()
		
	def onFinishStrip( self, event ):
		ShowFinishStrip( self, self.getT() )
	
	def onHistory( self, event ):
		mainWin = Utils.getMainWin()
		if not mainWin:
			return
		mainWin.openMenuWindow( 'Passings' )
	
	def selectGridRow( self, row ):
		self.grid.SelectRow( row )
		self.grid.SetGridCursor( row, 0 )
		self.grid.ShowCellEditControl()
	
	def onNew( self, event ):
		race = Model.race
		if not race:
			Utils.MessageOK( self, _('You must have a Race to create a Prime.'), _('Missing Race') )
			return
		self.commit()
		race.primes = getattr(race, 'primes', [])
		rowNew = len( race.primes )
		race.primes.append( {} )
		self.updateGrid()
		self.selectGridRow( rowNew )
	
	def onNextPosition( self, event ):
		rowNext = self.grid.GetGridCursorRow()
		if rowNext is None or rowNext < 0:
			return
		self.commit()
		race = Model.race
		if not race:
			Utils.MessageOK( self, _('You must have a Race to create a next Prime.'), _('Missing Race') )
			return
		
		nextPrime = race.primes[rowNext].copy()
		
		nextPoints = {
			(1, 5):	3,
			(2, 3): 2,
			(3, 2): 1,
		}.get( (nextPrime['position'], nextPrime['points']), None )
		
		if nextPoints is not None:
			nextPrime['points'] = nextPoints
		
		try:
			nextPrime['position'] += 1
		except:
			pass
		nextPrime['winnerBib'] = None
		race.primes = race.primes[:rowNext+1] + [nextPrime] + race.primes[rowNext+1:]
		self.updateGrid()
		self.selectGridRow( rowNext + 1 )
	
	def onNextPrime( self, event ):
		rowNext = self.grid.GetGridCursorRow()
		if rowNext is None or rowNext < 0:
			return
		self.commit()
		race = Model.race
		if not race:
			Utils.MessageOK( self, _('You must have a Race to create a next Prime.'), _('Missing Race') )
			return
		nextPrime = race.primes[rowNext].copy()
		nextPrime['position'] = 1
		if nextPrime['points']:
			nextPrime['points'] = 5
		if nextPrime['lapsToGo'] > 0:
			nextPrime['lapsToGo'] -= 1
		nextPrime['winnerBib'] = None
		race.primes = race.primes[:rowNext+1] + [nextPrime] + race.primes[rowNext+1:]
		self.updateGrid()
		self.selectGridRow( rowNext + 1 )
		
	def onDelete( self, event ):
		rowDelete = self.grid.GetGridCursorRow()
		if rowDelete is None or rowDelete < 0:
			return
		self.commit()
		race = Model.race
		if race and Utils.MessageOKCancel( self, u'{}: {} ?'.format(_('Delete Prime'), rowDelete+1), _('Confirm Delete Primes') ):
			race.primes = getattr(race, 'primes', [])
			try:
				del race.primes[rowDelete]
			except Exception as e:
				return
			self.updateGrid()
			if race.primes:
				self.grid.SetGridCursor( rowDelete, 0 )
		
	def getSponsors( self ):
		race = Model.race
		if not race:
			return []
		sponsors = [prime.get('sponsor', u'') for prime in getattr(race, 'primes', [])] + [race.organizer]
		sponsors = [s for s in sponsors if s]
		return sponsors
		
	def getMerchandise( self ):
		race = Model.race
		if not race:
			return []
		merchandise = [prime.get('merchandise', u'') for prime in getattr(race, 'primes', [])]
		merchandise = [m for m in merchandise if m]
		return merchandise
	
	def setRow( self, prime, row, updateGrid=True ):
		GetTranslation = _
		
		data = []
		for col, (name, attr, dataType) in enumerate(self.colNameFields):
			if attr == 'effortType':
				effortType = prime.get('effortType', 'Pack')
				v = GetTranslation(effortType)
			elif attr == 'position':
				position = prime.get('position', 1)
				v = u'' if position == 0 else unicode(position)
			elif attr == 'points':
				points = prime.get('points', 0)
				v = u'' if points == 0 else unicode(points)
			elif attr == 'winnerBib':
				winnerBib = prime.get('winnerBib', None)
				v = u'' if not winnerBib else unicode(winnerBib)
			elif attr == 'winnerInfo':
				v = getWinnerInfo(winnerBib)
			elif dataType == 'f':
				f = prime.get(attr, 0.0)
				v = u'{:.2f}'.format(f) if f else u''
			elif dataType == 't':
				t = prime.get(attr, 0.0)
				v = Utils.formatTime(t, forceHours=True, twoDigitHours=True) if t != 0 else u''
			else:
				v = unicode(prime.get(attr, u''))
			if updateGrid:
				self.grid.SetCellValue( row, col, v )
			data.append( v )
		
		return data
	
	def getRow( self, row ):
		values = {}
		for col, (name, attr, dataType) in enumerate(self.colNameFields):
			v = self.grid.GetCellValue( row, col ).strip()
			if dataType == 'i':
				v = u''.join( c for c in v if c.isdigit() )
				v = int( v or 0 )
			elif dataType == 'f':
				v = u''.join( c for c in v if c.isdigit() or c == '.')
				v = float( v or 0.0 )
			elif dataType == 't':
				v = Utils.StrToSeconds( v )
				
			if attr == 'position' and not v:
				v = 1
			
			values[attr] = v
		
		GetTranslation = _
		for code, name in EffortChoices:
			if values['effortType'] == GetTranslation(name):
				values['effortType'] = name
				break
		
		if values['effortCustom']:
			values['effortType'] = 'Custom'
		return values
	
	def updateGrid( self ):
		race = Model.race
		if not race or not getattr(race, 'primes', None):
			self.grid.ClearGrid()
			return
		
		Utils.AdjustGridSize( self.grid, len(race.primes) )
		for row, prime in enumerate(race.primes):
			self.setRow( prime, row )
		
		self.grid.AutoSizeColumns( False )								# Resize to fit the column name.
		self.grid.AutoSizeRows( False )
	
	def refresh( self ):
		if self.state.changed():
			self.updateGrid()
		
	def commit( self ):
		self.grid.SaveEditControlValue()	# Make sure the current edit is committed.
		self.grid.DisableCellEditControl()
		race = Model.race
		if not race:
			return
		race.primes = [self.getRow(row) for row in xrange(self.grid.GetNumberRows())]
Exemplo n.º 5
0
class Races(wx.Panel):
    #----------------------------------------------------------------------
    headerNames = ['Race', 'Grade', 'Points', 'Team Pts', 'Race File']

    RaceCol = 0
    GradeCol = 1
    PointsCol = 2
    TeamPointsCol = 3
    RaceFileCol = 4
    RaceStatusCol = 5

    def __init__(self, parent):
        wx.Panel.__init__(self, parent)

        self.seriesNameLabel = wx.StaticText(self, label='Series Name:')
        self.seriesName = wx.TextCtrl(self)

        self.organizerNameLabel = wx.StaticText(self, label='Organizer:')
        self.organizerName = wx.TextCtrl(self)

        self.explanation = wx.StaticText(
            self,
            label=u'\n'.join([
                _("Add all the races in your Series."),
                _("Make sure the races are in chronological order."),
                _("You can change the order by dragging-and-dropping the first grey column in the table."
                  ),
                u'',
                _("Configure the Points Structures or Time Scoring parameters on the Scoring Criteria page."
                  ),
                _("Each race can have its own Points Structure.  For example, you could create 'Double Points' for one race."
                  ),
                u'',
                _("Race results are shown Last-to-First in the output by default."
                  ),
                _("You can change this on the Options page."),
            ]))

        self.grid = ReorderableGrid(self, style=wx.BORDER_SUNKEN)
        self.grid.DisableDragRowSize()
        self.grid.SetRowLabelSize(64)
        self.grid.CreateGrid(0, len(self.headerNames))
        for col in range(self.grid.GetNumberCols()):
            self.grid.SetColLabelValue(col, self.headerNames[col])

        self.pointsChoiceEditor = gridlib.GridCellChoiceEditor(
            [], allowOthers=False)
        attr = gridlib.GridCellAttr()
        attr.SetEditor(self.pointsChoiceEditor)
        self.grid.SetColAttr(self.PointsCol, attr)

        self.teamPointsChoiceEditor = gridlib.GridCellChoiceEditor(
            [], allowOthers=False)
        attr = gridlib.GridCellAttr()
        attr.SetEditor(self.teamPointsChoiceEditor)
        self.grid.SetColAttr(self.TeamPointsCol, attr)

        attr = gridlib.GridCellAttr()
        attr.SetReadOnly(True)
        self.grid.SetColAttr(self.RaceCol, attr)

        attr = gridlib.GridCellAttr()
        attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
        self.grid.SetColAttr(self.GradeCol, attr)
        '''
		attr = gridlib.GridCellAttr()
		attr.SetReadOnly( True )
		self.grid.SetColAttr( self.RaceStatusCol, attr )
		'''

        attr = gridlib.GridCellAttr()
        attr.SetReadOnly(True)
        self.grid.SetColAttr(self.RaceFileCol, attr)

        self.grid.Bind(gridlib.EVT_GRID_CELL_CHANGED, self.onGridChange)
        self.gridAutoSize()
        self.grid.Bind(wx.grid.EVT_GRID_EDITOR_CREATED,
                       self.onGridEditorCreated)
        self.grid.Bind(wx.grid.EVT_GRID_CELL_LEFT_CLICK,
                       self.onEditRaceFileName)

        self.addButton = wx.Button(self, wx.ID_ANY, 'Add Races')
        self.addButton.Bind(wx.EVT_BUTTON, self.doAddRace)

        self.removeButton = wx.Button(self, wx.ID_ANY, 'Remove Race')
        self.removeButton.Bind(wx.EVT_BUTTON, self.doRemoveRace)

        fgs = wx.FlexGridSizer(rows=2, cols=2, vgap=2, hgap=2)
        fgs.AddGrowableCol(1, proportion=1)

        fgs.Add(self.seriesNameLabel,
                flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        fgs.Add(self.seriesName, flag=wx.EXPAND)
        fgs.Add(self.organizerNameLabel,
                flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALIGN_RIGHT)
        fgs.Add(self.organizerName, flag=wx.EXPAND)

        hs = wx.BoxSizer(wx.HORIZONTAL)
        hs.Add(self.addButton, 0, flag=wx.ALL, border=4)
        hs.Add(self.removeButton, 0, flag=wx.ALL, border=4)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(fgs, 0, flag=wx.EXPAND | wx.ALL, border=4)
        sizer.Add(self.explanation, 0, flag=wx.EXPAND | wx.ALL, border=4)
        sizer.Add(hs, 0, flag=wx.EXPAND)
        sizer.Add(self.grid, 1, flag=wx.EXPAND | wx.ALL, border=6)
        self.SetSizer(sizer)

    def getGrid(self):
        return self.grid

    wildcard = 'CrossMgr or Excel files (*.cmn, *.xlsx, *.xlsm, *.xls)|*.cmn;*.xlsx;*.xlsm;*.xls;'

    def onEditRaceFileName(self, event):
        col = event.GetCol()
        if col != self.RaceFileCol:
            event.Skip()
            return

        row = event.GetRow()
        dlg = wx.FileDialog(self,
                            message="Choose a CrossMgr or Excel file",
                            defaultFile='',
                            wildcard=self.wildcard,
                            style=wx.FD_OPEN | wx.FD_CHANGE_DIR)
        ret = dlg.ShowModal()
        fileName = ''
        if ret == wx.ID_OK:
            fileName = dlg.GetPath()
            self.grid.SetCellValue(row, self.RaceCol,
                                   SeriesModel.RaceNameFromPath(fileName))
            self.grid.SetCellValue(row, self.RaceFileCol, fileName)
        dlg.Destroy()
        self.commit()

    def doAddRace(self, event):
        dlg = wx.FileDialog(self,
                            message="Choose a CrossMgr or Excel file",
                            defaultFile='',
                            wildcard=self.wildcard,
                            style=wx.FD_OPEN | wx.FD_CHANGE_DIR
                            | wx.FD_MULTIPLE)
        ret = dlg.ShowModal()
        if ret == wx.ID_OK:
            for fileName in dlg.GetPaths():
                SeriesModel.model.addRace(fileName)
        dlg.Destroy()
        self.refresh()

    def doRemoveRace(self, event):
        row = self.grid.GetGridCursorRow()
        if row < 0:
            Utils.MessageOK(
                self, 'No Selected Race.\nPlease Select a Race to Remove.',
                'No Selected Race')
            return
        if Utils.MessageOKCancel(
                self, 'Confirm Remove Race:\n\n    {}'.format(
                    self.grid.GetCellValue(row, 0)), 'Remove Race'):
            self.grid.DeleteRows(row)
            self.commit()

    def updatePointsChoices(self):
        try:
            comboBox = self.comboBox
        except AttributeError:
            return
        comboBox.SetItems([p.name for p in SeriesModel.model.pointStructures])

    def updateTeamPointsChoices(self):
        try:
            teamComboBox = self.teamComboBox
        except AttributeError:
            return
        teamComboBox.SetItems(
            [''] + [p.name for p in SeriesModel.model.pointStructures])

    def onGridEditorCreated(self, event):
        if event.GetCol() == self.PointsCol:
            self.comboBox = event.GetControl()
            self.updatePointsChoices()
        elif event.GetCol() == self.TeamPointsCol:
            self.teamComboBox = event.GetControl()
            self.updateTeamPointsChoices()
        event.Skip()

    def gridAutoSize(self):
        self.grid.AutoSize()
        self.grid.EnableDragGridSize(False)
        self.grid.EnableDragColSize(False)
        self.Layout()
        self.Refresh()

    def onGridChange(self, event):
        wx.CallAfter(self.gridAutoSize)

    def refresh(self):
        model = SeriesModel.model
        Utils.AdjustGridSize(self.grid, len(model.races))
        for row, race in enumerate(model.races):
            self.grid.SetCellValue(row, self.RaceCol, race.getRaceName())
            self.grid.SetCellValue(row, self.GradeCol, race.grade)
            self.grid.SetCellValue(row, self.PointsCol,
                                   race.pointStructure.name)
            self.grid.SetCellValue(
                row, self.TeamPointsCol, race.teamPointStructure.name
                if race.teamPointStructure else u'')
            self.grid.SetCellValue(row, self.RaceFileCol, race.fileName)
        wx.CallAfter(self.gridAutoSize)

        self.seriesName.SetValue(SeriesModel.model.name)
        self.organizerName.SetValue(SeriesModel.model.organizer)

    def commit(self):
        self.grid.SaveEditControlValue()
        self.grid.DisableCellEditControl(
        )  # Make sure the current edit is committed.

        raceList = []
        for row in range(self.grid.GetNumberRows()):
            race = SeriesModel.model.races[row]
            fileName = self.grid.GetCellValue(row, self.RaceFileCol).strip()
            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]
            if not (grade and ord(u'A') <= ord(grade) <= ord(u'Z')):
                grade = u'A'
            if not fileName or not pname:
                continue
            raceList.append((fileName, pname, pteamname, grade))

        model = SeriesModel.model
        model.setRaces(raceList)

        if self.seriesName.GetValue() != model.name:
            model.name = self.seriesName.GetValue()
            model.changed = True

        if self.organizerName.GetValue() != model.organizer:
            model.organizer = self.organizerName.GetValue()
            model.changed = True

        wx.CallAfter(self.refresh)
Exemplo n.º 6
0
class Prizes(wx.Panel):
    rowsMax = 20

    def __init__(self, parent, id=wx.ID_ANY, size=wx.DefaultSize):
        super(Prizes, self).__init__(parent, id, size=size)

        vsOverall = wx.BoxSizer(wx.VERTICAL)

        self.grid = ReorderableGrid(self)
        self.grid.CreateGrid(0, 10)

        self.grid.AutoSizeColumns(False)
        self.grid.AutoSizeRows(False)

        self.grid.Bind(wx.grid.EVT_GRID_CELL_CHANGED, self.onCellChange)

        #---------------------------------------------------------------

        vsOverall.Add(self.grid, 1, flag=wx.EXPAND | wx.ALL, border=4)
        self.SetSizer(vsOverall)

    def onCellChange(self, event):
        race = Model.race
        if not race:
            return
        row, col = event.GetRow(), event.GetCol()
        if col & 1:
            return
        categories = race.getCategories(startWaveOnly=False, publishOnly=True)
        if self.grid.GetNumberCols() != len(categories) * 2:
            Utils.AdjustGridSize(self.grid, self.rowsMax, len(categories) * 2)
        if row >= self.grid.GetNumberRows() or col >= self.grid.GetNumberCols(
        ):
            return
        self.copyToRace()
        try:
            category = categories[col // 2]
        except IndexError:
            return
        self.grid.SetCellValue(
            row, col + 1,
            self.getRecepient(self.grid.GetCellValue(row, col), row, category))
        wx.CallAfter(self.grid.AutoSizeColumns, False)

    def getRecepient(self, prize, row, category):
        if not prize:
            return ''
        name = ''
        results = GetResults(category)
        try:
            name = u'{}: {}'.format(results[row].num, results[row].full_name())
        except IndexError:
            pass
        return name

    def setCellPair(self, row, col, category):
        try:
            prize = getattr(category, 'prizes', [])[row]
        except IndexError:
            prize = ''
        self.grid.SetCellValue(row, col, prize)
        self.grid.SetCellValue(row, col + 1,
                               self.getRecepient(prize, row, category))

    def updateGrid(self):
        race = Model.race
        if not race:
            self.grid.ClearGrid()
            return
        categories = race.getCategories(startWaveOnly=False, publishOnly=True)
        Utils.AdjustGridSize(self.grid, self.rowsMax, len(categories) * 2)
        col = 0
        for category in categories:
            fullname = category.fullname
            ib = fullname.rfind('(')
            catname, catgender = fullname[:ib].strip(), fullname[ib:]
            colName = '{}\n{}'.format(catname, catgender)
            self.grid.SetColLabelValue(col, colName)
            attr = wx.grid.GridCellAttr()
            attr.SetReadOnly(False)
            attr.SetAlignment(wx.ALIGN_CENTRE, wx.ALIGN_CENTRE)
            self.grid.SetColAttr(col, attr)

            self.grid.SetColLabelValue(col + 1, _('Recipient'))
            attr = wx.grid.GridCellAttr()
            attr.SetReadOnly(True)
            attr.SetAlignment(wx.ALIGN_LEFT, wx.ALIGN_CENTRE)
            attr.SetBackgroundColour(wx.Colour(152, 251, 152))
            self.grid.SetColAttr(col + 1, attr)

            for row in range(self.rowsMax):
                self.setCellPair(row, col, category)
            col += 2

        self.grid.AutoSizeColumns(False)  # Resize to fit the column name.
        self.grid.AutoSizeRows(False)

    def refresh(self):
        self.updateGrid()

    def copyToRace(self):
        race = Model.race
        if not race:
            return

        categories = race.getCategories(startWaveOnly=False, publishOnly=True)
        for i, category in enumerate(categories):
            prizes = []
            for row in range(self.rowsMax):
                v = self.grid.GetCellValue(row, i * 2).strip()
                if not v:
                    break
                prizes.append(v)
            category.prizes = prizes

    def commit(self):
        self.grid.SaveEditControlValue(
        )  # Make sure the current edit is committed.
        self.grid.DisableCellEditControl()
        self.copyToRace()