示例#1
0
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
示例#2
0
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)
示例#3
0
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)