From 76ccb8ef9178c37823090fbe38f86416f20df2cb Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 10:45:59 +0100 Subject: [PATCH 1/6] add swagger api docs --- backend/elixirapp/settings.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/elixirapp/settings.py b/backend/elixirapp/settings.py index f426419b..871f4443 100644 --- a/backend/elixirapp/settings.py +++ b/backend/elixirapp/settings.py @@ -74,7 +74,8 @@ def getenv(key, default=None, castf=str, ns=ENV_NAMESPACE): 'django_extensions', 'rest_framework_simplejwt', 'background_task', - 'corsheaders' + 'corsheaders', + 'drf_spectacular', ) @@ -202,7 +203,15 @@ def getenv(key, default=None, castf=str, ns=ENV_NAMESPACE): ), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': getenv('PAGE_SIZE', 50, castf=int), - 'NON_FIELD_ERRORS_KEY': 'general_errors' + 'NON_FIELD_ERRORS_KEY': 'general_errors', + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', +} + +# Spectacular settings for API docs +SPECTACULAR_SETTINGS = { + 'TITLE': 'bio.tools API', + 'DESCRIPTION': 'bio.tools is a registry of software tools and databases for life sciences', + 'VERSION': '1.0.0', } # REST Auth @@ -242,7 +251,7 @@ def getenv(key, default=None, castf=str, ns=ENV_NAMESPACE): DEPLOYMENT = getenv('DEPLOYMENT', 'dev') -RESERVED_URL_KEYWORDS = ['t', 'tool', 'user-list', 'edit-permissions', 'validate', 'f', 'function', 'o', 'ontology', 'used-terms', 'stats', 'env', 'sitemap.xml', 'd', 'domain', 'request', 'tool-list', 'w', 'register', 'edit-subdomain', 'subdomain', 'login', 'signup', 'reset-password', 'profile', 'requests', 'workflows', '404', 'documentation', 'about', 'schema', 'governance', 'roadmap', 'events', 'mail', 'faq', 'apidoc', 'changelog', 'helpdesk', 'projects'] +RESERVED_URL_KEYWORDS = ['t', 'tool', 'user-list', 'edit-permissions', 'validate', 'f', 'function', 'o', 'ontology', 'used-terms', 'stats', 'env', 'sitemap.xml', 'd', 'domain', 'request', 'tool-list', 'w', 'register', 'edit-subdomain', 'subdomain', 'login', 'signup', 'reset-password', 'profile', 'requests', 'workflows', '404', 'documentation', 'about', 'schema', 'governance', 'roadmap', 'events', 'mail', 'faq', 'apidoc', 'changelog', 'helpdesk', 'projects', 'redoc'] # Settings for Github Ecosystem From ceaabe29588abce0c2200b5dbd45710b6a2960f7 Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 10:49:15 +0100 Subject: [PATCH 2/6] add spectacular urls --- backend/elixirapp/urls.py | 11 ++++++++--- backend/requirements.txt | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/elixirapp/urls.py b/backend/elixirapp/urls.py index 14f50188..039eee1d 100644 --- a/backend/elixirapp/urls.py +++ b/backend/elixirapp/urls.py @@ -17,15 +17,14 @@ from django.contrib import admin from django.urls import include, path, re_path from elixir import views +from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView urlpatterns = [ re_path(r'^admin/', admin.site.urls), - # url(r'^', include('django.contrib.auth.urls')), - # url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - # url(r'^api-token-auth/', 'rest_framework_jwt.views.obtain_jwt_token'), path('rest-auth/', include('dj_rest_auth.urls')), path('rest-auth/registration/', include('dj_rest_auth.registration.urls')), + # Password reset path('rest-auth/password/reset/', PasswordResetView.as_view(), @@ -35,6 +34,12 @@ PasswordResetConfirmView.as_view(), name='password_reset_confirm'), path('accounts/', include('allauth.urls')), + + # API Documentation + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), + path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path('', include('elixir.urls')), # Social auth diff --git a/backend/requirements.txt b/backend/requirements.txt index 5b664e02..260212d5 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,6 +11,7 @@ djangorestframework-xml==2.0.0 djangorestframework-yaml==2.0.0 djangorestframework-simplejwt==5.4.0 dj-rest-auth==7.0.1 +drf-spectacular==0.27.2 elasticsearch==7.12.1 jsonfield==3.1.0 lxml==5.3.0 From d8124f9263a203dc45ffcbd44f31c55625f1d450 Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 14:41:47 +0100 Subject: [PATCH 3/6] fix api urls for swagger --- backend/elixirapp/urls.py | 86 +++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/backend/elixirapp/urls.py b/backend/elixirapp/urls.py index 039eee1d..38cc1f9f 100644 --- a/backend/elixirapp/urls.py +++ b/backend/elixirapp/urls.py @@ -1,50 +1,66 @@ """elixirapp URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ + https://docs.djangoproject.com/en/1.8/topics/http/urls/ Examples: Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf - 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) + 1. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ + from dj_rest_auth.views import PasswordResetView, PasswordResetConfirmView -from dj_rest_auth.registration.views import SocialAccountListView, SocialAccountDisconnectView +from dj_rest_auth.registration.views import ( + SocialAccountListView, + SocialAccountDisconnectView, +) from django.contrib import admin from django.urls import include, path, re_path from elixir import views -from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularSwaggerView, +) urlpatterns = [ - re_path(r'^admin/', admin.site.urls), - path('rest-auth/', include('dj_rest_auth.urls')), - path('rest-auth/registration/', - include('dj_rest_auth.registration.urls')), - - # Password reset - path('rest-auth/password/reset/', - PasswordResetView.as_view(), - name='rest_password_reset' - ), - path('rest-auth/password/reset/confirm///', - PasswordResetConfirmView.as_view(), - name='password_reset_confirm'), - path('accounts/', include('allauth.urls')), - - # API Documentation - path('api/schema/', SpectacularAPIView.as_view(), name='schema'), - path('api/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), - path('api/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), - - path('', include('elixir.urls')), - - # Social auth - path('rest-auth/orcid/', views.OrcidLogin.as_view(), name='orcid_login'), - path('rest-auth/orcid/connect/', views.OrcidConnect.as_view(), name='orcid_connect'), - path('rest-auth/socialaccounts/', SocialAccountListView.as_view(), name='social_account_list'), - path('rest-auth/socialaccounts//disconnect/', SocialAccountDisconnectView.as_view(), name='social_account_disconnect'), + re_path(r"^admin/", admin.site.urls), + path("rest-auth/", include("dj_rest_auth.urls")), + path("rest-auth/registration/", include("dj_rest_auth.registration.urls")), + # Password reset + path( + "rest-auth/password/reset/", + PasswordResetView.as_view(), + name="rest_password_reset", + ), + path( + "rest-auth/password/reset/confirm///", + PasswordResetConfirmView.as_view(), + name="password_reset_confirm", + ), + path("accounts/", include("allauth.urls")), + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("", include("elixir.urls")), + # Swagger for /api + re_path( + r"^$", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger-ui" + ), + # Social auth + path("rest-auth/orcid/", views.OrcidLogin.as_view(), name="orcid_login"), + path( + "rest-auth/orcid/connect/", views.OrcidConnect.as_view(), name="orcid_connect" + ), + path( + "rest-auth/socialaccounts/", + SocialAccountListView.as_view(), + name="social_account_list", + ), + path( + "rest-auth/socialaccounts//disconnect/", + SocialAccountDisconnectView.as_view(), + name="social_account_disconnect", + ), ] From fdf26046776e0f6da0a432287d8a6ac5b48b4c44 Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 14:44:37 +0100 Subject: [PATCH 4/6] add schema path prefix and favicon for swagger ui --- backend/elixirapp/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/elixirapp/settings.py b/backend/elixirapp/settings.py index 871f4443..1cf68df8 100644 --- a/backend/elixirapp/settings.py +++ b/backend/elixirapp/settings.py @@ -212,6 +212,8 @@ def getenv(key, default=None, castf=str, ns=ENV_NAMESPACE): 'TITLE': 'bio.tools API', 'DESCRIPTION': 'bio.tools is a registry of software tools and databases for life sciences', 'VERSION': '1.0.0', + 'SCHEMA_PATH_PREFIX': '/api', + 'SWAGGER_UI_FAVICON_HREF': '/img/favicon.ico', } # REST Auth From 5321df4c1d2eae0c933143e5e37ed5cbcf4703a4 Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 14:48:00 +0100 Subject: [PATCH 5/6] update urls patterns to match swagger --- backend/elixir/urls.py | 93 +++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/backend/elixir/urls.py b/backend/elixir/urls.py index e09835c3..f3078627 100644 --- a/backend/elixir/urls.py +++ b/backend/elixir/urls.py @@ -1,6 +1,5 @@ # patterns is no longer supported, to achieve the same effect use re_path from django.urls import re_path -from django.urls import re_path # patterns from django.conf.urls.static import static from django.views.static import serve from django.conf import settings @@ -9,35 +8,63 @@ from elixir import edit_permissions urlpatterns = [ - re_path(r'^user-list/?$', views.UserList.as_view()), - re_path(r'^edit-permissions/?$', edit_permissions.EditPermissions.as_view()), - re_path(r'^edit-permissions/(?P[0-9]+)/?$', edit_permissions.EditPermissions.as_view()), - re_path(r'^t(ool)?/?$', views.ResourceList.as_view()), - re_path(r'^t(ool)?/validate/?$', views.ResourceCreateValidator.as_view()), - re_path(r'^t(ool)?/(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/?$', views.ResourceDetail.as_view()), - re_path(r'^t(ool)?/(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/validate/?$', views.ResourceUpdateValidator.as_view()), - # url(r'^t(ool)?/(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/i(ssues)?/(?P[a-zA-Z0-9.~_-]+)?$', views.IssueView.as_view()), - re_path(r'^t(ool)?/(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/disown/?$', views.DisownResourceView.as_view()), - re_path(r'^f(unction)?/?$', views.FunctionList.as_view()), - re_path(r'^o(ntology)?/(?P[a-zA-Z0-9.~_-]+)/?$', views.OntologyDetail.as_view()), - re_path(r'^used-terms/(?P[a-zA-Z0-9.~_-]+)/?$', views.UsedTermsList.as_view()), - re_path(r'^stats/?$', views.Stats.as_view()), - re_path(r'^stats/total-entries/?$', views.TotalEntriesStats.as_view()), - re_path(r'^stats/annotation-count/?$', views.AnnotationCountStats.as_view()), - re_path(r'^stats/users/?$', views.UserStats.as_view()), - re_path(r'^env/?$', views.Environment.as_view()), - re_path(r'^sitemap.xml$', sitemap.Sitemap.as_view()), - re_path(r'^d(omain)?/?$', views.DomainView.as_view()), - re_path(r'^d(omain)?/(?P[a-zA-Z0-9.~_-]+)/?$', views.DomainResourceView.as_view()), - re_path(r'^request/?$', views.ResourceRequestView.as_view()), - re_path(r'^request/conclude/?$', views.ProcessResourceRequest.as_view()), - re_path(r'^tool-list/?$', views.ToolList.as_view()), - re_path(r'^w/?$', views.WorkflowView.as_view()), - re_path(r'^w/(?P[a-zA-Z0-9.~_-]+)/?$', views.WorkflowDetailView.as_view()), - re_path(r'^(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/?$', views.ResourceDetail.as_view()), - re_path(r'^(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/validate/?$', views.ResourceUpdateValidator.as_view()), - # url(r'^(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/i(ssues)?/(?P[a-zA-Z0-9.~_-]+)?$', views.IssueView.as_view()), - re_path(r'^(biotools\:)?(?P[a-zA-Z0-9.~_-]+)/disown/?$', views.DisownResourceView.as_view()), + re_path(r"^user-list/?$", views.UserList.as_view()), + re_path(r"^edit-permissions/?$", edit_permissions.EditPermissions.as_view()), + re_path( + r"^edit-permissions/(?P[0-9]+)/?$", + edit_permissions.EditPermissions.as_view(), + ), + re_path(r"^t(?:ool)?/?$", views.ResourceList.as_view()), + re_path(r"^t(?:ool)?/validate/?$", views.ResourceCreateValidator.as_view()), + re_path( + r"^t(?:ool)?/(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/?$", + views.ResourceDetail.as_view(), + ), + re_path( + r"^t(?:ool)?/(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/validate/?$", + views.ResourceUpdateValidator.as_view(), + ), + # url(r'^t(?:ool)?/(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/i(?:ssues)?/(?P[a-zA-Z0-9.~_-]+)?$', views.IssueView.as_view()), + re_path( + r"^t(?:ool)?/(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/disown/?$", + views.DisownResourceView.as_view(), + ), + re_path(r"^f(?:unction)?/?$", views.FunctionList.as_view()), + re_path( + r"^o(?:ntology)?/(?P[a-zA-Z0-9.~_-]+)/?$", views.OntologyDetail.as_view() + ), + re_path( + r"^used-terms/(?P[a-zA-Z0-9.~_-]+)/?$", views.UsedTermsList.as_view() + ), + re_path(r"^stats/?$", views.Stats.as_view()), + re_path(r"^stats/total-entries/?$", views.TotalEntriesStats.as_view()), + re_path(r"^stats/annotation-count/?$", views.AnnotationCountStats.as_view()), + re_path(r"^stats/users/?$", views.UserStats.as_view()), + re_path(r"^env/?$", views.Environment.as_view()), + re_path(r"^sitemap.xml$", sitemap.Sitemap.as_view()), + re_path(r"^d(?:omain)?/?$", views.DomainView.as_view()), + re_path( + r"^d(?:omain)?/(?P[a-zA-Z0-9.~_-]+)/?$", + views.DomainResourceView.as_view(), + ), + re_path(r"^request/?$", views.ResourceRequestView.as_view()), + re_path(r"^request/conclude/?$", views.ProcessResourceRequest.as_view()), + re_path(r"^tool-list/?$", views.ToolList.as_view()), + re_path(r"^w/?$", views.WorkflowView.as_view()), + re_path(r"^w/(?P[a-zA-Z0-9.~_-]+)/?$", views.WorkflowDetailView.as_view()), + re_path( + r"^(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/?$", + views.ResourceDetail.as_view(), + ), + re_path( + r"^(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/validate/?$", + views.ResourceUpdateValidator.as_view(), + ), + # url(r'^(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/i(?:ssues)?/(?P[a-zA-Z0-9.~_-]+)?$', views.IssueView.as_view()), + re_path( + r"^(?:biotools\:)?(?P[a-zA-Z0-9.~_-]+)/disown/?$", + views.DisownResourceView.as_view(), + ), ] urlpatterns = format_suffix_patterns(urlpatterns) @@ -48,7 +75,5 @@ urlpatterns += [ - re_path(r'^media/(?P.*)$', serve, { - 'document_root': settings.MEDIA_ROOT - }) -] \ No newline at end of file + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT}) +] From 4c0abcf080fb7bff1a099746a8a8ee718afbc00c Mon Sep 17 00:00:00 2001 From: anitamnd Date: Mon, 23 Feb 2026 14:57:17 +0100 Subject: [PATCH 6/6] bump drf-spectacular --- backend/requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 260212d5..a80ce3c2 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,7 +11,7 @@ djangorestframework-xml==2.0.0 djangorestframework-yaml==2.0.0 djangorestframework-simplejwt==5.4.0 dj-rest-auth==7.0.1 -drf-spectacular==0.27.2 +drf-spectacular==0.29.0 elasticsearch==7.12.1 jsonfield==3.1.0 lxml==5.3.0 @@ -30,4 +30,3 @@ rstr==3.2.2 six==1.17.0 tzdata==2025.2 urllib3==1.26.20 -