diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2ddae3d6..86b4169e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,37 +1,39 @@ Changelog ========= - Development ------------ +---------- + +Added +~~~~~ + +- `[#443]`_: Added support and testing for Python 3.11 +- `[#479]`_: A "Filters" resource to the admin interface, allowing users to view, edit and delete filters there. This resolves `[#476]`_. +- `[#433]`_: "Statistic" selector that allows the selection of "measurement" or "isolation forest". The isolation forest statistic plots the multivariate outlier score in a non-parametric way, with an adjustable threshold +- `[#479]`_: Give all users access to the admin interface. Unprivileged users won't be able to edit or delete most entities, however +- `[#479]`_: Consistently added show, edit and delete buttons to all pages of the admin interface -- `[#433]`_ Rewrite of trend chart outlier detection - * Added a "statistic" selector that allows the selection of "measurement" or "isolation forest" - * Remove outlier detection from normal measurement plot, because the normal assumption is not reasonable - * The isolation forest statistic plots the multivariate outlier score in a non-parametric way, with an adjustable threshold +Changed +~~~~~~~ + +- `[#479]`_: Allow unprivileged users to create new filters and delete their own filters via the REST API. This also resolves issues where these users couldn't create their own filters on certain pages. +- `[#479]`_: The "Edit Filters" page now redirects to the Filters tab of the Data Admin page. This relates to `[#476]`_. - `[#440]`_ Set up poetry to manage the dependencies, which might keep the dependencies from breaking down, and reduce the chances of the happening of issues such as `[#430]`_ -- `[#443]`_ - * Dropped support for Python 3.6, added support and testing for Python <= 3.11 - * Added docker-compose logging in the CI - * Stopped using Meinheld workers in the Docker image, since this is largely unmaintained - * Fix a bug in the database script when constructing URLs that broken under new SQLAlchemy versions - * Bump pytest - * Fix a bug in pytest where we used `scope` as a positional argument - * Update the SubFactoryList to a new version that works with newer FactoryBoy versions -.. _[#430]: https://github.com/ewels/MegaQC/issues/430 -.. _[#440]: https://github.com/ewels/MegaQC/pull/440 -.. _[#433]: https://github.com/ewels/MegaQC/pull/433 +Fixed +~~~~~ -======= +- `[#443]`_: Fix a bug in the database script when constructing URLs that broken under new SQLAlchemy versions -.. _section-1: +Removed +~~~~~~~ + +- `[#433]`_ Removed outlier detection from normal measurement plot, because the normal assumption is not reasonable +- `[#443]`_: Dropped support for Python 3.6 0.3.0 ----- -.. _breaking-changes-1: - Breaking Changes ~~~~~~~~~~~~~~~~ @@ -56,8 +58,6 @@ Breaking Changes - Dropped support for Node 8 -.. _new-features-1: - New Features ~~~~~~~~~~~~ @@ -65,9 +65,6 @@ New Features - Sphinx based documentation on Github Pages - `[#69]`_ Added a check to verify that a database exists and exit nicely if not - -.. _bug-fixes-1: - Bug Fixes ~~~~~~~~~ @@ -77,8 +74,6 @@ Bug Fixes - `[#170]`_ Improved handling of environment variables with environs - `[#194]`_ Forward more headers through nginx when using Docker Compose. This should avoid bad HTTP redirects. -.. _internal-changes-1: - Internal Changes ~~~~~~~~~~~~~~~~ @@ -86,7 +81,6 @@ Internal Changes - Enforce inactive users (by default) in the model layer - Many and more dependency updates - .. _[#69]: https://github.com/ewels/MegaQC/issues/69 .. _[#138]: https://github.com/ewels/MegaQC/issues/138 .. _[#139]: https://github.com/ewels/MegaQC/issues/139 @@ -95,4 +89,9 @@ Internal Changes .. _[#156]: https://github.com/ewels/MegaQC/issues/156 .. _[#170]: https://github.com/ewels/MegaQC/issues/170 .. _[#194]: https://github.com/ewels/MegaQC/issues/194 +.. _[#430]: https://github.com/ewels/MegaQC/issues/430 +.. _[#433]: https://github.com/ewels/MegaQC/pull/433 +.. _[#440]: https://github.com/ewels/MegaQC/pull/440 .. _[#443]: https://github.com/ewels/MegaQC/pull/443 +.. _[#476]: https://github.com/ewels/MegaQC/issues/476 +.. _[#479]: https://github.com/ewels/MegaQC/issues/479 \ No newline at end of file diff --git a/megaqc/rest_api/views.py b/megaqc/rest_api/views.py index f3562d99..5c0039e2 100644 --- a/megaqc/rest_api/views.py +++ b/megaqc/rest_api/views.py @@ -3,10 +3,16 @@ JSON API standard where relevant: https://jsonapi.org/format/ """ +import typing from hashlib import sha1 from http import HTTPStatus -from flapison import ResourceDetail, ResourceList, ResourceRelationship +from flapison import ( + JsonApiException, + ResourceDetail, + ResourceList, + ResourceRelationship, +) from flapison.schema import get_nested_fields, get_relationships from flask import Blueprint, current_app, jsonify, make_response, request from flask_login import current_user, login_required @@ -284,11 +290,34 @@ class FilterList(PermissionsMixin, ResourceList): schema = schemas.SampleFilterSchema data_layer = dict(session=db.session, model=models.SampleFilter) + # Users should be able to create new filters + @api_perms(Permission.USER) + def post(self, **kwargs): + # Bypass the default admin-only permissions inherited from the parent class + return ResourceList.post(self, **kwargs) + class Filter(PermissionsMixin, ResourceDetail): schema = schemas.SampleFilterSchema data_layer = dict(session=db.session, model=models.SampleFilter) + @api_perms(Permission.USER) + def delete(self, **kwargs): + if kwargs["permission"] == Permission.USER: + # Users should be able to delete their own filters + filter = db.session.query(models.SampleFilter).get(kwargs["id"]) + if filter is not None: + filter = typing.cast(models.SampleFilter, filter) + if filter.user_id != kwargs["user"].user_id: + raise JsonApiException( + title="Insufficient permissions to access this resource", + detail="You do not own this filter", + status=403, + ) + + # Bypass the default admin-only permissions inherited from the parent class + return ResourceDetail.delete(self, **kwargs) + class FilterRelationship(PermissionsMixin, ResourceRelationship): schema = schemas.SampleFilterSchema diff --git a/megaqc/templates/nav.html b/megaqc/templates/nav.html index 69e76ce5..7eadd434 100644 --- a/megaqc/templates/nav.html +++ b/megaqc/templates/nav.html @@ -42,15 +42,15 @@ Create Dashboard Favourite Plots - + Pending Uploads - Edit Filters + Edit Filters Edit Reports + Data Admin {% if current_user.is_admin %} Manage Users - Data Admin {% endif %} Log Out diff --git a/src/admin.js b/src/admin.js index 4981f9f5..dc204c20 100644 --- a/src/admin.js +++ b/src/admin.js @@ -24,6 +24,7 @@ import { DashboardEdit, DashboardShow, } from "./admin/dashboards"; +import { FilterList, FilterEdit, FilterShow } from "./admin/filter"; import { DataList, DataEdit, DataShow, DataCreate } from "./admin/sampleData"; import { UserList, UserEdit, UserShow } from "./admin/user"; import { getClient, getToken } from "./util/api"; @@ -133,6 +134,12 @@ function App() { show={UserShow} edit={UserEdit} /> + ( + + + ); diff --git a/src/admin/dataType.js b/src/admin/dataType.js index 0c996330..80f16782 100644 --- a/src/admin/dataType.js +++ b/src/admin/dataType.js @@ -9,6 +9,9 @@ import { SimpleShowLayout, TextField, TextInput, + EditButton, + ShowButton, + DeleteButton, } from "react-admin"; export const DataTypeList = (props) => ( @@ -17,6 +20,9 @@ export const DataTypeList = (props) => ( + + + ); diff --git a/src/admin/favourite.js b/src/admin/favourite.js index acb48a91..41e86715 100644 --- a/src/admin/favourite.js +++ b/src/admin/favourite.js @@ -10,6 +10,9 @@ import { SimpleShowLayout, TextField, TextInput, + EditButton, + ShowButton, + DeleteButton, } from "react-admin"; import { JsonInput, JsonField } from "./components/jsonField"; @@ -21,6 +24,9 @@ export const FavouriteList = (props) => ( + + + ); diff --git a/src/admin/filter.js b/src/admin/filter.js new file mode 100644 index 00000000..68a827b7 --- /dev/null +++ b/src/admin/filter.js @@ -0,0 +1,83 @@ +import React from "react"; +import { + Datagrid, + DateField, + DateInput, + Edit, + List, + Show, + SimpleForm, + SimpleShowLayout, + TextField, + TextInput, + ReferenceField, + EditButton, + ShowButton, + BooleanField, + DeleteButton, + BooleanInput, +} from "react-admin"; +import { JsonInput, JsonField } from "./components/jsonField"; + +export const FilterList = (props) => ( + + + + + + + + + + + + + + + +); + +export const FilterShow = (props) => ( + + + + + + + + + + + + +); + +export const FilterEdit = (props) => ( + + + + + + + + + + + + +); diff --git a/src/admin/filterGroup.js b/src/admin/filterGroup.js index 9f6da9ab..17fcac05 100644 --- a/src/admin/filterGroup.js +++ b/src/admin/filterGroup.js @@ -1,10 +1,20 @@ import React from "react"; -import { Datagrid, List, TextField } from "react-admin"; +import { + Datagrid, + List, + TextField, + EditButton, + ShowButton, + DeleteButton, +} from "react-admin"; export const FilterGroupList = (props) => ( + + + ); diff --git a/src/admin/meta.js b/src/admin/meta.js index 99f116d8..7ad161dd 100644 --- a/src/admin/meta.js +++ b/src/admin/meta.js @@ -12,6 +12,9 @@ import { SimpleShowLayout, TextField, TextInput, + EditButton, + ShowButton, + DeleteButton, } from "react-admin"; import DefaultForm from "./components/defaultForm"; @@ -30,6 +33,9 @@ export const ReportMetaList = (props) => ( > + + + ); diff --git a/src/admin/report.js b/src/admin/report.js index 5b46baa0..b59b7d27 100644 --- a/src/admin/report.js +++ b/src/admin/report.js @@ -14,6 +14,7 @@ import { SimpleShowLayout, TextField, TextInput, + DeleteButton, } from "react-admin"; import ResourceLink from "./components/resourceLink"; @@ -34,6 +35,7 @@ export const ReportList = (props) => ( + ); diff --git a/src/admin/sample.js b/src/admin/sample.js index 72348f87..2a20c065 100644 --- a/src/admin/sample.js +++ b/src/admin/sample.js @@ -15,6 +15,7 @@ import { SimpleShowLayout, TextField, TextInput, + DeleteButton, } from "react-admin"; import ResourceLink from "./components/resourceLink"; @@ -34,6 +35,7 @@ export const SampleList = (props) => ( + ); diff --git a/src/admin/sampleData.js b/src/admin/sampleData.js index fd144557..481ce335 100644 --- a/src/admin/sampleData.js +++ b/src/admin/sampleData.js @@ -14,6 +14,7 @@ import { SimpleShowLayout, TextField, TextInput, + DeleteButton, } from "react-admin"; import DefaultForm from "./components/defaultForm"; @@ -40,6 +41,7 @@ export const DataList = (props) => ( + ); diff --git a/src/admin/upload.js b/src/admin/upload.js index d68e6024..e4b9efe7 100644 --- a/src/admin/upload.js +++ b/src/admin/upload.js @@ -10,6 +10,9 @@ import { SimpleShowLayout, TextField, TextInput, + EditButton, + ShowButton, + DeleteButton, } from "react-admin"; export const UploadList = (props) => ( @@ -21,6 +24,9 @@ export const UploadList = (props) => ( + + + ); diff --git a/src/admin/user.js b/src/admin/user.js index e4397314..b498bbe1 100644 --- a/src/admin/user.js +++ b/src/admin/user.js @@ -18,6 +18,7 @@ import { SimpleShowLayout, TextField, TextInput, + DeleteButton, } from "react-admin"; export const UserList = (props) => ( @@ -34,6 +35,9 @@ export const UserList = (props) => ( + + + ); diff --git a/tests/api/test_permissions.py b/tests/api/test_permissions.py index c40281c6..55cfa14e 100644 --- a/tests/api/test_permissions.py +++ b/tests/api/test_permissions.py @@ -74,9 +74,10 @@ def test_single_resource_permissions( @pytest.mark.parametrize( - # These two have unusual permissions + # These three have unusual permissions "endpoint", - set(list_resource_endpoints) - {"rest_api.uploadlist", "rest_api.userlist"}, + set(list_resource_endpoints) + - {"rest_api.uploadlist", "rest_api.userlist", "rest_api.filterlist"}, ) @pytest.mark.parametrize( "method,success",