Skip to content

Commit

Permalink
Merge pull request #5 from devos50/rename_tags_endpoint
Browse files Browse the repository at this point in the history
Update KnowledgeEndpoint to allow editing statements through the GUI
  • Loading branch information
drew2a authored Oct 18, 2022
2 parents 4afd110 + 8262795 commit 56e98ae
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import binascii
from typing import Optional, Set, Tuple
from typing import Optional, Set, Tuple, Dict

from aiohttp import web
from aiohttp_apispec import docs
Expand All @@ -17,9 +17,9 @@


@froze_it
class TagsEndpoint(RESTEndpoint):
class KnowledgeEndpoint(RESTEndpoint):
"""
Top-level endpoint for tags.
Top-level endpoint for knowledge management.
"""

def __init__(self, db: KnowledgeDatabase, community: KnowledgeCommunity):
Expand All @@ -40,60 +40,69 @@ def validate_infohash(infohash: bytes) -> Tuple[bool, Optional[RESTResponse]]:
def setup_routes(self):
self.app.add_routes(
[
web.patch('/{infohash}', self.update_tags_entries),
web.get('/{infohash}/suggestions', self.get_suggestions),
web.patch('/{infohash}', self.update_knowledge_entries),
web.get('/{infohash}/tag_suggestions', self.get_tag_suggestions),
]
)

@docs(
tags=["General"],
summary="Update a particular torrent with tags.",
summary="Update the metadata associated with a particular torrent.",
responses={
200: {
"schema": schema(UpdateTagsResponse={'success': Boolean()})
},
HTTP_BAD_REQUEST: {
"schema": HandledErrorSchema, 'example': {"error": "Invalid tag length"}},
},
description="This endpoint updates a particular torrent with the provided tags."
description="This endpoint updates a particular torrent with the provided metadata."
)
async def update_tags_entries(self, request):
async def update_knowledge_entries(self, request):
params = await request.json()
infohash = request.match_info["infohash"]
ih_valid, error_response = TagsEndpoint.validate_infohash(infohash)
ih_valid, error_response = KnowledgeEndpoint.validate_infohash(infohash)
if not ih_valid:
return error_response

# Validate whether the size of the tag is within the allowed range
tags = set(params["tags"])
for tag in tags:
if len(tag) < MIN_RESOURCE_LENGTH or len(tag) > MAX_RESOURCE_LENGTH:
# Validate whether the size of the tag is within the allowed range and filter out duplicate tags.
tags = set()
statements = []
for statement in params["statements"]:
obj = statement["object"]
if statement["predicate"] == ResourceType.TAG and \
(len(obj) < MIN_RESOURCE_LENGTH or len(obj) > MAX_RESOURCE_LENGTH):
return RESTResponse({"error": "Invalid tag length"}, status=HTTP_BAD_REQUEST)

self.modify_tags(infohash, tags)
if obj not in tags:
tags.add(obj)
statements.append(statement)

self.modify_statements(infohash, statements)

return RESTResponse({"success": True})

@db_session
def modify_tags(self, infohash: str, new_tags: Set[str]):
def modify_statements(self, infohash: str, statements):
"""
Modify the tags of a particular content item.
Modify the statements of a particular content item.
"""
if not self.community:
return

# First, get the current tags and compute the diff between the old and new tags
old_tags = set(self.db.get_objects(infohash, predicate=ResourceType.TAG))
added_tags = new_tags - old_tags
removed_tags = old_tags - new_tags
# First, get the current statements and compute the diff between the old and new statements
old_statements = set([(stmt.predicate, stmt.object) for stmt in self.db.get_statements(infohash)])
new_statements = set([(stmt["predicate"], stmt["object"]) for stmt in statements])
added_statements = new_statements - old_statements
removed_statements = old_statements - new_statements

# Create individual tag operations for the added/removed tags
# Create individual statement operations for the added/removed statements
public_key = self.community.key.pub().key_to_bin()
for tag in added_tags.union(removed_tags):
type_of_operation = Operation.ADD if tag in added_tags else Operation.REMOVE
for stmt in added_statements.union(removed_statements):
predicate, obj = stmt
type_of_operation = Operation.ADD if stmt in added_statements else Operation.REMOVE
operation = StatementOperation(subject_type=ResourceType.TORRENT, subject=infohash,
predicate=ResourceType.TAG,
object=tag, operation=type_of_operation, clock=0,
predicate=predicate,
object=obj, operation=type_of_operation, clock=0,
creator_public_key=public_key)
operation.clock = self.db.get_clock(operation) + 1
signature = self.community.sign(operation)
Expand All @@ -111,12 +120,12 @@ def modify_tags(self, infohash: str, new_tags: Set[str]):
},
description="This endpoint updates a particular torrent with the provided tags."
)
async def get_suggestions(self, request):
async def get_tag_suggestions(self, request):
"""
Get suggestions for a particular tag.
"""
infohash = request.match_info["infohash"]
ih_valid, error_response = TagsEndpoint.validate_infohash(infohash)
ih_valid, error_response = KnowledgeEndpoint.validate_infohash(infohash)
if not ih_valid:
return error_response

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import Dict
from unittest.mock import Mock

import pytest
Expand All @@ -8,7 +9,7 @@

from tribler.core.components.knowledge.community.knowledge_payload import StatementOperation
from tribler.core.components.knowledge.db.knowledge_db import Operation, ResourceType
from tribler.core.components.knowledge.restapi.tags_endpoint import TagsEndpoint
from tribler.core.components.knowledge.restapi.knowledge_endpoint import KnowledgeEndpoint
from tribler.core.components.restapi.rest.base_api_test import do_request
from tribler.core.conftest import TEST_PERSONAL_KEY
from tribler.core.utilities.unicode import hexlify
Expand All @@ -17,71 +18,75 @@
# pylint: disable=redefined-outer-name

@pytest.fixture
def tags_endpoint(knowledge_db):
def knowledge_endpoint(knowledge_db):
community = Mock()
community.key = TEST_PERSONAL_KEY
community.sign = Mock(return_value=b'')
endpoint = TagsEndpoint(knowledge_db, community)
endpoint = KnowledgeEndpoint(knowledge_db, community)
return endpoint


@pytest.fixture
def rest_api(loop, aiohttp_client, tags_endpoint):
def rest_api(loop, aiohttp_client, knowledge_endpoint):
app = Application()
app.add_subapp('/tags', tags_endpoint.app)
app.add_subapp('/knowledge', knowledge_endpoint.app)
return loop.run_until_complete(aiohttp_client(app))


def tag_to_statement(tag: str) -> Dict:
return {"predicate": ResourceType.TAG, "object": tag}


async def test_add_tag_invalid_infohash(rest_api):
"""
Test whether an error is returned if we try to add a tag to content with an invalid infohash
"""
post_data = {"tags": ["abc", "def"]}
await do_request(rest_api, 'tags/3f3', request_type="PATCH", expected_code=400, post_data=post_data)
await do_request(rest_api, 'tags/3f3f', request_type="PATCH", expected_code=400, post_data=post_data)
post_data = {"knowledge": [tag_to_statement("abc"), tag_to_statement("def")]}
await do_request(rest_api, 'knowledge/3f3', request_type="PATCH", expected_code=400, post_data=post_data)
await do_request(rest_api, 'knowledge/3f3f', request_type="PATCH", expected_code=400, post_data=post_data)


async def test_add_invalid_tag(rest_api):
"""
Test whether an error is returned if we try to add a tag that is too short or long.
"""
post_data = {"tags": ["a"]}
post_data = {"statements": [tag_to_statement("a")]}
infohash = b'a' * 20
await do_request(rest_api, f'tags/{hexlify(infohash)}', request_type="PATCH", expected_code=400,
await do_request(rest_api, f'knowledge/{hexlify(infohash)}', request_type="PATCH", expected_code=400,
post_data=post_data)

post_data = {"tags": ["a" * 60]}
await do_request(rest_api, f'tags/{hexlify(infohash)}', request_type="PATCH", expected_code=400,
post_data = {"statements": [tag_to_statement("a" * 60)]}
await do_request(rest_api, f'knowledge/{hexlify(infohash)}', request_type="PATCH", expected_code=400,
post_data=post_data)


async def test_modify_tags(rest_api, knowledge_db):
"""
Test modifying tags
"""
post_data = {"tags": ["abc", "def"]}
post_data = {"statements": [tag_to_statement("abc"), tag_to_statement("def")]}
infohash = 'a' * 40
with freeze_time("2015-01-01") as frozen_time:
await do_request(rest_api, f'tags/{infohash}', request_type="PATCH", expected_code=200,
await do_request(rest_api, f'knowledge/{infohash}', request_type="PATCH", expected_code=200,
post_data=post_data)
with db_session:
tags = knowledge_db.get_objects(infohash, predicate=ResourceType.TAG)
assert len(tags) == 2

# Now remove a tag
frozen_time.move_to("2016-01-01")
post_data = {"tags": ["abc"]}
await do_request(rest_api, f'tags/{infohash}', request_type="PATCH", expected_code=200,
post_data = {"statements": [tag_to_statement("abc")]}
await do_request(rest_api, f'knowledge/{infohash}', request_type="PATCH", expected_code=200,
post_data=post_data)
with db_session:
tags = knowledge_db.get_objects(infohash, predicate=ResourceType.TAG)
assert tags == ["abc"]


async def test_modify_tags_no_community(knowledge_db, tags_endpoint):
tags_endpoint.community = None
async def test_modify_tags_no_community(knowledge_db, knowledge_endpoint):
knowledge_endpoint.community = None
infohash = 'a' * 20
tags_endpoint.modify_tags(infohash, {"abc", "def"})
knowledge_endpoint.modify_statements(infohash, [tag_to_statement("abc"), tag_to_statement("def")])

with db_session:
tags = knowledge_db.get_objects(infohash, predicate=ResourceType.TAG)
Expand All @@ -93,9 +98,8 @@ async def test_get_suggestions_invalid_infohash(rest_api):
"""
Test whether an error is returned if we fetch suggestions from content with an invalid infohash
"""
post_data = {"tags": ["abc", "def"]}
await do_request(rest_api, 'tags/3f3/suggestions', expected_code=400, post_data=post_data)
await do_request(rest_api, 'tags/3f3f/suggestions', expected_code=400, post_data=post_data)
await do_request(rest_api, 'knowledge/3f3/tag_suggestions', expected_code=400)
await do_request(rest_api, 'knowledge/3f3f/tag_suggestions', expected_code=400)


async def test_get_suggestions(rest_api, knowledge_db):
Expand All @@ -104,7 +108,7 @@ async def test_get_suggestions(rest_api, knowledge_db):
"""
infohash = b'a' * 20
infohash_str = hexlify(infohash)
response = await do_request(rest_api, f'tags/{infohash_str}/suggestions')
response = await do_request(rest_api, f'knowledge/{infohash_str}/tag_suggestions')
assert "suggestions" in response
assert not response["suggestions"]

Expand All @@ -120,5 +124,5 @@ def _add_operation(op=Operation.ADD):
_add_operation(op=Operation.ADD)
_add_operation(op=Operation.REMOVE)

response = await do_request(rest_api, f'tags/{infohash_str}/suggestions')
response = await do_request(rest_api, f'knowledge/{infohash_str}/tag_suggestions')
assert response["suggestions"] == ["test"]
4 changes: 2 additions & 2 deletions src/tribler/core/components/restapi/restapi_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from tribler.core.components.ipv8.ipv8_component import Ipv8Component
from tribler.core.components.key.key_component import KeyComponent
from tribler.core.components.knowledge.knowledge_component import KnowledgeComponent
from tribler.core.components.knowledge.restapi.tags_endpoint import TagsEndpoint
from tribler.core.components.knowledge.restapi.knowledge_endpoint import KnowledgeEndpoint
from tribler.core.components.libtorrent.libtorrent_component import LibtorrentComponent
from tribler.core.components.libtorrent.restapi.create_torrent_endpoint import CreateTorrentEndpoint
from tribler.core.components.libtorrent.restapi.downloads_endpoint import DownloadsEndpoint
Expand Down Expand Up @@ -124,7 +124,7 @@ async def run(self):
tags_db=knowledge_component.knowledge_db)
self.maybe_add('/remote_query', RemoteQueryEndpoint, gigachannel_component.community,
metadata_store_component.mds)
self.maybe_add('/tags', TagsEndpoint, db=knowledge_component.knowledge_db,
self.maybe_add('/knowledge', KnowledgeEndpoint, db=knowledge_component.knowledge_db,
community=knowledge_component.community)

if not isinstance(ipv8_component, NoneComponent):
Expand Down
16 changes: 12 additions & 4 deletions src/tribler/gui/dialogs/addtagsdialog.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Dict, Optional
from typing import Dict, Optional, List

from PyQt5 import uic
from PyQt5.QtCore import QModelIndex, QPoint, pyqtSignal
from PyQt5.QtWidgets import QSizePolicy, QWidget

from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.core.components.knowledge.knowledge_constants import MAX_RESOURCE_LENGTH, MIN_RESOURCE_LENGTH

from tribler.gui.defs import TAG_HORIZONTAL_MARGIN
Expand Down Expand Up @@ -39,11 +40,13 @@ def __init__(self, parent: QWidget, infohash: str) -> None:
self.dialog_widget.suggestions_container.hide()

# Fetch suggestions
TriblerNetworkRequest(f"tags/{infohash}/suggestions", self.on_received_suggestions)
TriblerNetworkRequest(f"knowledge/{infohash}/tag_suggestions", self.on_received_tag_suggestions)

self.update_window()

def on_save_tags_button_clicked(self, _) -> None:
statements: List[Dict] = []

# Sanity check the entered tags
entered_tags = self.dialog_widget.edit_tags_input.get_entered_tags()
for tag in entered_tags:
Expand All @@ -57,9 +60,14 @@ def on_save_tags_button_clicked(self, _) -> None:
self.dialog_widget.error_text_label.setHidden(False)
return

self.save_button_clicked.emit(self.index, entered_tags)
statements.append({
"predicate": ResourceType.TAG,
"object": tag,
})

self.save_button_clicked.emit(self.index, statements)

def on_received_suggestions(self, data: Dict) -> None:
def on_received_tag_suggestions(self, data: Dict) -> None:
self.suggestions_loaded.emit()
if data["suggestions"]:
self.dialog_widget.suggestions_container.show()
Expand Down
16 changes: 10 additions & 6 deletions src/tribler/gui/tests/test_gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
from PyQt5.QtWidgets import QListWidget, QTableView, QTextEdit, QTreeWidget, QTreeWidgetItem

import tribler.gui
from tribler.core.components.reporter.reported_error import ReportedError
from tribler.core.components.knowledge.db.knowledge_db import ResourceType
from tribler.core.components.knowledge.knowledge_constants import MIN_RESOURCE_LENGTH
from tribler.core.components.reporter.reported_error import ReportedError
from tribler.core.sentry_reporter.sentry_reporter import SentryReporter
from tribler.core.tests.tools.common import TESTS_DATA_DIR
from tribler.core.utilities.rest_utils import path_to_url
Expand Down Expand Up @@ -733,7 +734,7 @@ def test_tags_dialog(window):
screenshot(window, name="edit_tags_dialog_long_tag_removed")

QTest.mouseClick(widget.content_table.add_tags_dialog.dialog_widget.save_button, Qt.LeftButton)
wait_for_signal(widget.content_table.edited_tags)
wait_for_signal(widget.content_table.edited_metadata)
QTest.qWait(200) # It can take a bit of time to hide the dialog


Expand All @@ -747,10 +748,13 @@ def test_no_tags(window):
wait_for_list_populated(widget.content_table)

idx = widget.content_table.model().index(0, 0)
widget.content_table.save_edited_tags(idx, []) # Remove all tags
wait_for_signal(widget.content_table.edited_tags)
widget.content_table.save_edited_metadata(idx, []) # Remove all tags
wait_for_signal(widget.content_table.edited_metadata)
screenshot(window, name="content_item_no_tags")

# Put some tags back (so further tests do not fail)
widget.content_table.save_edited_tags(idx, ["abc", "def"])
wait_for_signal(widget.content_table.edited_tags)
statements = []
for tag in ["abc", "def"]:
statements.append({"predicate": ResourceType.TAG, "object": tag})
widget.content_table.save_edited_metadata(idx, statements)
wait_for_signal(widget.content_table.edited_metadata)
Loading

0 comments on commit 56e98ae

Please sign in to comment.