From 539000f4d056f5178df465ff7f9aba50f9eaa19f Mon Sep 17 00:00:00 2001 From: Mike Fiedler Date: Tue, 31 Oct 2023 18:04:26 -0400 Subject: [PATCH] feat(admin): Add Project.Observation behaviors Signed-off-by: Mike Fiedler --- tests/unit/admin/test_routes.py | 14 +++ tests/unit/admin/views/test_projects.py | 116 ++++++++++++++++++ warehouse/admin/routes.py | 14 +++ warehouse/admin/static/js/warehouse.js | 10 ++ .../templates/admin/projects/detail.html | 91 ++++++++++++++ .../admin/projects/observations_list.html | 73 +++++++++++ .../admin/templates/admin/users/detail.html | 33 +++++ warehouse/admin/views/projects.py | 99 +++++++++++++++ 8 files changed, 450 insertions(+) create mode 100644 warehouse/admin/templates/admin/projects/observations_list.html diff --git a/tests/unit/admin/test_routes.py b/tests/unit/admin/test_routes.py index 0d2e19854d20..e85918be25bc 100644 --- a/tests/unit/admin/test_routes.py +++ b/tests/unit/admin/test_routes.py @@ -141,6 +141,20 @@ def test_includeme(): traverse="/{project_name}/{version}", domain=warehouse, ), + pretend.call( + "admin.project.observations", + "/admin/projects/{project_name}/observations/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ), + pretend.call( + "admin.project.add_observation", + "/admin/projects/{project_name}/add_observation/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ), pretend.call( "admin.project.journals", "/admin/projects/{project_name}/journals/", diff --git a/tests/unit/admin/views/test_projects.py b/tests/unit/admin/views/test_projects.py index 989274a3e2eb..16afd261b486 100644 --- a/tests/unit/admin/views/test_projects.py +++ b/tests/unit/admin/views/test_projects.py @@ -21,14 +21,17 @@ from tests.common.db.oidc import GitHubPublisherFactory from warehouse.admin.views import projects as views +from warehouse.observations.models import ObservationKind from warehouse.packaging.models import Project, Role from warehouse.packaging.tasks import update_release_description from warehouse.search.tasks import reindex_project from ....common.db.accounts import UserFactory +from ....common.db.observations import ObserverFactory from ....common.db.packaging import ( JournalEntryFactory, ProjectFactory, + ProjectObservationFactory, ReleaseFactory, RoleFactory, ) @@ -101,6 +104,8 @@ def test_gets_project(self, db_request): "MAX_PROJECT_SIZE": views.MAX_PROJECT_SIZE, "ONE_GB": views.ONE_GB, "UPLOAD_LIMIT_CAP": views.UPLOAD_LIMIT_CAP, + "observation_kinds": ObservationKind, + "observations": [], } def test_non_normalized_name(self, db_request): @@ -347,6 +352,117 @@ def test_non_normalized_name(self, db_request): views.journals_list(project, db_request) +class TestProjectObservationsList: + def test_with_page(self, db_request): + observer = ObserverFactory.create() + UserFactory.create(observer=observer) + project = ProjectFactory.create() + observations = ProjectObservationFactory.create_batch( + size=30, related=project, observer=observer + ) + + db_request.matchdict["project_name"] = project.normalized_name + db_request.GET["page"] = "2" + result = views.observations_list(project, db_request) + + assert result == { + "observations": observations[25:], + "project": project, + } + + def test_with_invalid_page(self, db_request): + project = ProjectFactory.create() + db_request.matchdict["project_name"] = project.normalized_name + db_request.GET["page"] = "not an integer" + + with pytest.raises(HTTPBadRequest): + views.observations_list(project, db_request) + + +class TestProjectAddObservation: + def test_add_observation(self, db_request): + project = ProjectFactory.create() + observer = ObserverFactory.create() + user = UserFactory.create(observer=observer) + db_request.route_path = pretend.call_recorder( + lambda *a, **kw: "/admin/projects/" + ) + db_request.matchdict["project_name"] = project.normalized_name + db_request.POST["kind"] = ObservationKind.IsSpam.value[0] + db_request.POST["summary"] = "This is a summary" + db_request.user = user + + views.add_observation(project, db_request) + + assert len(project.observations) == 1 + + def test_no_user_observer(self, db_request): + project = ProjectFactory.create() + user = UserFactory.create() + db_request.route_path = pretend.call_recorder( + lambda *a, **kw: "/admin/projects/" + ) + db_request.matchdict["project_name"] = project.normalized_name + db_request.POST["kind"] = ObservationKind.IsSpam.value[0] + db_request.POST["summary"] = "This is a summary" + db_request.user = user + + views.add_observation(project, db_request) + + assert len(project.observations) == 1 + + def test_no_kind_errors(self): + project = pretend.stub(name="foo", normalized_name="foo") + request = pretend.stub( + POST={}, + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + route_path=lambda *a, **kw: "/foo/bar/", + ) + + with pytest.raises(HTTPSeeOther) as exc: + views.add_observation(project, request) + assert exc.value.status_code == 303 + assert exc.value.headers["Location"] == "/foo/bar/" + + assert request.session.flash.calls == [ + pretend.call("Provide a kind", queue="error") + ] + + def test_invalid_kind_errors(self): + project = pretend.stub(name="foo", normalized_name="foo") + request = pretend.stub( + POST={"kind": "not a valid kind"}, + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + route_path=lambda *a, **kw: "/foo/bar/", + ) + + with pytest.raises(HTTPSeeOther) as exc: + views.add_observation(project, request) + assert exc.value.status_code == 303 + assert exc.value.headers["Location"] == "/foo/bar/" + + assert request.session.flash.calls == [ + pretend.call("Invalid kind", queue="error") + ] + + def test_no_summary_errors(self): + project = pretend.stub(name="foo", normalized_name="foo") + request = pretend.stub( + POST={"kind": ObservationKind.IsSpam.value[0]}, + session=pretend.stub(flash=pretend.call_recorder(lambda *a, **kw: None)), + route_path=lambda *a, **kw: "/foo/bar/", + ) + + with pytest.raises(HTTPSeeOther) as exc: + views.add_observation(project, request) + assert exc.value.status_code == 303 + assert exc.value.headers["Location"] == "/foo/bar/" + + assert request.session.flash.calls == [ + pretend.call("Provide a summary", queue="error") + ] + + class TestProjectSetTotalSizeLimit: def test_sets_total_size_limitwith_integer(self, db_request): project = ProjectFactory.create(name="foo") diff --git a/warehouse/admin/routes.py b/warehouse/admin/routes.py index b9fd8fb14431..72b5592e981f 100644 --- a/warehouse/admin/routes.py +++ b/warehouse/admin/routes.py @@ -145,6 +145,20 @@ def includeme(config): traverse="/{project_name}/{version}", domain=warehouse, ) + config.add_route( + "admin.project.observations", + "/admin/projects/{project_name}/observations/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ) + config.add_route( + "admin.project.add_observation", + "/admin/projects/{project_name}/add_observation/", + factory="warehouse.packaging.models:ProjectFactory", + traverse="/{project_name}", + domain=warehouse, + ) config.add_route( "admin.project.journals", "/admin/projects/{project_name}/journals/", diff --git a/warehouse/admin/static/js/warehouse.js b/warehouse/admin/static/js/warehouse.js index 2470f3f9cf34..1191e22d234a 100644 --- a/warehouse/admin/static/js/warehouse.js +++ b/warehouse/admin/static/js/warehouse.js @@ -123,3 +123,13 @@ table.columns([".ip_address", ".hashed_ip"]).visible(false); // add column visibility button new $.fn.dataTable.Buttons(table, {buttons: ["copy", "csv", "colvis"]}); table.buttons().container().appendTo($(".col-md-6:eq(0)", table.table().container())); + +// Observations +let obs_table = $("#observations").DataTable({ + responsive: true, + lengthChange: false, +}); +obs_table.column(".time").order("desc").draw(); +obs_table.columns([".payload"]).visible(false); +new $.fn.dataTable.Buttons(obs_table, {buttons: ["copy", "csv", "colvis"]}); +obs_table.buttons().container().appendTo($(".col-md-6:eq(0)", obs_table.table().container())); diff --git a/warehouse/admin/templates/admin/projects/detail.html b/warehouse/admin/templates/admin/projects/detail.html index aa3a8041e825..849ebfaa2e9e 100644 --- a/warehouse/admin/templates/admin/projects/detail.html +++ b/warehouse/admin/templates/admin/projects/detail.html @@ -267,6 +267,97 @@