diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f87d0fb08..9c0fe75fc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,12 +7,12 @@ jobs: strategy: matrix: python-version: - - '3.8' - '3.9' - '3.10' + - '3.11' services: postgres: - image: postgres:11 + image: postgres:16 env: POSTGRES_DB: sodar POSTGRES_USER: postgres @@ -64,7 +64,7 @@ jobs: uses: actions/checkout@v3 - name: Install project Python dependencies run: | - pip install wheel==0.37.1 + pip install wheel==0.42.0 pip install -r requirements/local.txt pip install -r requirements/test.txt - name: Setup Node.js @@ -86,14 +86,15 @@ jobs: coverage report - name: Run Vue app tests run: make test_samplesheets_vue - if: ${{ matrix.python-version == '3.8' }} + if: ${{ matrix.python-version == '3.11' }} - name: Check Python linting run: flake8 . - name: Check Python formatting run: make black arg=--check + if: ${{ matrix.python-version == '3.11' }} - name: Report coverage with Coveralls uses: coverallsapp/github-action@master with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: './coverage.lcov' - if: ${{ matrix.python-version == '3.8' }} + if: ${{ matrix.python-version == '3.11' }} diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e72d09389..2c8ba6f42 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: '3.8' + python: '3.11' # Build documentation in the docs/ directory with Sphinx sphinx: diff --git a/Makefile b/Makefile index a0996e68b..c919238a5 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ define USAGE= @echo -e "Usage:" @echo -e "\tmake black [arg=--] -- format python with black" @echo -e "\tmake serve [arg=sync] -- start server" +@echo -e "\tmake flake -- run flake8" @echo -e "\tmake celery -- start celery & celerybeat" @echo -e "\tmake demo -- start demo server" @echo -e "\tmake samplesheets_vue -- start samplesheet vue.js app" @@ -39,6 +40,11 @@ endif $(MANAGE) runserver 0.0.0.0:8000 --settings=config.settings.local +.PHONY: flake +flake: + flake8 . + + .PHONY: celery celery: celery -A config worker -l info --beat diff --git a/config/settings/base.py b/config/settings/base.py index 7a1629124..30dda3db7 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -61,6 +61,7 @@ 'markupfield', # For markdown 'rest_framework', # For API views 'knox', # For token auth + 'social_django', # For OIDC authentication 'docs', # For the online user documentation/manual 'dal', # For user search combo box 'dal_select2', @@ -303,7 +304,7 @@ AUTOSLUG_SLUGIFY_FUNCTION = 'slugify.slugify' # Location of root django.contrib.admin URL, use {% url 'admin:index' %} -ADMIN_URL = r'^admin/' +ADMIN_URL = 'admin/' # Celery @@ -434,79 +435,40 @@ ) -# SAML configuration +# OpenID Connect (OIDC) configuration # ------------------------------------------------------------------------------ -ENABLE_SAML = env.bool('ENABLE_SAML', False) -SAML2_AUTH = { - # Required setting - # Pysaml2 Saml client settings - # See: https://pysaml2.readthedocs.io/en/latest/howto/config.html - 'SAML_CLIENT_SETTINGS': { - # Optional entity ID string to be passed in the 'Issuer' element of - # authn request, if required by the IDP. - 'entityid': env.str('SAML_CLIENT_ENTITY_ID', 'SODAR'), - 'entitybaseurl': env.str( - 'SAML_CLIENT_ENTITY_URL', 'https://localhost:8000' - ), - # The auto(dynamic) metadata configuration URL of SAML2 - 'metadata': { - 'local': [ - env.str('SAML_CLIENT_METADATA_FILE', 'metadata.xml'), - ], - }, - 'service': { - 'sp': { - 'idp': env.str( - 'SAML_CLIENT_IPD', - 'https://sso.hpc.bihealth.org/auth/realms/cubi', - ), - # Keycloak expects client signature - 'authn_requests_signed': 'true', - # Enforce POST binding which is required by keycloak - 'binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', - }, - }, - 'key_file': env.str('SAML_CLIENT_KEY_FILE', 'key.pem'), - 'cert_file': env.str('SAML_CLIENT_CERT_FILE', 'cert.pem'), - 'xmlsec_binary': env.str('SAML_CLIENT_XMLSEC1', '/usr/bin/xmlsec1'), - 'encryption_keypairs': [ - { - 'key_file': env.str('SAML_CLIENT_KEY_FILE', 'key.pem'), - 'cert_file': env.str('SAML_CLIENT_CERT_FILE', 'cert.pem'), - } - ], - }, - # Custom target redirect URL after the user get logged in. - # Defaults to /admin if not set. This setting will be overwritten if you - # have parameter ?next= specificed in the login URL. - 'DEFAULT_NEXT_URL': '/', - # # Optional settings below - # 'NEW_USER_PROFILE': { - # 'USER_GROUPS': [], # The default group name when a new user logs in - # 'ACTIVE_STATUS': True, # The default active status for new users - # 'STAFF_STATUS': True, # The staff status for new users - # 'SUPERUSER_STATUS': False, # The superuser status for new users - # }, - 'ATTRIBUTES_MAP': env.dict( - 'SAML_ATTRIBUTES_MAP', - default={ - # Change values to corresponding SAML2 userprofile attributes. - 'email': 'Email', - 'username': 'UserName', - 'first_name': 'FirstName', - 'last_name': 'LastName', - }, - ), - # 'TRIGGER': { - # 'FIND_USER': 'path.to.your.find.user.hook.method', - # 'NEW_USER': 'path.to.your.new.user.hook.method', - # 'CREATE_USER': 'path.to.your.create.user.hook.method', - # 'BEFORE_LOGIN': 'path.to.your.login.hook.method', - # }, - # Custom URL to validate incoming SAML requests against - # 'ASSERTION_URL': 'https://your.url.here', -} +ENABLE_OIDC = env.bool('ENABLE_OIDC', False) + +if ENABLE_OIDC: + AUTHENTICATION_BACKENDS = tuple( + itertools.chain( + ('social_core.backends.open_id_connect.OpenIdConnectAuth',), + AUTHENTICATION_BACKENDS, + ) + ) + TEMPLATES[0]['OPTIONS']['context_processors'] += [ + 'social_django.context_processors.backends', + 'social_django.context_processors.login_redirect', + ] + SOCIAL_AUTH_JSONFIELD_ENABLED = True + SOCIAL_AUTH_JSONFIELD_CUSTOM = 'django.db.models.JSONField' + SOCIAL_AUTH_USER_MODEL = AUTH_USER_MODEL + SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = [ + 'username', + 'name', + 'first_name', + 'last_name', + 'email', + ] + SOCIAL_AUTH_OIDC_OIDC_ENDPOINT = env.str( + 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT', None + ) + SOCIAL_AUTH_OIDC_KEY = env.str('SOCIAL_AUTH_OIDC_KEY', 'CHANGEME') + SOCIAL_AUTH_OIDC_SECRET = env.str('SOCIAL_AUTH_OIDC_SECRET', 'CHANGEME') + SOCIAL_AUTH_OIDC_USERNAME_KEY = env.str( + 'SOCIAL_AUTH_OIDC_USERNAME_KEY', 'username' + ) # Logging diff --git a/config/urls.py b/config/urls.py index 0ab844bf9..feee7eb8d 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,5 +1,5 @@ from django.conf import settings -from django.conf.urls import include, url +from django.conf.urls import include from django.conf.urls.static import static from django.contrib import admin from django.contrib.auth import views as auth_views @@ -26,7 +26,7 @@ def handler500(request, *args, **argv): urlpatterns = [ path(route='', view=HomeView.as_view(), name='home'), # Django Admin, use {% url 'admin:index' %} - url(settings.ADMIN_URL, admin.site.urls), + path(settings.ADMIN_URL, admin.site.urls), # Login and logout path( route='login/', @@ -40,6 +40,8 @@ def handler500(request, *args, **argv): path('api/auth/', include('knox.urls')), # Iconify SVG icons path('icons/', include('dj_iconify.urls')), + # Social auth for OIDC support + path('social/', include('social_django.urls')), # General site apps path('alerts/adm/', include('adminalerts.urls')), path('alerts/app/', include('appalerts.urls')), diff --git a/irodsinfo/tests/test_permissions.py b/irodsinfo/tests/test_permissions.py index 12d3f246a..0436638c1 100644 --- a/irodsinfo/tests/test_permissions.py +++ b/irodsinfo/tests/test_permissions.py @@ -3,10 +3,10 @@ from django.urls import reverse # Projectroles dependency -from projectroles.tests.test_permissions import TestSiteAppPermissionBase +from projectroles.tests.test_permissions import SiteAppPermissionTestBase -class TestIrodsinfoPermissions(TestSiteAppPermissionBase): +class TestIrodsinfoPermissions(SiteAppPermissionTestBase): """Tests for irodsinfo UI view permissions""" def test_get_irods_info(self): diff --git a/irodsinfo/tests/test_permissions_api.py b/irodsinfo/tests/test_permissions_api.py index ba42883c9..03a04da1d 100644 --- a/irodsinfo/tests/test_permissions_api.py +++ b/irodsinfo/tests/test_permissions_api.py @@ -3,10 +3,10 @@ from django.urls import reverse # Projectroles dependency -from projectroles.tests.test_permissions import TestSiteAppPermissionBase +from projectroles.tests.test_permissions import SiteAppPermissionTestBase -class TestIrodsConfigRetrieveAPIView(TestSiteAppPermissionBase): +class TestIrodsConfigRetrieveAPIView(SiteAppPermissionTestBase): """Tests for irodsinfo API""" def test_get_irods_config(self): diff --git a/isatemplates/tests/test_permissions.py b/isatemplates/tests/test_permissions.py index d154ad952..9f17e8ece 100644 --- a/isatemplates/tests/test_permissions.py +++ b/isatemplates/tests/test_permissions.py @@ -6,7 +6,7 @@ from django.urls import reverse # Projectroles dependency -from projectroles.tests.test_permissions import TestSiteAppPermissionBase +from projectroles.tests.test_permissions import SiteAppPermissionTestBase from isatemplates.tests.test_models import ( CookiecutterISATemplateMixin, @@ -16,7 +16,7 @@ class TestISATemplatesPermissions( - CookiecutterISATemplateMixin, TestSiteAppPermissionBase + CookiecutterISATemplateMixin, SiteAppPermissionTestBase ): """Tests for isatemplates UI view permissions""" diff --git a/isatemplates/tests/test_ui.py b/isatemplates/tests/test_ui.py index 244538199..a27345277 100644 --- a/isatemplates/tests/test_ui.py +++ b/isatemplates/tests/test_ui.py @@ -12,7 +12,7 @@ from selenium.webdriver.common.by import By # Projectroles dependency -from projectroles.tests.test_ui import TestUIBase +from projectroles.tests.test_ui import UITestBase from isatemplates.models import ISA_FILE_PREFIXES from isatemplates.tests.test_models import ( @@ -29,7 +29,7 @@ BACKEND_PLUGINS_NO_TPL.remove('isatemplates_backend') -class TestISATemplateListView(CookiecutterISATemplateMixin, TestUIBase): +class TestISATemplateListView(CookiecutterISATemplateMixin, UITestBase): """Tests for ISATemplateListView UI""" def setUp(self): @@ -106,7 +106,7 @@ def test_get_disable_backend(self): class TestISATemplateDetailView( - CookiecutterISATemplateMixin, CookiecutterISAFileMixin, TestUIBase + CookiecutterISATemplateMixin, CookiecutterISAFileMixin, UITestBase ): """Tests for ISATemplateDetailView UI""" @@ -156,7 +156,7 @@ def test_get(self): class TestCUBIISATemplateDetailView( - CookiecutterISATemplateMixin, CookiecutterISAFileMixin, TestUIBase + CookiecutterISATemplateMixin, CookiecutterISAFileMixin, UITestBase ): """Tests for CUBIISATemplateDetailView UI""" diff --git a/isatemplates/tests/test_views.py b/isatemplates/tests/test_views.py index da6ea28a5..b2819f2d9 100644 --- a/isatemplates/tests/test_views.py +++ b/isatemplates/tests/test_views.py @@ -16,7 +16,7 @@ from test_plus.test import TestCase # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from isatemplates.forms import ( NO_JSON_MSG, @@ -250,7 +250,8 @@ def test_post_zip(self): self.assertEqual(CookiecutterISATemplate.objects.count(), 0) self.assertEqual(CookiecutterISAFile.objects.count(), 0) self.assertEqual( - ProjectEvent.objects.filter(event_name='template_create').count(), 0 + TimelineEvent.objects.filter(event_name='template_create').count(), + 0, ) data = { @@ -290,7 +291,8 @@ def test_post_zip(self): } self.assertEqual(model_to_dict(file_obj), expected) self.assertEqual( - ProjectEvent.objects.filter(event_name='template_create').count(), 1 + TimelineEvent.objects.filter(event_name='template_create').count(), + 1, ) def test_post_zip_no_json(self): @@ -615,7 +617,8 @@ def test_post(self): for f in self._get_files(): self.assertEqual(f.content, '') self.assertEqual( - ProjectEvent.objects.filter(event_name='template_update').count(), 0 + TimelineEvent.objects.filter(event_name='template_update').count(), + 0, ) data = { @@ -642,7 +645,8 @@ def test_post(self): with open(fp, 'rb') as f: self.assertEqual(file_obj.content, f.read().decode('utf-8')) self.assertEqual( - ProjectEvent.objects.filter(event_name='template_update').count(), 1 + TimelineEvent.objects.filter(event_name='template_update').count(), + 1, ) def test_post_no_file(self): @@ -655,7 +659,8 @@ def test_post_no_file(self): for f in self._get_files(): self.assertEqual(f.content, '') self.assertEqual( - ProjectEvent.objects.filter(event_name='template_update').count(), 0 + TimelineEvent.objects.filter(event_name='template_update').count(), + 0, ) data = { 'description': TEMPLATE_DESC_UPDATE, @@ -673,7 +678,8 @@ def test_post_no_file(self): for f in self._get_files(): self.assertEqual(f.content, '') self.assertEqual( - ProjectEvent.objects.filter(event_name='template_update').count(), 1 + TimelineEvent.objects.filter(event_name='template_update').count(), + 1, ) def test_post_no_name(self): @@ -712,7 +718,8 @@ def test_post_no_json(self): for f in self._get_files(): self.assertEqual(f.content, '') self.assertEqual( - ProjectEvent.objects.filter(event_name='template_update').count(), 0 + TimelineEvent.objects.filter(event_name='template_update').count(), + 0, ) def test_post_no_investigation(self): @@ -817,7 +824,8 @@ def test_get(self): def test_post(self): """Test POST""" self.assertEqual( - ProjectEvent.objects.filter(event_name='template_delete').count(), 0 + TimelineEvent.objects.filter(event_name='template_delete').count(), + 0, ) self.assertEqual(CookiecutterISATemplate.objects.count(), 1) self.assertEqual(CookiecutterISAFile.objects.count(), 3) @@ -827,7 +835,8 @@ def test_post(self): self.assertEqual(CookiecutterISATemplate.objects.count(), 0) self.assertEqual(CookiecutterISAFile.objects.count(), 0) self.assertEqual( - ProjectEvent.objects.filter(event_name='template_delete').count(), 1 + TimelineEvent.objects.filter(event_name='template_delete').count(), + 1, ) diff --git a/landingzones/configapps/bih_proteomics_smb/urls.py b/landingzones/configapps/bih_proteomics_smb/urls.py index 8c6581432..a86e75996 100644 --- a/landingzones/configapps/bih_proteomics_smb/urls.py +++ b/landingzones/configapps/bih_proteomics_smb/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from . import views @@ -6,8 +6,8 @@ app_name = 'landingzones.configapps.bih_proteomics_smb' urlpatterns = [ - url( - regex=r'^(?P[0-9a-f-]+)$', + path( + route='', view=views.ZoneTicketGetView.as_view(), name='ticket_get', ) diff --git a/landingzones/tests/test_permissions.py b/landingzones/tests/test_permissions.py index 09b40926a..cba66844c 100644 --- a/landingzones/tests/test_permissions.py +++ b/landingzones/tests/test_permissions.py @@ -5,7 +5,7 @@ # Projectroles dependency from projectroles.models import SODAR_CONSTANTS -from projectroles.tests.test_permissions import TestProjectPermissionBase +from projectroles.tests.test_permissions import ProjectPermissionTestBase # Samplesheets dependency from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR @@ -33,7 +33,7 @@ class LandingzonesPermissionTestBase( LandingZoneMixin, SampleSheetIOMixin, - TestProjectPermissionBase, + ProjectPermissionTestBase, ): """Base class for landingzones permissions tests""" diff --git a/landingzones/tests/test_permissions_api.py b/landingzones/tests/test_permissions_api.py index 5b8a612a3..2a6c08513 100644 --- a/landingzones/tests/test_permissions_api.py +++ b/landingzones/tests/test_permissions_api.py @@ -5,7 +5,7 @@ # Projectroles dependency from projectroles.models import SODAR_CONSTANTS -from projectroles.tests.test_permissions_api import TestProjectAPIPermissionBase +from projectroles.tests.test_permissions_api import ProjectAPIPermissionTestBase # Samplesheets dependency from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR @@ -32,7 +32,7 @@ class ZoneAPIPermissionTestBase( LandingZoneMixin, SampleSheetIOMixin, - TestProjectAPIPermissionBase, + ProjectAPIPermissionTestBase, ): """Base class for landingzones REST API view permission tests""" diff --git a/landingzones/tests/test_ui.py b/landingzones/tests/test_ui.py index 9925feb29..56e0d927f 100644 --- a/landingzones/tests/test_ui.py +++ b/landingzones/tests/test_ui.py @@ -12,7 +12,7 @@ # Projectroles dependency from projectroles.app_settings import AppSettingAPI -from projectroles.tests.test_ui import TestUIBase +from projectroles.tests.test_ui import UITestBase # Samplesheets dependency from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR @@ -36,7 +36,7 @@ class LandingZoneUITestBase( - SampleSheetIOMixin, SheetConfigMixin, LandingZoneMixin, TestUIBase + SampleSheetIOMixin, SheetConfigMixin, LandingZoneMixin, UITestBase ): """Base class for landingzones UI tests""" @@ -51,7 +51,7 @@ def _setup_investigation(self): def _assert_element(self, by, element, expected=True): """Assert element existence for an already logged in user""" - # TODO: Add this into TestUIBase (see bihealth/sodar-core#1104) + # TODO: Add this into UITestBase (see bihealth/sodar-core#1104) if expected: self.assertIsNotNone(self.selenium.find_element(by, element)) else: diff --git a/landingzones/tests/test_views_api.py b/landingzones/tests/test_views_api.py index cf068201d..5dab0c546 100644 --- a/landingzones/tests/test_views_api.py +++ b/landingzones/tests/test_views_api.py @@ -7,7 +7,7 @@ # Projectroles dependency from projectroles.models import SODAR_CONSTANTS from projectroles.plugins import get_backend_api -from projectroles.tests.test_views_api import TestAPIViewsBase +from projectroles.tests.test_views_api import APIViewTestBase # Samplesheets dependency from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR @@ -38,7 +38,7 @@ class TestLandingZoneAPIViewsBase( - LandingZoneMixin, SampleSheetIOMixin, TestAPIViewsBase + LandingZoneMixin, SampleSheetIOMixin, APIViewTestBase ): """Base class for Landingzones API view testing""" diff --git a/landingzones/tests/test_views_taskflow.py b/landingzones/tests/test_views_taskflow.py index 0826c638a..d8ae22702 100644 --- a/landingzones/tests/test_views_taskflow.py +++ b/landingzones/tests/test_views_taskflow.py @@ -28,7 +28,7 @@ # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from landingzones.constants import ( ZONE_STATUS_CREATING, @@ -188,7 +188,7 @@ def test_create_zone(self): """Test landingzones creation with taskflow""" self.assertEqual(LandingZone.objects.count(), 0) self.assertEqual( - ProjectEvent.objects.filter(event_name='zone_create').count(), 0 + TimelineEvent.objects.filter(event_name='zone_create').count(), 0 ) self.assertEqual(len(mail.outbox), 1) @@ -210,7 +210,9 @@ def test_create_zone(self): self.assert_irods_coll(zone) for c in ZONE_BASE_COLLS: self.assert_irods_coll(zone, c, False) - tl_event = ProjectEvent.objects.filter(event_name='zone_create').first() + tl_event = TimelineEvent.objects.filter( + event_name='zone_create' + ).first() expected_extra = { 'title': zone.title, 'assay': str(zone.assay.sodar_uuid), @@ -246,7 +248,9 @@ def test_create_zone_colls(self): self.assert_zone_count(1) zone = LandingZone.objects.first() self.assert_zone_status(zone, ZONE_STATUS_ACTIVE) - tl_event = ProjectEvent.objects.filter(event_name='zone_create').first() + tl_event = TimelineEvent.objects.filter( + event_name='zone_create' + ).first() self.assertEqual(tl_event.extra_data['create_colls'], True) self.assertEqual(tl_event.extra_data['restrict_colls'], False) self.assert_irods_coll(zone) @@ -660,7 +664,7 @@ def test_move_invalid_status(self): self.assertEqual(len(self.assay_coll.data_objects), 0) self.assertEqual(len(mail.outbox), 1) self.assertEqual( - ProjectEvent.objects.filter(event_name='zone_move').count(), 0 + TimelineEvent.objects.filter(event_name='zone_move').count(), 0 ) self.assertEqual( AppAlert.objects.filter(alert_name='zone_move').count(), 0 @@ -678,7 +682,7 @@ def test_move_invalid_status(self): self.assertEqual(len(self.zone_coll.data_objects), 2) self.assertEqual(len(self.assay_coll.data_objects), 0) self.assertEqual(len(mail.outbox), 1) - tl_event = ProjectEvent.objects.filter(event_name='zone_move').first() + tl_event = TimelineEvent.objects.filter(event_name='zone_move').first() self.assertIsNone(tl_event) self.assertEqual( AppAlert.objects.filter(alert_name='zone_move').count(), 0 @@ -695,7 +699,7 @@ def test_move_lock_failure(self): self.assertEqual(len(self.assay_coll.data_objects), 0) self.assertEqual(len(mail.outbox), 1) self.assertEqual( - ProjectEvent.objects.filter(event_name='zone_move').count(), 0 + TimelineEvent.objects.filter(event_name='zone_move').count(), 0 ) self.assertEqual( AppAlert.objects.filter(alert_name='zone_move').count(), 0 @@ -709,8 +713,8 @@ def test_move_lock_failure(self): self.assertEqual(len(self.zone_coll.data_objects), 2) self.assertEqual(len(self.assay_coll.data_objects), 0) self.assertEqual(len(mail.outbox), 1) # TODO: Should this send email? - tl_event = ProjectEvent.objects.filter(event_name='zone_move').first() - self.assertIsInstance(tl_event, ProjectEvent) + tl_event = TimelineEvent.objects.filter(event_name='zone_move').first() + self.assertIsInstance(tl_event, TimelineEvent) self.assertEqual(tl_event.get_status().status_type, ZONE_STATUS_FAILED) # TODO: Create app alerts for async failures (see #1499) self.assertEqual( diff --git a/ontologyaccess/tests/test_permissions.py b/ontologyaccess/tests/test_permissions.py index 1b0acce8c..b244654f0 100644 --- a/ontologyaccess/tests/test_permissions.py +++ b/ontologyaccess/tests/test_permissions.py @@ -3,7 +3,7 @@ from django.urls import reverse # Projectroles dependency -from projectroles.tests.test_permissions import TestSiteAppPermissionBase +from projectroles.tests.test_permissions import SiteAppPermissionTestBase from ontologyaccess.models import DEFAULT_TERM_URL from ontologyaccess.tests.test_models import OBOFormatOntologyModelMixin @@ -29,7 +29,7 @@ class OntologyAccessPermissionTestBase( - OBOFormatOntologyModelMixin, TestSiteAppPermissionBase + OBOFormatOntologyModelMixin, SiteAppPermissionTestBase ): """Base class for ontologyaccess UI view permission tests""" diff --git a/requirements/base.txt b/requirements/base.txt index a77640a9c..2e111f135 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,51 +1,57 @@ # Wheel # NOTE: For best results wheel should be installed separately before other deps -wheel==0.40.0 +wheel==0.42.0 # Setuptools -setuptools==67.6.0 +setuptools==70.0.0 # Django -django==3.2.25 +django==4.2.14 # Configuration -django-environ==0.10.0 +django-environ==0.11.2 # Forms -django-crispy-forms==2.0 -crispy-bootstrap4==2022.1 +django-crispy-forms==2.1 +crispy-bootstrap4==2024.1 # Models -django-model-utils==4.3.1 +django-model-utils==4.4.0 # Password storage argon2-cffi==21.3.0 # Python-PostgreSQL Database Adapter -psycopg2-binary==2.9.5 +psycopg2-binary==2.9.9 # Unicode slugification awesome-slugify==1.6.5 # Time zones support -pytz==2022.7.1 +pytz==2024.1 + +# SVG icon support +django-iconify==0.3 + +# OpenID Connect (OIDC) authentication support +social-auth-app-django==5.4.0 # Redis support -redis>=4.5.4, <4.6 +redis>=5.0.2, <5.1 # Profiling django-cprofile-middleware==1.0.5 -# Versioning -versioneer==0.28 - # Online documentation via django-docs -docutils==0.18.1 -Sphinx==6.2.1 # NOTE: sphinx-rtd-theme v1.2.2 forces <7 +docutils==0.20.1 +Sphinx==7.2.6 django-docs==0.3.3 -sphinx-rtd-theme==1.2.2 +sphinx-rtd-theme==2.0.0 sphinxcontrib-youtube==1.2.0 +# Versioning +versioneer==0.29 + ##################### # SODAR Core imports ##################### @@ -54,27 +60,27 @@ sphinxcontrib-youtube==1.2.0 rules==3.3 # REST framework -djangorestframework==3.14.0 +djangorestframework==3.15.2 # Token authentication django-rest-knox==4.2.0 # Markdown field support -markdown==3.4.1 +markdown==3.5.2 django-markupfield==2.0.1 django-pagedown==2.2.1 -mistune==2.0.5 +mistune==3.0.2 # Pin to avoid issue with v3.9.5 # See issue #166 and bihealth/sodar-core#1225 -django-autocomplete-light==3.9.4 +django-autocomplete-light==3.11.0 # SODAR Core -django-sodar-core==0.13.4 -# -e git+https://github.com/bihealth/sodar-core.git@be012e5536bacf8bfbfe95e3c930324edae0309b#egg=django-sodar-core +# django-sodar-core==1.0.0 +-e git+https://github.com/bihealth/sodar-core.git@70332724630a679c2358e053c48e57530aa3ee8a#egg=django-sodar-core # Celery -celery==5.2.7 +celery==5.3.6 #################### # SODAR app imports diff --git a/requirements/local.txt b/requirements/local.txt index 35d2a276f..73bf77dbe 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,9 +1,14 @@ # Local development dependencies go here -r base.txt -django-extensions==3.2.1 +django-extensions==3.2.3 Werkzeug==3.0.3 -django-debug-toolbar==3.8.1 +django-debug-toolbar==4.3.0 # improved REPL ipdb==0.13.13 + +# OpenAPI support +inflection>=0.5.1, <0.6 +pyyaml>=6.0.1, <6.1 +uritemplate>=4.1.1, <4.2 diff --git a/requirements/test.txt b/requirements/test.txt index 896b65109..a9945a904 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -1,21 +1,21 @@ # Test dependencies go here. -r base.txt -flake8==6.0.0 -django-test-plus==2.2.1 -factory-boy==3.2.1 +flake8==7.0.0 +django-test-plus==2.2.3 +factory-boy==3.3.0 coverage==6.5.0 # NOTE: coveralls 3.3.1 requires <7.0 -django-coverage-plugin==3.0.0 +django-coverage-plugin==3.1.0 # pytest -pytest-django==4.5.2 -pytest-sugar==0.9.6 +pytest-django==4.8.0 +pytest-sugar==1.0.0 # Selenium for UI testing -selenium==4.8.2 +selenium==4.18.1 # BeautifulSoup for HTML testing -beautifulsoup4==4.11.2 +beautifulsoup4==4.12.3 # Black for formatting black==24.3.0 diff --git a/samplesheets/studyapps/cancer/urls.py b/samplesheets/studyapps/cancer/urls.py index 03e80db17..1684c39a5 100644 --- a/samplesheets/studyapps/cancer/urls.py +++ b/samplesheets/studyapps/cancer/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from . import views @@ -6,8 +6,8 @@ app_name = 'samplesheets.studyapps.cancer' urlpatterns = [ - url( - regex=r'^render/igv/(?P[0-9a-f-]+)(\..*)?$', + path( + route='render/igv/', view=views.IGVSessionFileRenderView.as_view(), name='igv', ) diff --git a/samplesheets/studyapps/germline/urls.py b/samplesheets/studyapps/germline/urls.py index 844e6dc12..1f7445a23 100644 --- a/samplesheets/studyapps/germline/urls.py +++ b/samplesheets/studyapps/germline/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import path from . import views @@ -6,8 +6,8 @@ app_name = 'samplesheets.studyapps.germline' urlpatterns = [ - url( - regex=r'^render/igv/(?P[0-9a-f-]+)(\..*)?$', + path( + route='render/igv/', view=views.IGVSessionFileRenderView.as_view(), name='igv', ) diff --git a/samplesheets/tests/test_commands.py b/samplesheets/tests/test_commands.py index 18835784d..72acad22b 100644 --- a/samplesheets/tests/test_commands.py +++ b/samplesheets/tests/test_commands.py @@ -19,7 +19,7 @@ from sodarcache.models import JSONCacheItem # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from samplesheets.management.commands.normalizesheets import ( LIB_NAME, @@ -80,7 +80,7 @@ def _assert_study_table_header(self, study_tables, assay, header, expected): def _assert_tl_event(self, expected): self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( event_name='sheet_normalize', project=self.project ).count(), expected, diff --git a/samplesheets/tests/test_permissions.py b/samplesheets/tests/test_permissions.py index 9fdb4ac8b..68eb2c0b2 100644 --- a/samplesheets/tests/test_permissions.py +++ b/samplesheets/tests/test_permissions.py @@ -7,7 +7,7 @@ # Projectroles dependency from projectroles.app_settings import AppSettingAPI -from projectroles.tests.test_permissions import TestProjectPermissionBase +from projectroles.tests.test_permissions import ProjectPermissionTestBase from projectroles.utils import build_secret from samplesheets.models import ( @@ -37,7 +37,7 @@ class SamplesheetsPermissionTestBase( - SampleSheetIOMixin, TestProjectPermissionBase + SampleSheetIOMixin, ProjectPermissionTestBase ): """Base test class for samplesheets UI view permissions""" @@ -307,7 +307,7 @@ def test_get_sync(self): self.assert_response(self.url, bad_users, 302) -class TestSheetTemplateCreateView(TestProjectPermissionBase): +class TestSheetTemplateCreateView(ProjectPermissionTestBase): """Permission tests for SheetTemplateCreateView""" def setUp(self): diff --git a/samplesheets/tests/test_permissions_ajax.py b/samplesheets/tests/test_permissions_ajax.py index c8b76b718..593388733 100644 --- a/samplesheets/tests/test_permissions_ajax.py +++ b/samplesheets/tests/test_permissions_ajax.py @@ -5,7 +5,7 @@ # Projectroles dependency from projectroles.app_settings import AppSettingAPI -from projectroles.tests.test_permissions import TestProjectPermissionBase +from projectroles.tests.test_permissions import ProjectPermissionTestBase from projectroles.utils import build_secret from samplesheets.models import ( @@ -31,7 +31,7 @@ class SampleSheetsAjaxPermissionTestBase( - SampleSheetIOMixin, TestProjectPermissionBase + SampleSheetIOMixin, ProjectPermissionTestBase ): """Base test class for samplesheets Ajax view permissions""" diff --git a/samplesheets/tests/test_permissions_api.py b/samplesheets/tests/test_permissions_api.py index 96f65084c..fb815a7c2 100644 --- a/samplesheets/tests/test_permissions_api.py +++ b/samplesheets/tests/test_permissions_api.py @@ -11,8 +11,8 @@ # Projectroles dependency from projectroles.models import SODAR_CONSTANTS from projectroles.tests.test_models import RemoteSiteMixin, RemoteProjectMixin -from projectroles.tests.test_permissions import TestProjectPermissionBase -from projectroles.tests.test_permissions_api import TestProjectAPIPermissionBase +from projectroles.tests.test_permissions import ProjectPermissionTestBase +from projectroles.tests.test_permissions_api import ProjectAPIPermissionTestBase from samplesheets.models import ( Investigation, @@ -40,7 +40,7 @@ class TestInvestigationRetrieveAPIView( SampleSheetIOMixin, - TestProjectAPIPermissionBase, + ProjectAPIPermissionTestBase, ): """Tests for InvestigationRetrieveAPIView permissions""" @@ -120,7 +120,7 @@ def test_get_archive(self): self.assert_response_api(url, self.anonymous, 401) -class TestSheetImportAPIView(SampleSheetIOMixin, TestProjectAPIPermissionBase): +class TestSheetImportAPIView(SampleSheetIOMixin, ProjectAPIPermissionTestBase): """Tests for SheetImportAPIView permissions""" def _cleanup_import(self): @@ -291,7 +291,7 @@ def test_post_archive(self): class TestSheetISAExportAPIView( SampleSheetIOMixin, - TestProjectAPIPermissionBase, + ProjectAPIPermissionTestBase, ): """Tests for SheetISAExportAPIView permissions""" @@ -364,7 +364,7 @@ def test_get_archive(self): class TestIrodsAccessTicketListAPIView( - SampleSheetIOMixin, IrodsAccessTicketMixin, TestProjectAPIPermissionBase + SampleSheetIOMixin, IrodsAccessTicketMixin, ProjectAPIPermissionTestBase ): """Test permissions for IrodsAccessTicketListAPIView""" @@ -438,7 +438,7 @@ def test_get_archive(self): class TestIrodsAccessTicketRetrieveAPIView( - SampleSheetIOMixin, IrodsAccessTicketMixin, TestProjectAPIPermissionBase + SampleSheetIOMixin, IrodsAccessTicketMixin, ProjectAPIPermissionTestBase ): """Test permissions for IrodsAccessTicketRetrieveAPIView""" @@ -509,7 +509,7 @@ def test_get_archive(self): class TestIrodsDataRequestRetrieveAPIView( - IrodsDataRequestMixin, TestProjectAPIPermissionBase + IrodsDataRequestMixin, ProjectAPIPermissionTestBase ): """Tests for TestIrodsDataRequestRetrieveAPIView permissions""" @@ -581,7 +581,7 @@ def test_get_archive(self): self.assert_response_api(self.url, self.anonymous, 401) -class TestIrodsDataRequestListAPIView(TestProjectAPIPermissionBase): +class TestIrodsDataRequestListAPIView(ProjectAPIPermissionTestBase): """Tests for TestIrodsDataRequestListAPIView permissions""" def setUp(self): @@ -646,7 +646,7 @@ def test_get_archive(self): class TestIrodsDataRequestRejectAPIView( - IrodsDataRequestMixin, TestProjectAPIPermissionBase + IrodsDataRequestMixin, ProjectAPIPermissionTestBase ): """Test permissions for TestIrodsDataRequestRejectAPIView""" @@ -728,7 +728,7 @@ def test_reject_archive(self): class TestIrodsDataRequestDestroyAPIView( - SampleSheetIOMixin, IrodsDataRequestMixin, TestProjectAPIPermissionBase + SampleSheetIOMixin, IrodsDataRequestMixin, ProjectAPIPermissionTestBase ): """Test permissions for IrodsDataRequestDestroyAPIView""" @@ -816,7 +816,7 @@ class TestRemoteSheetGetAPIView( SampleSheetIOMixin, RemoteSiteMixin, RemoteProjectMixin, - TestProjectPermissionBase, + ProjectPermissionTestBase, ): """Tests for RemoteSheetGetAPIView permissions""" diff --git a/samplesheets/tests/test_tasks_celery_taskflow.py b/samplesheets/tests/test_tasks_celery_taskflow.py index ba97509ca..03ae778b6 100644 --- a/samplesheets/tests/test_tasks_celery_taskflow.py +++ b/samplesheets/tests/test_tasks_celery_taskflow.py @@ -18,7 +18,7 @@ from taskflowbackend.tests.base import TaskflowViewTestBase # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from samplesheets.models import ISATab from samplesheets.tasks_celery import ( @@ -79,7 +79,7 @@ def test_update_cache(self): self.assertEqual( AppAlert.objects.filter(alert_name=CACHE_UPDATE_EVENT).count(), 0 ) - self.assertEqual(ProjectEvent.objects.count(), 2) + self.assertEqual(TimelineEvent.objects.count(), 2) update_project_cache_task( self.project.sodar_uuid, @@ -109,7 +109,7 @@ def test_update_cache(self): ) alert = AppAlert.objects.order_by('-pk').first() self.assertTrue(alert.message.endswith(CACHE_ALERT_MESSAGE)) - self.assertEqual(ProjectEvent.objects.count(), 3) + self.assertEqual(TimelineEvent.objects.count(), 3) def test_update_cache_no_alert(self): """Test cache update with app alert disabled""" @@ -119,7 +119,7 @@ def test_update_cache_no_alert(self): self.assertEqual( AppAlert.objects.filter(alert_name=CACHE_UPDATE_EVENT).count(), 0 ) - self.assertEqual(ProjectEvent.objects.count(), 2) + self.assertEqual(TimelineEvent.objects.count(), 2) update_project_cache_task( self.project.sodar_uuid, self.user.sodar_uuid, add_alert=False @@ -131,7 +131,7 @@ def test_update_cache_no_alert(self): self.assertEqual( AppAlert.objects.filter(alert_name=CACHE_UPDATE_EVENT).count(), 0 ) - self.assertEqual(ProjectEvent.objects.count(), 3) + self.assertEqual(TimelineEvent.objects.count(), 3) def test_update_cache_no_user(self): """Test cache update with no user""" @@ -141,7 +141,7 @@ def test_update_cache_no_user(self): self.assertEqual( AppAlert.objects.filter(alert_name=CACHE_UPDATE_EVENT).count(), 0 ) - self.assertEqual(ProjectEvent.objects.count(), 2) + self.assertEqual(TimelineEvent.objects.count(), 2) update_project_cache_task(self.project.sodar_uuid, None, add_alert=True) @@ -151,7 +151,7 @@ def test_update_cache_no_user(self): self.assertEqual( AppAlert.objects.filter(alert_name=CACHE_UPDATE_EVENT).count(), 0 ) - self.assertEqual(ProjectEvent.objects.count(), 3) + self.assertEqual(TimelineEvent.objects.count(), 3) class TestSheetRemoteSyncTask(SheetRemoteSyncTestBase): diff --git a/samplesheets/tests/test_ui.py b/samplesheets/tests/test_ui.py index f3b5222ba..72b5b09bf 100644 --- a/samplesheets/tests/test_ui.py +++ b/samplesheets/tests/test_ui.py @@ -18,7 +18,7 @@ # Projectroles dependency from projectroles.app_settings import AppSettingAPI from projectroles.plugins import get_backend_api -from projectroles.tests.test_ui import TestUIBase +from projectroles.tests.test_ui import UITestBase from samplesheets.forms import TPL_DIR_FIELD, TPL_DIR_LABEL from samplesheets.models import ( @@ -59,7 +59,7 @@ CONFIG_DATA_UPDATED = json.load(fp) -class SamplesheetsUITestBase(SampleSheetIOMixin, SheetConfigMixin, TestUIBase): +class SamplesheetsUITestBase(SampleSheetIOMixin, SheetConfigMixin, UITestBase): """Base view samplesheets view UI tests""" def setup_investigation(self, config_data=None): @@ -609,7 +609,7 @@ def test_render_davrods_button(self): class TestSheetVersionCompareView( - SampleSheetIOMixin, SheetConfigMixin, TestUIBase + SampleSheetIOMixin, SheetConfigMixin, UITestBase ): """Tests for sheet version compare view UI""" @@ -647,7 +647,7 @@ def test_render(self): class TestSheetVersionCompareFileView( - SampleSheetIOMixin, SheetConfigMixin, TestUIBase + SampleSheetIOMixin, SheetConfigMixin, UITestBase ): """Tests for sheet version compare file view UI""" diff --git a/samplesheets/tests/test_views_ajax.py b/samplesheets/tests/test_views_ajax.py index 2e5e8e5b0..e17be5e18 100644 --- a/samplesheets/tests/test_views_ajax.py +++ b/samplesheets/tests/test_views_ajax.py @@ -17,7 +17,7 @@ from sodarcache.models import JSONCacheItem # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent # Ontologyaccess dependency from ontologyaccess.io import OBOFormatOntologyIO @@ -1834,7 +1834,7 @@ def test_post_study_column(self): ) self.assertEqual(sheet_config, CONFIG_DATA_DEFAULT) self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( project=self.project, app=APP_NAME, event_name='field_update', @@ -1871,7 +1871,7 @@ def test_post_study_column(self): expected, ) self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( project=self.project, app=APP_NAME, event_name='field_update', diff --git a/samplesheets/tests/test_views_api.py b/samplesheets/tests/test_views_api.py index 061619aa7..0fd345db9 100644 --- a/samplesheets/tests/test_views_api.py +++ b/samplesheets/tests/test_views_api.py @@ -14,13 +14,13 @@ from projectroles.models import SODAR_CONSTANTS from projectroles.plugins import get_backend_api from projectroles.tests.test_models import RemoteSiteMixin, RemoteProjectMixin -from projectroles.tests.test_views_api import TestAPIViewsBase +from projectroles.tests.test_views_api import APIViewTestBase # Sodarcache dependency from sodarcache.models import JSONCacheItem # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent # Landingzones dependency from landingzones.models import LandingZone @@ -93,7 +93,7 @@ # TODO: Add testing for study table cache updates -class SampleSheetAPIViewTestBase(SampleSheetIOMixin, TestAPIViewsBase): +class SampleSheetAPIViewTestBase(SampleSheetIOMixin, APIViewTestBase): """Base view for samplesheets API views tests""" @@ -923,9 +923,9 @@ class TestIrodsDataRequestDestroyAPIView( """Tests for IrodsDataRequestDestroyAPIView""" def _assert_tl_count(self, count): - """Assert timeline ProjectEvent count""" + """Assert timeline TimelineEvent count""" self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( event_name='irods_request_delete' ).count(), count, diff --git a/samplesheets/tests/test_views_api_taskflow.py b/samplesheets/tests/test_views_api_taskflow.py index da68980ab..38e17b9d9 100644 --- a/samplesheets/tests/test_views_api_taskflow.py +++ b/samplesheets/tests/test_views_api_taskflow.py @@ -20,7 +20,7 @@ from projectroles.plugins import get_backend_api # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent # Taskflowbackend dependency from taskflowbackend.tests.base import ( @@ -830,9 +830,9 @@ class TestIrodsDataRequestUpdateAPIView( """Tests for IrodsDataRequestUpdateAPIView""" def _assert_tl_count(self, count): - """Assert timeline ProjectEvent count""" + """Assert timeline TimelineEvent count""" self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( event_name='irods_request_update' ).count(), count, diff --git a/samplesheets/tests/test_views_taskflow.py b/samplesheets/tests/test_views_taskflow.py index 179882775..2fcd86350 100644 --- a/samplesheets/tests/test_views_taskflow.py +++ b/samplesheets/tests/test_views_taskflow.py @@ -20,13 +20,13 @@ from projectroles.app_settings import AppSettingAPI from projectroles.models import SODAR_CONSTANTS from projectroles.plugins import get_backend_api -from projectroles.views import MSG_NO_AUTH +from projectroles.views import NO_AUTH_MSG # Appalerts dependency from appalerts.models import AppAlert # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent # Taskflowbackend dependency from taskflowbackend.tests.base import TaskflowViewTestBase @@ -185,7 +185,7 @@ def get_tl_event_count(cls, action): :param action: "create", "update" or "delete" (string) :return: Integer """ - return ProjectEvent.objects.filter( + return TimelineEvent.objects.filter( event_name='irods_ticket_' + action ).count() @@ -1099,9 +1099,9 @@ def _assert_tl_count(self, event_name, count, **kwargs): :param kwargs: Extra kwargs for query (dict, optional) """ timeline = get_backend_api('timeline_backend') - ProjectEvent, _ = timeline.get_models() + TimelineEvent, _ = timeline.get_models() self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( event_name=event_name, **kwargs ).count(), count, @@ -1196,7 +1196,7 @@ def test_post(self): self.assertEqual(obj.description, IRODS_REQUEST_DESC) self._assert_tl_count(EVENT_CREATE, 1) self.assertEqual( - ProjectEvent.objects.get(event_name=EVENT_CREATE).extra_data, + TimelineEvent.objects.get(event_name=EVENT_CREATE).extra_data, { 'action': IRODS_REQUEST_ACTION_DELETE, 'path': obj.path, @@ -1368,7 +1368,7 @@ def test_post(self): self.assertEqual(self.request.description, IRODS_REQUEST_DESC_UPDATE) self._assert_tl_count(EVENT_UPDATE, 1) self.assertEqual( - ProjectEvent.objects.get(event_name=EVENT_UPDATE).extra_data, + TimelineEvent.objects.get(event_name=EVENT_UPDATE).extra_data, { 'action': IRODS_REQUEST_ACTION_DELETE, 'path': self.request.path, @@ -1466,7 +1466,7 @@ def test_post(self): self.assert_irods_obj(self.obj_path) self._assert_tl_count(EVENT_DELETE, 1) self.assertEqual( - ProjectEvent.objects.get(event_name=EVENT_DELETE).extra_data, {} + TimelineEvent.objects.get(event_name=EVENT_DELETE).extra_data, {} ) # Create alerts should be deleted self._assert_alert_count(EVENT_CREATE, self.user, 0) @@ -2268,7 +2268,7 @@ def test_get_contributor(self): self.assertRedirects(response, reverse('home')) self.assertEqual( - list(get_messages(response.wsgi_request))[-1].message, MSG_NO_AUTH + list(get_messages(response.wsgi_request))[-1].message, NO_AUTH_MSG ) request.refresh_from_db() self.assertEqual(request.status, IRODS_REQUEST_STATUS_ACTIVE) diff --git a/samplesheets/views.py b/samplesheets/views.py index b69186297..9de1a377e 100644 --- a/samplesheets/views.py +++ b/samplesheets/views.py @@ -182,7 +182,7 @@ def add_tl_event(self, project, action, tpl_name=None): :param project: Project object :param action: "import", "create" or "replace" (string) :param tpl_name: Optional template name (string) - :return: ProjectEvent object + :return: TimelineEvent object """ if action not in ['create', 'import', 'replace']: raise ValueError('Invalid action "{}"'.format(action)) diff --git a/setup.cfg b/setup.cfg index 23abd30b6..b1cfce7d5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ max-line-length = 80 exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs_manual, node_modules,src/*,config/*,versioneer.py,env/*,.venv,_version.py -ignore = E203, E266, E501, F405, W503, W504, C901 +ignore = E203, E266, E501, F405, W503, W504, C901, E721 max-complexity = 18 select = B,C,E,F,W,T4,B9 diff --git a/setup.py b/setup.py index 414ed0cc8..357dae221 100644 --- a/setup.py +++ b/setup.py @@ -25,14 +25,14 @@ classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', - 'Framework :: Django :: 3.2', + 'Framework :: Django :: 4.2', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX :: Linux', 'Programming Language :: Python', - 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], diff --git a/taskflowbackend/api.py b/taskflowbackend/api.py index 990858ad5..89a4d0b3a 100644 --- a/taskflowbackend/api.py +++ b/taskflowbackend/api.py @@ -105,7 +105,7 @@ def get_flow( :param flow_name: Name of flow (string) :param flow_data: Flow parameters (dict) :param async_mode: Set up flow asynchronously if True (boolean) - :param tl_event: ProjectEvent object for timeline updating or None + :param tl_event: TimelineEvent object for timeline updating or None """ flow_cls = flows.get_flow(flow_name) if not flow_cls: @@ -142,8 +142,8 @@ def run_flow( :param project: Project object :param force_fail: Force failure (boolean, for testing) :param async_mode: Submit in async mode (boolean, default=False) - :param tl_event: Timeline ProjectEvent object or None. Event status will - be updated if the flow is run in async mode + :param tl_event: TimelineEvent object or None. Event status will be + updated if the flow is run in async mode :return: Dict """ flow_result = None @@ -235,7 +235,7 @@ def submit( :param flow_data: Input data for flow execution (dict, must be JSON serializable) :param async_mode: Run flow asynchronously (boolean, default False) - :param tl_event: Corresponding timeline ProjectEvent (optional) + :param tl_event: Corresponding TimelineEvent (optional) :param force_fail: Make flow fail on purpose (boolean, default False) :return: Boolean :raise: FlowSubmitException if submission fails diff --git a/taskflowbackend/tests/base.py b/taskflowbackend/tests/base.py index 2686ee9e9..c5cbf9b7c 100644 --- a/taskflowbackend/tests/base.py +++ b/taskflowbackend/tests/base.py @@ -37,10 +37,13 @@ RoleMixin, RoleAssignmentMixin, ) -from projectroles.tests.test_permissions import TestPermissionMixin +from projectroles.tests.test_permissions import PermissionTestMixin from projectroles.tests.test_permissions_api import SODARAPIPermissionTestMixin from projectroles.tests.test_views_api import SODARAPIViewTestMixin -from projectroles.views_api import CORE_API_MEDIA_TYPE, CORE_API_DEFAULT_VERSION +from projectroles.views_api import ( + PROJECTROLES_API_MEDIA_TYPE, + PROJECTROLES_API_DEFAULT_VERSION, +) app_settings = AppSettingAPI() @@ -433,8 +436,8 @@ def make_project_taskflow( reverse('projectroles:api_project_create'), method='POST', data=post_data, - media_type=CORE_API_MEDIA_TYPE, - version=CORE_API_DEFAULT_VERSION, + media_type=PROJECTROLES_API_MEDIA_TYPE, + version=PROJECTROLES_API_DEFAULT_VERSION, ) # Assert response and object status self.assertEqual(response.status_code, 201, msg=response.content) @@ -452,8 +455,8 @@ def make_assignment_taskflow(self, project, user, role): url, method='POST', data=request_data, - media_type=CORE_API_MEDIA_TYPE, - version=CORE_API_DEFAULT_VERSION, + media_type=PROJECTROLES_API_MEDIA_TYPE, + version=PROJECTROLES_API_DEFAULT_VERSION, ) self.assertEqual(response.status_code, 201, msg=response.content) return RoleAssignment.objects.get(project=project, user=user, role=role) @@ -485,7 +488,7 @@ def setUp(self): class TaskflowPermissionTestBase( TaskflowProjectTestMixin, TaskflowPermissionTestMixin, - TestPermissionMixin, + PermissionTestMixin, TestCase, ): """Base class for testing UI and Ajax view permissions with taskflow""" diff --git a/taskflowbackend/tests/test_plugins.py b/taskflowbackend/tests/test_plugins.py index d6a471008..c75a65f31 100644 --- a/taskflowbackend/tests/test_plugins.py +++ b/taskflowbackend/tests/test_plugins.py @@ -14,7 +14,7 @@ from irodsbackend.api import USER_GROUP_TEMPLATE # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from taskflowbackend.tests.base import TaskflowViewTestBase @@ -97,7 +97,7 @@ def test_create(self): self.irods.users.get(self.user_owner_cat.username), iRODSUser ) self.assert_group_member(project, self.user_owner_cat, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=project, plugin='taskflow', user=self.user, @@ -256,7 +256,7 @@ def test_create_category(self): with self.assertRaises(UserGroupDoesNotExist): self.irods.user_groups.get(group_name) self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( project=category, plugin='taskflow', user=self.user, @@ -446,7 +446,7 @@ def test_revert_create(self): self.assert_irods_coll(self.project, expected=False) with self.assertRaises(UserGroupDoesNotExist): self.irods.user_groups.get(self.group_name) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -485,7 +485,7 @@ def test_create(self): ) self.assert_group_member(self.project, self.user_new, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -508,7 +508,7 @@ def test_create_parent(self): ) self.assert_group_member(self.project, self.user_new, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -532,7 +532,7 @@ def test_create_parent_finder(self): # Should still be False self.assert_group_member(self.project, self.user_new, False) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -595,7 +595,7 @@ def test_update(self): self.assert_group_member(self.project, self.user_new, True) self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -698,7 +698,7 @@ def test_revert_create(self): ) self.assert_group_member(self.project, self.user_new, False) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -721,7 +721,7 @@ def test_revert_create_parent(self): ) self.assert_group_member(self.project, self.user_new, False) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -816,7 +816,7 @@ def test_revert_update(self): ) self.assert_group_member(self.project, self.user_new, True) self.assertEqual( - ProjectEvent.objects.filter( + TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -900,7 +900,7 @@ def test_delete(self): self.assert_group_member(self.project, self.user_new, True) self.plugin.perform_role_delete(self.role_as, self.request) self.assert_group_member(self.project, self.user_new, False) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -917,7 +917,7 @@ def test_delete_parent(self): self.assert_group_member(self.project, self.user_new, True) self.plugin.perform_role_delete(self.role_as, self.request) self.assert_group_member(self.project, self.user_new, False) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -984,7 +984,7 @@ def test_revert(self): self.plugin.revert_role_delete(self.role_as, self.request) self.assert_group_member(self.project, self.user_new, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -1004,7 +1004,7 @@ def test_revert_parent(self): self.plugin.revert_role_delete(self.role_as, self.request) self.assert_group_member(self.project, self.user_new, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -1102,7 +1102,7 @@ def test_transfer_category(self): self.assert_group_member(self.project, self.user, True) self.assert_group_member(self.project, self.user_new, True) self.assert_group_member(self.project, self.user_owner_cat, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.category, plugin='taskflow', user=self.user, @@ -1241,7 +1241,7 @@ def test_sync_new_project(self): self.irods.users.get(self.user_owner_cat.username), iRODSUser ) self.assert_group_member(project, self.user_owner_cat, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=project, plugin='taskflow', user=None, @@ -1276,7 +1276,7 @@ def test_sync_existing(self): ) self.assert_group_member(project, self.user, True) self.assert_group_member(project, self.user_owner_cat, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=project, plugin='taskflow', user=None, diff --git a/taskflowbackend/tests/test_project_api_views.py b/taskflowbackend/tests/test_project_api_views.py index 06c63a500..a05e86aa6 100644 --- a/taskflowbackend/tests/test_project_api_views.py +++ b/taskflowbackend/tests/test_project_api_views.py @@ -12,7 +12,10 @@ from projectroles.app_settings import AppSettingAPI from projectroles.models import Project, RoleAssignment, SODAR_CONSTANTS -from projectroles.views_api import CORE_API_MEDIA_TYPE, CORE_API_DEFAULT_VERSION +from projectroles.views_api import ( + PROJECTROLES_API_MEDIA_TYPE, + PROJECTROLES_API_DEFAULT_VERSION, +) from taskflowbackend.tests.base import TaskflowAPIViewTestBase @@ -47,8 +50,8 @@ class CoreTaskflowAPITestBase(TaskflowAPIViewTestBase): """Override of TestTaskflowAPIBase for SODAR Core API views""" - media_type = CORE_API_MEDIA_TYPE - api_version = CORE_API_DEFAULT_VERSION + media_type = PROJECTROLES_API_MEDIA_TYPE + api_version = PROJECTROLES_API_DEFAULT_VERSION class TestProjectCreateAPIView(CoreTaskflowAPITestBase): diff --git a/taskflowbackend/tests/test_project_views.py b/taskflowbackend/tests/test_project_views.py index 0c9334c48..df8c16b55 100644 --- a/taskflowbackend/tests/test_project_views.py +++ b/taskflowbackend/tests/test_project_views.py @@ -20,7 +20,7 @@ from projectroles.tests.test_models import ProjectInviteMixin # Timeline dependency -from timeline.models import ProjectEvent +from timeline.models import TimelineEvent from taskflowbackend.tests.base import TaskflowViewTestBase @@ -137,7 +137,7 @@ def test_create_project(self): ) self.assertEqual(group.hasmember(self.user_owner_cat.username), True) # Assert timeline event - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=project, plugin='taskflow', user=self.user, @@ -233,7 +233,7 @@ def test_update(self): ) self.assert_group_member(self.project, self.user, True) self.assert_group_member(self.project, self.user_owner_cat, True) - tl_events = ProjectEvent.objects.filter( + tl_events = TimelineEvent.objects.filter( project=self.project, plugin='taskflow', user=self.user, @@ -771,7 +771,7 @@ def test_accept_invite_ldap(self): [ ( reverse( - 'projectroles:invite_process_ldap', + 'projectroles:invite_process_login', kwargs={'secret': invite.secret}, ), 302, @@ -823,7 +823,7 @@ def test_accept_invite_ldap_category(self): [ ( reverse( - 'projectroles:invite_process_ldap', + 'projectroles:invite_process_login', kwargs={'secret': invite.secret}, ), 302, @@ -862,7 +862,7 @@ def test_accept_invite_local(self): [ ( reverse( - 'projectroles:invite_process_local', + 'projectroles:invite_process_new_user', kwargs={'secret': invite.secret}, ), 302, @@ -904,7 +904,7 @@ def test_accept_invite_local_category(self): [ ( reverse( - 'projectroles:invite_process_local', + 'projectroles:invite_process_new_user', kwargs={'secret': invite.secret}, ), 302,