Example #1
0
class TableModel(QtCore.QAbstractTableModel):
    TRANSLATE_SECTION = [
        Section('gender', 'M/F', '%s', None, False),
        Section('flight', 'Flight', '%d', 'toInt', False),
        Section('team', 'Team', '%s', None, False),
        Section('name', 'Name', '%s', None, False),
        Section('weight', 'Weight', '%.1f', None, False),
        Section('rack_height', 'Rack Height', '%d', 'toInt', False),
        Section('squat_0', 'Squat 1', '%.1f', 'toDouble', True),
        Section('squat_1', 'Squat 2', '%.1f', 'toDouble', True),
        Section('squat_2', 'Squat 3', '%.1f', 'toDouble', True),
        Section('bench_0', 'Bench 1', '%.1f', 'toDouble', True),
        Section('bench_1', 'Bench 2', '%.1f', 'toDouble', True),
        Section('bench_2', 'Bench 3', '%.1f', 'toDouble', True),
        Section('deadlift_0', 'Deadlift 1', '%.1f', 'toDouble', True),
        Section('deadlift_1', 'Deadlift 2', '%.1f', 'toDouble', True),
        Section('deadlift_2', 'Deadlift 3', '%.1f', 'toDouble', True),
        Section('total', 'Total', '%.1f', None, False),
        Section('points', 'Points', '%.2f', None, False)
    ]

    model_changed = QtCore.pyqtSignal()

    def __init__(self, top=3, parent=None):
        QtCore.QAbstractTableModel.__init__(self, parent)

        self.flight_filter = None
        self.last_clicked = None
        self.next_sort = QtCore.Qt.AscendingOrder

        self.lifters_map = LifterCollection(top=top)
        self.sorted_by('lifter_id')

        self.reset()

    # Required Qt methods
    def headerData(self, section, orient, role):
        if role == QtCore.Qt.DisplayRole and orient == QtCore.Qt.Horizontal:
            section_info = self.TRANSLATE_SECTION[section]

            if section_info.attribute == 'flight':
                if self.flight_filter is not None:
                    return section_info.heading + ' [%d]' % self.flight_filter

            return section_info.heading

        return QtCore.QVariant()

    def index_to_lifter(self, index):
        if not index.isValid():
            return None

        lifter_index, section = index.row(), index.column()
        lifter = self.lifters[lifter_index]
        section_info = self.TRANSLATE_SECTION[section]

        return lifter, section_info

    def data(self, index, role):
        if not index.isValid():
            return QtCore.QVariant()

        # Get active lifter and section info
        lifter, section_info = self.index_to_lifter(index)

        if role == QtCore.Qt.DisplayRole:
            # Get section info
            value = getattr(lifter, section_info.attribute)
            return section_info.format % value

        # Handle lift representation here
        elif role == QtCore.Qt.ForegroundRole:
            pass
        elif role == QtCore.Qt.BackgroundRole:
            pass
        elif role == QtCore.Qt.FontRole:
            if section_info.is_lift:
                # Translate attribute string into lift and attempt
                lift, attempt_str = section_info.attribute.split('_')
                attempt = int(attempt_str)

                # Get record
                record = lifter.get_lift(lift, attempt)[0]

                # Set font accordingly
                font = QtGui.QFont()
                if record == Lifter.GOOD_LIFT:
                    font.setBold(True)
                elif record == Lifter.FAIL_LIFT:
                    font.setStrikeOut(True)
                elif record == Lifter.PASS_LIFT:
                    font.setStrikeOut(True)
                    font.setItalic(True)
                elif record == Lifter.SET_LIFT:
                    font.setItalic(True)

                return font

        return QtCore.QVariant()

    def setData(self, index, value, role):
        if not index.isValid():
            return False

        if role == QtCore.Qt.EditRole:
            # Resolve lifter and section
            lifter, section_info = self.index_to_lifter(index)

            # None conversion means it isn't editable
            if section_info.conversion is None:
                return False

            # Convert value
            value, ok = getattr(value, section_info.conversion)()
            if not ok:
                return False

            # Catch entry error
            try:
                setattr(lifter, section_info.attribute, value)
            except ValueError as ex:
                logger.error('Previous attempt not completed.\n%s', ex.message)
                return False

            # Emit change over the specified index
            top_left = self.index(index.row(), 0)
            bottom_right = self.index(index.row(), self.columnCount(None)-1)
            self.dataChanged.emit(top_left, bottom_right)

            # Emit change of the model
            self.model_changed.emit()

            return True

        return False

    def validate_lift(self, index, valid):
        if not index.isValid():
            return

        # Get lifter and section if valid
        lifter, section_info = self.index_to_lifter(index)

        # If section is not a lift then don't do anything
        if not section_info.is_lift:
            return

        # Translate attribute string into lift and attempt
        lift, attempt_str = section_info.attribute.split('_')
        attempt = int(attempt_str)

        # Validate the lift
        lifter.validate_lift(lift, attempt, valid)

        # Emit signals
        self.model_changed.emit()
        self.dataChanged.emit(index, index)

    def flags(self, index):
        if not index.isValid():
            return QtCore.Qt.NoItemFlags

        # Default flags
        flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled

        # Resolve index and check if also editable
        lifter, section_info = self.index_to_lifter(index)

        if section_info.conversion is not None:
            flags |= QtCore.Qt.ItemIsEditable

        return flags

    def rowCount(self, parent):
        return len(self.lifters)

    def columnCount(self, parent):
        return len(self.TRANSLATE_SECTION)

    def section_clicked(self, section):
        section_info = self.TRANSLATE_SECTION[section]

        # If flight
        if section_info.attribute == 'flight':
            # Get flights
            flights = self.lifters_map.flights()

            # Get next flight filter
            reset_filter = True
            try:
                index = flights.index(self.flight_filter) + 1
            except ValueError:
                pass
            else:
                reset_filter = False
                if index >= len(flights):
                    self.flight_filter = None
                else:
                    self.flight_filter = flights[index]

            if reset_filter:
                self.flight_filter = flights[0]

        # If NOT flight, sort by attribute, then weight and lifter_id
        else:
            next_sort = QtCore.Qt.AscendingOrder

            if self.last_clicked is not None and \
                self.last_clicked == section_info.attribute:

                if self.next_sort == QtCore.Qt.AscendingOrder:
                    next_sort = QtCore.Qt.DescendingOrder

            self.next_sort = next_sort

            self.last_clicked = section_info.attribute
            self.sorted_by(section_info.attribute, 'weight', 'lifter_id')

        # All paths lead to large change
        self.reset()

    def reset(self):
        # Apply flight filter and reset it if required
        reset_all = True

        if self.flight_filter is not None:
            self.lifters = [l for l in self.lifters_ if l.flight == \
                self.flight_filter]

            if len(self.lifters) > 0:
                reset_all = False

        if reset_all:
            self.lifters = self.lifters_[:]
            self.flight_filter = None

        # reset
        QtCore.QAbstractTableModel.reset(self)

    # Sort method
    def sorted_by(self, *args):
        if len(args) > 0:
            # Save args
            self.sort_args = args

        # Sort based on args
        sort_args = list(self.sort_args)

        # Only reverse first attribute as others are used to tie-break
        if self.next_sort == QtCore.Qt.DescendingOrder:
            sort_args[0] = 'REV_' + sort_args[0]

        self.lifters_ = self.lifters_map.sorted_by(*sort_args)

    # Add / remove methods
    def add(self, lifter):
        self.lifters_map.add(lifter)

        self.sorted_by()
        self.reset()

        # Emit change of model
        self.model_changed.emit()

    def remove(self, index):
        if not index.isValid():
            return

        lifter, section_info = self.index_to_lifter(index)
        self.lifters_map.remove(lifter)

        self.sorted_by()
        self.reset()

        # Emit change of model
        self.model_changed.emit()

    # Save / load / export
    def save(self, file_):
        pickle_.dump(file_, self.lifters_map)

    def load(self, file_):
        self.lifters_map = pickle_.load(file_)

        self.sorted_by()
        self.reset()

    def export(self, file_):
        # Results summary

        # Get overall info
        best_lifter, best_total, team_info = self.lifters_map.overall_info()

        # Overall
        best_team = '%s [%.2f]' % best_total
        best_lifter = '%s [%.2f]' % \
            (best_lifter.name, best_lifter.points)

        # Team summary
        tsum = ''

        # Headers
        tsum += '<tr>'
        for heading in ['Team / lifter', 'Points']:
            tsum += '<th>%s</th>' % heading
        tsum += '</tr>\n'

        # Summary
        row = 0
        for team, info in team_info.iteritems():
            # Prepare data to output
            data = [(team, info[0])]
            for lifter in info[1]:
                data.append( ('&nbsp;' * 4 + lifter.name, lifter.points) )

            # Output the data
            for perf, points in data:
                # Alternate colours of rows
                if row % 2 == 1:
                    row_str = '<tr class="alt">'
                else:
                    row_str = '<tr>'

                # Manually increment row
                row += 1

                row_str += '<td>%s</td><td>%.2f</td></tr>\n' % (perf, points)
                tsum += row_str

        # Main results
        tbody = ''

        # Headers
        tbody += '<tr>'
        for section_info in self.TRANSLATE_SECTION:
            tbody += '<th>%s</th>' % section_info.heading
        tbody += '</tr>\n'

        # Get lifters sorted by points, then weight, then id
        lifters = self.lifters_map.sorted_by(
            'REV_points', 'weight', 'lifter_id'
        )

        # Results table
        for row, lifter in enumerate(lifters):
            # Alternate colours of rows
            if row % 2 == 1:
                row_str = '<tr class="alt">'
            else:
                row_str = '<tr>'

            # Add data
            for section_info in self.TRANSLATE_SECTION:
                # Get data as string
                value = getattr(lifter, section_info.attribute)
                data = section_info.format % value

                # If a lift, set up style string
                style_str = '"'
                if section_info.is_lift:
                    # Translate attribute string into lift and attempt
                    lift, attempt_str = section_info.attribute.split('_')
                    attempt = int(attempt_str)

                    # Get record
                    record = lifter.get_lift(lift, attempt)[0]

                    # Set font accordingly
                    if record == Lifter.GOOD_LIFT:
                        style_str += 'font-weight:bold;'
                    elif record == Lifter.FAIL_LIFT:
                        style_str += 'text-decoration:line-through;'
                    elif record == Lifter.PASS_LIFT:
                        style_str += 'text-decoration:line-through;' \
                            'font-style:italic;'
                    elif record == Lifter.SET_LIFT:
                        style_str += 'font-style:italic;'

                style_str += '"'

                # If style str is added
                if len(style_str) > 2:
                    row_str += '<td style=%s>%s</td>' % (style_str, data)
                else:
                    row_str += '<td>%s</td>' % data

            row_str += '</tr>\n'
            tbody += row_str

        # XXX Set title
        title = ''

        # Save full table
        html_table = HTML_TEMPLATE.substitute(
            title=title,
            best_team=best_team,
            best_lifter=best_lifter,
            tsum=tsum,
            tbody=tbody)
        with open(file_, 'w') as fp:
            fp.write(html_table)