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( (' ' * 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)