def define_columns(): d = ColumnDefinition # fmt:off # pylint: disable=line-too-long columns_dict = { Column.ACTIONS: d('', "", width=60, sortable=False), Column.CATEGORY: d('category', "", width=30, tooltip_filter=lambda data: data), Column.NAME: d('name', tr("Name"), width=EXPANDING), Column.SIZE: d('size', tr("Size"), width=90, display_filter=lambda data: (format_size(float(data)) if data != "" else "")), Column.HEALTH: d( 'health', tr("Health"), width=100, tooltip_filter=lambda data: f"{data}" + ('' if data == HEALTH_CHECKING else '\n(Click to recheck)'), ), Column.UPDATED: d( 'updated', tr("Updated"), width=120, display_filter=lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', ), Column.VOTES: d( 'votes', tr("Popularity"), width=120, display_filter=format_votes, tooltip_filter=lambda data: get_votes_rating_description(data) if data is not None else None, ), Column.STATUS: d('status', "", sortable=False), Column.STATE: d('state', "", width=80, tooltip_filter=lambda data: data, sortable=False), Column.TORRENTS: d('torrents', tr("Torrents"), width=90), Column.SUBSCRIBED: d('subscribed', tr("Subscribed"), width=90), } # pylint: enable=line-too-long # fmt:on return columns_dict
class ChannelContentModel(RemoteTableModel): columns = [ACTION_BUTTONS, 'category', 'name', 'size', 'health', 'updated'] column_headers = [ '', '', tr('Name'), tr('Size'), tr('Health'), tr('Updated') ] unsortable_columns = ['status', 'state', ACTION_BUTTONS] column_flags = { 'subscribed': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'category': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'name': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'torrents': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'size': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'updated': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'health': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'votes': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'state': Qt.ItemIsEnabled | Qt.ItemIsSelectable, 'status': Qt.ItemIsEnabled | Qt.ItemIsSelectable, ACTION_BUTTONS: Qt.ItemIsEnabled | Qt.ItemIsSelectable, } column_width = { 'state': lambda _: 100, 'subscribed': lambda _: 100, 'name': lambda table_width: table_width - 450, 'action_buttons': lambda _: 50, 'category': lambda _: 30, } column_tooltip_filters = { 'state': lambda data: data, 'votes': lambda data: get_votes_rating_description(data) if data is not None else None, 'category': lambda data: data, 'health': lambda data: f"{data}" + ('' if data == HEALTH_CHECKING else '\n(Click to recheck)'), } column_display_filters = { 'size': lambda data: (format_size(float(data)) if data != '' else ''), 'votes': format_votes, 'updated': lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', } def __init__( self, channel_info=None, hide_xxx=None, exclude_deleted=None, subscribed_only=None, endpoint_url=None, text_filter='', type_filter=None, ): RemoteTableModel.__init__(self, parent=None) self.column_position = {name: i for i, name in enumerate(self.columns)} # Remote query (model) parameters self.hide_xxx = hide_xxx self.text_filter = text_filter self.subscribed_only = subscribed_only self.exclude_deleted = exclude_deleted self.type_filter = type_filter self.category_filter = None # Current channel attributes. This is intentionally NOT copied, so local changes # can propagate to the origin, e.g. parent channel. self.channel_info = channel_info or { "name": "My channels", "status": 123 } self.endpoint_url_override = endpoint_url # Load the initial batch of entries self.perform_query() @property def edit_enabled(self): return False @property def endpoint_url(self): return self.endpoint_url_override or "channels/%s/%i" % ( self.channel_info["public_key"], self.channel_info["id"], ) def headerData(self, num, orientation, role=None): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.column_headers[num] if role == Qt.InitialSortOrderRole and num != self.column_position.get( 'name'): return Qt.DescendingOrder if role == Qt.TextAlignmentRole: return (Qt.AlignHCenter if num in [ self.column_position.get('subscribed'), self.column_position.get('torrents') ] else Qt.AlignLeft) return super().headerData(num, orientation, role) def rowCount(self, *_, **__): return len(self.data_items) def columnCount(self, *_, **__): return len(self.columns) def flags(self, index): return self.column_flags[self.columns[index.column()]] def filter_item_txt(self, txt_filter, index, show_default=True): # ACHTUNG! Dumb workaround for some mysterious race condition try: item = self.data_items[index.row()] except IndexError: return "" column = self.columns[index.column()] data = item.get(column, '') # Print number of torrents in the channel for channel rows in the "size" column if (column == "size" and "torrents" not in self.columns and "torrents" in item and item["type"] in (CHANNEL_TORRENT, COLLECTION_NODE)): return item["torrents"] # 'subscribed' column gets special treatment in case of ToolTipRole, because # its tooltip uses information from both 'subscribed' and 'state' keys if (column == 'subscribed' and txt_filter == self.column_tooltip_filters and 'subscribed' in item and 'state' in item): state_message = f" ({item['state']})" if item[ 'state'] != CHANNEL_STATE.COMPLETE.value else "" tooltip_txt = ( f"Subscribed.{state_message}\n(Click to unsubscribe)" if item['subscribed'] else "Not subscribed.\n(Click to subscribe)") return tooltip_txt if column in txt_filter: display_txt = txt_filter.get(column, str(data))(data) elif show_default: display_txt = data else: display_txt = None return display_txt def data(self, index, role): if role in (Qt.DisplayRole, Qt.EditRole): return self.filter_item_txt(self.column_display_filters, index) if role == Qt.ToolTipRole: return self.filter_item_txt(self.column_tooltip_filters, index, show_default=False) if role == Qt.TextAlignmentRole: if index.column() == self.column_position.get('votes', -1): return Qt.AlignLeft | Qt.AlignVCenter if index.column() == self.column_position.get('torrents', -1): return Qt.AlignHCenter | Qt.AlignVCenter return None def reset(self): self.item_uid_map.clear() super().reset() def update_node_info(self, update_dict): """ This method updates/inserts rows based on updated_dict. It should be typically invoked by a signal from Events endpoint. One special case it when the channel_info of the model itself is updated. In that case, info_changed signal is emitted, so the controller/widget knows it is time to update the labels. """ if (self.channel_info.get("public_key") == update_dict.get("public_key") is not None and self.channel_info.get("id") == update_dict.get("id") is not None): self.channel_info.update(**update_dict) self.info_changed.emit([]) return row = self.item_uid_map.get(get_item_uid(update_dict)) if row is not None and row < len(self.data_items): self.data_items[row].update(**update_dict) self.dataChanged.emit(self.index(row, 0), self.index(row, len(self.columns)), []) def perform_query(self, **kwargs): """ Fetch search results. """ if self.type_filter is not None: kwargs.update({"metadata_type": self.type_filter}) else: kwargs.update( {"metadata_type": [REGULAR_TORRENT, COLLECTION_NODE]}) if self.subscribed_only is not None: kwargs.update({"subscribed": self.subscribed_only}) if self.exclude_deleted is not None: kwargs.update({"exclude_deleted": self.exclude_deleted}) if self.category_filter is not None: if self.category_filter == "Channels": kwargs.update({'metadata_type': 'channel'}) else: kwargs.update({"category": self.category_filter}) if "total" not in self.channel_info: # Only include total for the first query to the endpoint kwargs.update({"include_total": 1}) super().perform_query(**kwargs) def setData(self, index, new_value, role=None): if role != Qt.EditRole: return True item = self.data_items[index.row()] attribute_name = self.columns[index.column()] attribute_name = 'tags' if attribute_name == 'category' else attribute_name attribute_name = 'title' if attribute_name == 'name' else attribute_name if attribute_name == 'subscribed': return True def on_row_update_results(response): if not response: return item_row = self.item_uid_map.get(get_item_uid(item)) if item_row is None: return data_item_dict = index.model().data_items[item_row] data_item_dict.update(response) self.info_changed.emit([data_item_dict]) TriblerNetworkRequest( f"metadata/{item['public_key']}/{item['id']}", on_row_update_results, method='PATCH', raw_data=json.dumps({attribute_name: new_value}), ) # ACHTUNG: instead of reloading the whole row from DB, this line just changes the displayed value! self.data_items[index.row()][self.columns[index.column()]] = new_value return True def on_new_entry_received(self, response): self.on_query_results(response, remote=True)
class ChannelContentModel(RemoteTableModel): columns = [ u'category', u'name', u'size', u'health', u'updated', ACTION_BUTTONS ] column_headers = [ u'Category', u'Name', u'Size', u'Health', u'Updated', u'' ] unsortable_columns = [u'status', u'state', ACTION_BUTTONS] column_flags = { u'subscribed': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'category': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'name': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'torrents': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'size': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'updated': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'health': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'votes': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'state': Qt.ItemIsEnabled | Qt.ItemIsSelectable, u'status': Qt.ItemIsEnabled | Qt.ItemIsSelectable, ACTION_BUTTONS: Qt.ItemIsEnabled | Qt.ItemIsSelectable, } column_width = { u'state': lambda _: 20, u'name': lambda table_width: table_width - 510, u'action_buttons': lambda _: 70, } column_tooltip_filters = { u'state': lambda data: data, u'votes': lambda data: "{0:.0%}".format(float(data)) if data else None, } column_display_filters = { u'size': lambda data: (format_size(float(data)) if data != '' else ''), u'votes': format_votes, u'state': lambda data: str(data)[:1] if data == u'Downloading' else "", u'updated': lambda timestamp: pretty_date(timestamp) if timestamp and timestamp > BITTORRENT_BIRTHDAY else 'N/A', } def __init__( self, channel_info=None, hide_xxx=None, exclude_deleted=None, subscribed_only=None, endpoint_url=None, text_filter='', ): RemoteTableModel.__init__(self, parent=None) self.column_position = {name: i for i, name in enumerate(self.columns)} self.data_items = [] # Remote query (model) parameters self.hide_xxx = hide_xxx self.text_filter = text_filter self.subscribed_only = subscribed_only self.exclude_deleted = exclude_deleted self.type_filter = None self.category_filter = None # Current channel attributes. This is intentionally NOT copied, so local changes # can propagate to the origin, e.g. parent channel. self.channel_info = channel_info or { "name": "My channels", "status": 123 } self.endpoint_url_override = endpoint_url # Load the initial batch of entries self.perform_query() @property def edit_enabled(self): return False @property def endpoint_url(self): return self.endpoint_url_override or "channels/%s/%i" % ( self.channel_info["public_key"], self.channel_info["id"], ) def headerData(self, num, orientation, role=None): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return self.column_headers[num] if role == Qt.InitialSortOrderRole and num != self.column_position.get( 'name'): return Qt.DescendingOrder def rowCount(self, parent=QModelIndex()): return len(self.data_items) def columnCount(self, parent=QModelIndex()): return len(self.columns) def flags(self, index): return self.column_flags[self.columns[index.column()]] def filter_item_txt(self, txt_filter, index, show_default=True): # FIXME: dumb workaround for some mysterious race condition try: item = self.data_items[index.row()] except IndexError: return "" column = self.columns[index.column()] data = item.get(column, u'') # Print number of torrents in the channel for channel rows in the "size" column if (column == "size" and "torrents" not in self.columns and "torrents" in item and item["type"] in (CHANNEL_TORRENT, COLLECTION_NODE)): return item["torrents"] if column in txt_filter: display_txt = txt_filter.get(column, str(data))(data) elif show_default: display_txt = data else: display_txt = None return display_txt def data(self, index, role): if role == Qt.DisplayRole or role == Qt.EditRole: return self.filter_item_txt(self.column_display_filters, index) elif role == Qt.ToolTipRole: return self.filter_item_txt(self.column_tooltip_filters, index, show_default=False) elif role == Qt.TextAlignmentRole: if index.column() == self.column_position.get(u'votes', -1): return Qt.AlignLeft | Qt.AlignVCenter return None def reset(self): self.item_uid_map.clear() super(ChannelContentModel, self).reset() def update_node_info(self, update_dict): """ This method updates/inserts rows based on updated_dict. It should be typically invoked by a signal from Events endpoint. One special case it when the channel_info of the model itself is updated. In that case, info_changed signal is emitted, so the controller/widget knows it is time to update the labels. """ # TODO: better mechanism for identifying channel entries for pushing updates if (self.channel_info.get("public_key") == update_dict.get("public_key") is not None and self.channel_info.get("id") == update_dict.get("id") is not None): self.channel_info.update(**update_dict) self.info_changed.emit([]) row = self.item_uid_map.get(get_item_uid(update_dict)) if row in self.data_items: self.data_items[row].update(**update_dict) self.dataChanged.emit(self.index(row, 0), self.index(row, len(self.columns)), []) def perform_query(self, **kwargs): """ Fetch search results. """ if self.type_filter is not None: kwargs.update({"metadata_type": self.type_filter}) if self.subscribed_only is not None: kwargs.update({"subscribed": self.subscribed_only}) if self.exclude_deleted is not None: kwargs.update({"exclude_deleted": self.exclude_deleted}) if self.category_filter is not None: if self.category_filter == "Channels": kwargs.update({'metadata_type': 'channel'}) else: kwargs.update({"category": self.category_filter}) if "total" not in self.channel_info: # Only include total for the first query to the endpoint kwargs.update({"include_total": 1}) super(ChannelContentModel, self).perform_query(**kwargs) def setData(self, index, new_value, role=None): if role != Qt.EditRole: return True item = self.data_items[index.row()] attribute_name = self.columns[index.column()] attribute_name = u'tags' if attribute_name == u'category' else attribute_name attribute_name = u'title' if attribute_name == u'name' else attribute_name attribute_name = u'subscribed' if attribute_name == u'votes' else attribute_name def on_row_update_results(response): if not response: return item_row = self.item_uid_map.get(get_item_uid(item)) if item_row is None: return data_item_dict = index.model().data_items[item_row] data_item_dict.update(response) self.info_changed.emit([data_item_dict]) TriblerNetworkRequest( f"metadata/{item['public_key']}/{item['id']}", on_row_update_results, method='PATCH', raw_data=json.dumps({attribute_name: new_value}), ) # TODO: reload the whole row from DB instead of just changing the displayed value self.data_items[index.row()][self.columns[index.column()]] = new_value return True def on_new_entry_received(self, response): self.on_query_results(response, remote=True)