diff --git a/conf/env/dev b/conf/env/dev index 89e1c0c7..bf41e053 100644 --- a/conf/env/dev +++ b/conf/env/dev @@ -30,3 +30,4 @@ ACTIVITY_STREAM_SECRET_ACCESS_KEY=123-secret-key FEATURE_ENFORCE_STAFF_SSO_ENABLED=false USERS_REQUEST_ACCESS_PREVENT_RESUBMISSION=false STATICFILES_STORAGE=django.contrib.staticfiles.storage.StaticFilesStorage +FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED=true \ No newline at end of file diff --git a/conf/preprocessors.py b/conf/preprocessors.py new file mode 100644 index 00000000..82f0a7d9 --- /dev/null +++ b/conf/preprocessors.py @@ -0,0 +1,12 @@ +def preprocessing_filter_admin_spec(endpoints): + """ + Filters all Wagtail Admin API endpoints from the Open API schema generated by drf-spectacular. Spec generated at + /openui/ui/. + """ + + filtered = [] + for path, path_regex, method, callback in endpoints: + # Remove all Wagtail admin endpoints + if not path.startswith('/admin/'): + filtered.append((path, path_regex, method, callback)) + return filtered diff --git a/conf/settings.py b/conf/settings.py index a519b0b9..9eb575d5 100644 --- a/conf/settings.py +++ b/conf/settings.py @@ -90,6 +90,7 @@ 'django_filters', 'authbroker_client', 'django_celery_beat', + 'drf_spectacular', ] MIDDLEWARE = [ @@ -429,7 +430,10 @@ else: LOGIN_URL = '/admin/login/' -REST_FRAMEWORK = {'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)} +REST_FRAMEWORK = { + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) +} if FEATURE_FLAGS['DEBUG_TOOLBAR_ON']: INSTALLED_APPS += ['debug_toolbar'] @@ -475,3 +479,18 @@ FOREIGN_DIRECT_INVESTMENT_SNIPPET_LABEL = env.str( 'FOREIGN_DIRECT_INVESTMENT_SNIPPET_LABEL', FOREIGN_DIRECT_INVESTMENT_SNIPPET_LABEL_DEFAULT ) + + +# Resolves DEFAULT_AUTO_FIELD warnings on Django 3.2 and above +DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' + +# OpenAPI +FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = env.bool('FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED', False) + +SPECTACULAR_SETTINGS = { + 'TITLE': 'Directory CMS API', + 'DESCRIPTION': 'Directory CMS API - the Department for Business and Trade (DBT)', + 'VERSION': os.environ.get('GIT_TAG', 'dev'), + 'SERVE_INCLUDE_SCHEMA': False, + 'PREPROCESSING_HOOKS': ['conf.preprocessors.preprocessing_filter_admin_spec'], +} diff --git a/conf/urls.py b/conf/urls.py index eab87460..b6aa7802 100644 --- a/conf/urls.py +++ b/conf/urls.py @@ -14,6 +14,11 @@ from django.views.decorators.csrf import csrf_exempt from django.views.generic import RedirectView from django.urls import path, re_path +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) import core.views from groups.views import GroupInfoModalView @@ -100,3 +105,19 @@ urlpatterns = [ re_path(r'^__debug__/', include(debug_toolbar.urls)), ] + urlpatterns + + +if settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED: + urlpatterns = [ + path('openapi/', SpectacularAPIView.as_view(), name='schema'), + path( + 'openapi/ui/', + login_required(SpectacularSwaggerView.as_view(url_name='schema'), login_url='admin:login'), + name='swagger-ui', + ), + path( + 'openapi/ui/redoc/', + login_required(SpectacularRedocView.as_view(url_name='schema'), login_url='admin:login'), + name='redoc', + ), + ] + urlpatterns diff --git a/core/static/core/logo.png b/core/static/core/logo.png index 89835825..93f4c2fe 100644 Binary files a/core/static/core/logo.png and b/core/static/core/logo.png differ diff --git a/pull_request_template.md b/pull_request_template.md index fb10d909..bbfa39d4 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -21,6 +21,7 @@ _Tick or delete as appropriate:_ - [ ] Upgraded any vulnerable dependencies. - [ ] I have updated security dependencies - [ ] Python requirements have been re-compiled. +- [ ] I have checked that my PR is using the latest package versions of: sigauth, directory-healthcheck, directory-components,directory-constants, django-staff-sso-client ### Merging diff --git a/requirements.in b/requirements.in index 2af0612c..6a1de019 100644 --- a/requirements.in +++ b/requirements.in @@ -20,7 +20,7 @@ django-filter>=2.4.0 django-redis celery[redis] django-celery-beat==2.5.0 -kombu==5.3.0 +kombu==5.3.1 requests[security]>=2.31.0 markdown==2.* bleach==3.* @@ -39,3 +39,4 @@ psycogreen==1.0.2 wagtailmedia==0.14.* cryptography==41.* oauthlib==3.2.* +drf-spectacular diff --git a/requirements.txt b/requirements.txt index f7378989..1fa258fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ botocore==1.27.96 # via # boto3 # s3transfer -celery[redis]==5.3.0 +celery[redis]==5.3.1 # via # -r requirements.in # django-celery-beat @@ -55,11 +55,11 @@ click-didyoumean==0.3.0 # via celery click-plugins==1.1.1 # via celery -click-repl==0.2.0 +click-repl==0.3.0 # via celery cron-descriptor==1.4.0 # via django-celery-beat -cryptography==41.0.0 +cryptography==41.0.1 # via -r requirements.in directory-components==39.1.2 # via -r requirements.in @@ -88,8 +88,10 @@ django==4.1.9 # django-staff-sso-client # django-storages # django-taggit + # django-timezone-field # django-treebeard # djangorestframework + # drf-spectacular # sigauth # wagtail # wagtail-modeltranslation @@ -114,7 +116,7 @@ django-permissionedforms==0.1 # via wagtail django-pglocks==1.0.4 # via -r requirements.in -django-redis==5.2.0 +django-redis==5.3.0 # via -r requirements.in django-staff-sso-client==4.2.0 # via -r requirements.in @@ -122,13 +124,14 @@ django-storages==1.13.2 # via -r requirements.in django-taggit==3.1.0 # via wagtail -django-timezone-field==5.0 +django-timezone-field==5.1 # via django-celery-beat django-treebeard==4.7 # via wagtail djangorestframework==3.14.0 # via # -r requirements.in + # drf-spectacular # sigauth # wagtail docopt==0.6.2 @@ -137,6 +140,8 @@ docopt==0.6.2 # num2words draftjs-exporter==2.1.7 # via wagtail +drf-spectacular==0.26.3 + # via -r requirements.in ecs-logging==2.0.2 # via elastic-apm elastic-apm==6.16.2 @@ -155,13 +160,17 @@ html5lib==1.1 # via wagtail idna==3.4 # via requests +inflection==0.5.1 + # via drf-spectacular jmespath==1.0.1 # via # boto3 # botocore jsonschema==3.2.0 - # via directory-components -kombu==5.3.0 + # via + # directory-components + # drf-spectacular +kombu==5.3.1 # via # -r requirements.in # celery @@ -214,7 +223,9 @@ pytz==2023.3 # django-timezone-field # djangorestframework # l18n -redis==4.5.5 +pyyaml==6.0 + # via drf-spectacular +redis==4.6.0 # via # celery # django-redis @@ -228,14 +239,13 @@ requests-oauthlib==1.3.1 # via django-staff-sso-client s3transfer==0.6.1 # via boto3 -sentry-sdk==1.25.1 +sentry-sdk==1.26.0 # via -r requirements.in sigauth==5.2.0 # via -r requirements.in six==1.16.0 # via # bleach - # click-repl # django-pglocks # html5lib # jsonschema @@ -249,7 +259,7 @@ sqlparse==0.4.4 # via django telepath==0.3.1 # via wagtail -typing-extensions==4.6.3 +typing-extensions==4.7.0 # via # asgiref # dj-database-url @@ -259,6 +269,8 @@ tzdata==2023.3 # via # celery # django-celery-beat +uritemplate==4.1.1 + # via drf-spectacular urllib3==1.26.16 # via # -r requirements.in @@ -294,7 +306,7 @@ willow==1.4.1 # via wagtail wrapt==1.15.0 # via elastic-apm -zope-event==4.6 +zope-event==5.0 # via gevent zope-interface==6.0 # via gevent diff --git a/requirements_test.in b/requirements_test.in index e0fb70a9..9abf810c 100644 --- a/requirements_test.in +++ b/requirements_test.in @@ -13,4 +13,4 @@ wagtail-factories==2.0.1 django-debug-toolbar==3.2.* pip-tools pytest-codecov -GitPython +GitPython \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 6b1f9f07..6039b6cd 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -34,7 +34,7 @@ botocore==1.27.96 # s3transfer build==0.10.0 # via pip-tools -celery[redis]==5.3.0 +celery[redis]==5.3.1 # via # -r requirements.in # django-celery-beat @@ -58,7 +58,7 @@ click-didyoumean==0.3.0 # via celery click-plugins==1.1.1 # via celery -click-repl==0.2.0 +click-repl==0.3.0 # via celery coverage[toml]==6.5.0 # via @@ -69,7 +69,7 @@ coveralls==3.3.1 # via -r requirements_test.in cron-descriptor==1.4.0 # via django-celery-beat -cryptography==41.0.0 +cryptography==41.0.1 # via -r requirements.in directory-components==39.1.2 # via -r requirements.in @@ -99,8 +99,10 @@ django==4.1.9 # django-staff-sso-client # django-storages # django-taggit + # django-timezone-field # django-treebeard # djangorestframework + # drf-spectacular # sigauth # wagtail # wagtail-modeltranslation @@ -127,7 +129,7 @@ django-permissionedforms==0.1 # via wagtail django-pglocks==1.0.4 # via -r requirements.in -django-redis==5.2.0 +django-redis==5.3.0 # via -r requirements.in django-staff-sso-client==4.2.0 # via -r requirements.in @@ -135,13 +137,14 @@ django-storages==1.13.2 # via -r requirements.in django-taggit==3.1.0 # via wagtail -django-timezone-field==5.0 +django-timezone-field==5.1 # via django-celery-beat django-treebeard==4.7 # via wagtail djangorestframework==3.14.0 # via # -r requirements.in + # drf-spectacular # sigauth # wagtail docopt==0.6.2 @@ -151,6 +154,8 @@ docopt==0.6.2 # num2words draftjs-exporter==2.1.7 # via wagtail +drf-spectacular==0.26.3 + # via -r requirements.in ecs-logging==2.0.2 # via elastic-apm elastic-apm==6.16.2 @@ -163,7 +168,7 @@ factory-boy==2.12.0 # via # -r requirements_test.in # wagtail-factories -faker==18.10.1 +faker==18.11.2 # via factory-boy flake8==6.0.0 # via -r requirements_test.in @@ -185,6 +190,8 @@ html5lib==1.1 # via wagtail idna==3.4 # via requests +inflection==0.5.1 + # via drf-spectacular iniconfig==2.0.0 # via pytest jmespath==1.0.1 @@ -192,8 +199,10 @@ jmespath==1.0.1 # boto3 # botocore jsonschema==3.2.0 - # via directory-components -kombu==5.3.0 + # via + # directory-components + # drf-spectacular +kombu==5.3.1 # via # -r requirements.in # celery @@ -225,7 +234,7 @@ pillow==9.5.0 # via wagtail pip-tools==6.13.0 # via -r requirements_test.in -pluggy==1.0.0 +pluggy==1.2.0 # via pytest prompt-toolkit==3.0.38 # via click-repl @@ -247,7 +256,7 @@ pyproject-hooks==1.0.0 # via build pyrsistent==0.19.3 # via jsonschema -pytest==7.3.2 +pytest==7.4.0 # via # -r requirements_test.in # pytest-codecov @@ -281,7 +290,9 @@ pytz==2023.3 # django-timezone-field # djangorestframework # l18n -redis==4.5.5 +pyyaml==6.0 + # via drf-spectacular +redis==4.6.0 # via # celery # django-redis @@ -300,14 +311,13 @@ requests-oauthlib==1.3.1 # via django-staff-sso-client s3transfer==0.6.1 # via boto3 -sentry-sdk==1.25.1 +sentry-sdk==1.26.0 # via -r requirements.in sigauth==5.2.0 # via -r requirements.in six==1.16.0 # via # bleach - # click-repl # django-pglocks # freezegun # html5lib @@ -335,7 +345,7 @@ tomli==2.0.1 # coverage # pyproject-hooks # pytest -typing-extensions==4.6.3 +typing-extensions==4.7.0 # via # asgiref # dj-database-url @@ -345,6 +355,8 @@ tzdata==2023.3 # via # celery # django-celery-beat +uritemplate==4.1.1 + # via drf-spectacular urllib3==1.26.16 # via # -r requirements.in @@ -385,7 +397,7 @@ willow==1.4.1 # via wagtail wrapt==1.15.0 # via elastic-apm -zope-event==4.6 +zope-event==5.0 # via gevent zope-interface==6.0 # via gevent diff --git a/tests/core/test_preprocessors.py b/tests/core/test_preprocessors.py new file mode 100644 index 00000000..da76d2c8 --- /dev/null +++ b/tests/core/test_preprocessors.py @@ -0,0 +1,46 @@ +import pytest + + +@pytest.mark.django_db +def test_openapi_root_path_open_to_all(client, settings): + settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = True + response = client.get( + '/openapi/' + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_openapi_ui_path_closed_to_user(client, settings): + settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = True + response = client.get( + '/openapi/ui/' + ) + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_openapi_ui_path_open_to_admin(admin_client, settings): + settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = True + response = admin_client.get( + '/openapi/ui/' + ) + assert response.status_code == 200 + + +@pytest.mark.django_db +def test_openapi_redoc_path_closed_to_user(client, settings): + settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = True + response = client.get( + '/openapi/ui/redoc/' + ) + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_openapi_redoc_path_open_to_admin(admin_client, settings): + settings.FEATURE_DIRECTORY_CMS_OPENAPI_ENABLED = True + response = admin_client.get( + '/openapi/ui/redoc/' + ) + assert response.status_code == 200 diff --git a/tests/core/test_wagtail_hooks.py b/tests/core/test_wagtail_hooks.py index 62900a5e..6e05a94b 100644 --- a/tests/core/test_wagtail_hooks.py +++ b/tests/core/test_wagtail_hooks.py @@ -15,9 +15,9 @@ def test_update_default_listing_buttons_from_base_page(page_with_reversion): page=page_with_reversion, page_perms=Mock() ) - expected_url = 'http://great.gov.uk/international/content/123-555-207/' + expected_url = r'http://great.gov.uk/international/content/123-555-[0-9][0-9][0-9]/' assert len(buttons) == 4 - assert buttons[1].url == expected_url + assert re.match(expected_url, buttons[1].url) @pytest.mark.django_db @@ -29,7 +29,7 @@ def test_update_default_listing_buttons_from_base_page_button_url_name_view_draf page=page_with_reversion, page_perms=Mock(), button_url_name=button_url_name, ) - expected_url = r'http://great[.]gov[.]uk/international/content/123-555-209/[?]draft_token=\w+' + expected_url = r'http://great[.]gov[.]uk/international/content/123-555-[0-9][0-9][0-9]/[?]draft_token=\w+' assert len(buttons) == 4 assert re.match(expected_url, buttons[1].url)