Skip to content

Commit

Permalink
Merge pull request #3948 from open-formulieren/feature/3825-public-api
Browse files Browse the repository at this point in the history
Public API endpoints
  • Loading branch information
SilviaAmAm authored Mar 1, 2024
2 parents 0c98b4b + 07901ce commit d6bfb3f
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 0 deletions.
59 changes: 59 additions & 0 deletions src/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3731,6 +3731,38 @@ paths:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/public/forms/:
get:
operationId: public_forms_list
description: List the available forms with minimal information. Does not include
soft deleted forms.
summary: List forms
parameters:
- in: query
name: category__uuid
schema:
type: string
format: uuid
tags:
- public
security:
- tokenAuth: []
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedPublicFormList'
description: ''
headers:
X-Session-Expires-In:
$ref: '#/components/headers/X-Session-Expires-In'
X-CSRFToken:
$ref: '#/components/headers/X-CSRFToken'
X-Is-Form-Designer:
$ref: '#/components/headers/X-Is-Form-Designer'
Content-Language:
$ref: '#/components/headers/Content-Language'
/api/v2/registration/attributes:
get:
operationId: registration_attributes_list
Expand Down Expand Up @@ -8681,6 +8713,13 @@ components:
type: array
items:
$ref: '#/components/schemas/FormDefinition'
PaginatedPublicFormList:
type: object
properties:
results:
type: array
items:
$ref: '#/components/schemas/PublicForm'
PaginatedServiceFetchConfigurationList:
type: object
properties:
Expand Down Expand Up @@ -9219,6 +9258,26 @@ components:
- price
- url
- uuid
PublicForm:
type: object
properties:
uuid:
type: string
format: uuid
name:
type: string
maxLength: 150
internalName:
type: string
description: internal name for management purposes
maxLength: 150
slug:
type: string
maxLength: 100
pattern: ^[-a-zA-Z0-9_]+$
required:
- name
- slug
RegistrationAttribute:
type: object
properties:
Expand Down
7 changes: 7 additions & 0 deletions src/openforms/api/public_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import include, path

app_name = "public"

urlpatterns = [
path("forms/", include("openforms.forms.api.public_api.urls")),
]
1 change: 1 addition & 0 deletions src/openforms/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
path("dmn/", include("openforms.dmn.api.urls")),
path("translations/", include("openforms.translations.urls")),
path("variables/", include("openforms.variables.urls")),
path("public/", include("openforms.api.public_urls")),
path(
"appointments/",
include("openforms.appointments.api.urls"),
Expand Down
13 changes: 13 additions & 0 deletions src/openforms/fixtures/default_groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -1621,5 +1621,18 @@
]
]
}
},
{
"model": "auth.group",
"fields": {
"name": "Public API client",
"permissions": [
[
"view_form",
"forms",
"form"
]
]
}
}
]
Empty file.
10 changes: 10 additions & 0 deletions src/openforms/forms/api/public_api/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django_filters import rest_framework as filters

from ...models import Form


class FormCategoryNameFilter(filters.FilterSet):

class Meta:
model = Form
fields = ("category__uuid",)
18 changes: 18 additions & 0 deletions src/openforms/forms/api/public_api/pagination.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from rest_framework.pagination import BasePagination
from rest_framework.response import Response


class NoPagination(BasePagination):
def paginate_queryset(self, queryset, request, view=None):
return queryset

def get_paginated_response(self, data):
return Response({"results": data})

def get_paginated_response_schema(self, schema):
return {
"type": "object",
"properties": {
"results": schema,
},
}
6 changes: 6 additions & 0 deletions src/openforms/forms/api/public_api/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from rest_framework.permissions import BasePermission


class ViewFormPermission(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm("forms.view_form")
16 changes: 16 additions & 0 deletions src/openforms/forms/api/public_api/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from rest_framework import serializers

from ...models import Form


class FormSerializer(serializers.ModelSerializer):

class Meta:
model = Form
fields = (
"uuid",
"name",
"internal_name",
"slug",
)
ref_name = "PublicForm"
13 changes: 13 additions & 0 deletions src/openforms/forms/api/public_api/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django.urls import path

from .views import FormListView

app_name = "forms"

urlpatterns = [
path(
"",
FormListView.as_view(),
name="forms-list",
),
]
30 changes: 30 additions & 0 deletions src/openforms/forms/api/public_api/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.utils.translation import gettext_lazy as _

from drf_spectacular.utils import extend_schema, extend_schema_view
from rest_framework import permissions
from rest_framework.authentication import TokenAuthentication
from rest_framework.generics import ListAPIView

from ...models import Form
from .filters import FormCategoryNameFilter
from .pagination import NoPagination
from .permissions import ViewFormPermission
from .serializers import FormSerializer


@extend_schema_view(
get=extend_schema(
summary=_("List forms"),
description=_(
"List the available forms with minimal information. Does not include soft deleted forms."
),
),
)
class FormListView(ListAPIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (permissions.IsAuthenticated, ViewFormPermission)

serializer_class = FormSerializer
filterset_class = FormCategoryNameFilter
queryset = Form.objects.filter(_is_deleted=False)
pagination_class = NoPagination
Empty file.
74 changes: 74 additions & 0 deletions src/openforms/forms/tests/public_api/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from rest_framework import status
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase

from openforms.accounts.tests.factories import TokenFactory, UserFactory

from ..factories import CategoryFactory, FormFactory


class TestPublicFormEndpoint(APITestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()

user = UserFactory.create(user_permissions=["forms.view_form"])

blue_cat = CategoryFactory.create(name="Blue")
red_cat = CategoryFactory.create(name="Red")

FormFactory.create(slug="deleted-form", deleted_=True)
FormFactory.create(slug="1-form", category=blue_cat)
FormFactory.create(slug="2-form", category=blue_cat)
FormFactory.create(slug="3-form", category=red_cat)

cls.blue_cat = blue_cat
cls.user = user

def test_cant_access_without_token(self):
response = self.client.get(
reverse(
"api:public:forms:forms-list",
)
)

self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

def test_cant_access_without_view_form_permission(self):
token = TokenFactory()

response = self.client.get(
reverse(
"api:public:forms:forms-list",
),
HTTP_AUTHORIZATION=f"Token {token.key}",
)

self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)

def test_deleted_forms_not_included(self):
token = TokenFactory(user=self.user)

response = self.client.get(
reverse(
"api:public:forms:forms-list",
),
HTTP_AUTHORIZATION=f"Token {token.key}",
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 3)

def test_filter_on_category_uuid(self):
token = TokenFactory(user=self.user)

response = self.client.get(
reverse(
"api:public:forms:forms-list",
),
data={"category__uuid": str(self.blue_cat.uuid)},
HTTP_AUTHORIZATION=f"Token {token.key}",
)

self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data["results"]), 2)

0 comments on commit d6bfb3f

Please sign in to comment.