From b7977cff007756f73e2fe945c6b3e867d97811a4 Mon Sep 17 00:00:00 2001 From: David Slusser Date: Fri, 22 Aug 2025 16:47:24 -0700 Subject: [PATCH 1/5] adding rest apis --- src/django_project/core/settings.py | 5 +- src/django_project/core/urls/rest.py | 14 ++- src/django_project/core/urls/urls.py | 2 +- src/django_project/web/admin.py | 14 +-- src/django_project/web/filtersets.py | 109 ++++++++++++++++++++++- src/django_project/web/serializers.py | 108 +++++++++++++++++++++- src/django_project/web/urls/__init__.py | 3 +- src/django_project/web/urls/rest.py | 15 ---- src/django_project/web/views/rest.py | 11 --- src/django_project/web/views/viewsets.py | 101 +++++++++++++++++++++ 10 files changed, 337 insertions(+), 45 deletions(-) delete mode 100644 src/django_project/web/urls/rest.py delete mode 100644 src/django_project/web/views/rest.py create mode 100644 src/django_project/web/views/viewsets.py diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index c6a0cf6..f7dcfc6 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -54,6 +54,7 @@ "django.contrib.messages", "django.contrib.staticfiles", # third party apps + "djangoaddicts.codegen", "django_extensions", "django_filters", "drf_spectacular", @@ -268,7 +269,9 @@ # drf configuration REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",), - "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticatedOrReadOnly",), + "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticatedOrReadOnly",) + if DEBUG + else ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", diff --git a/src/django_project/core/urls/rest.py b/src/django_project/core/urls/rest.py index e2da145..7f5dacc 100644 --- a/src/django_project/core/urls/rest.py +++ b/src/django_project/core/urls/rest.py @@ -1,14 +1,22 @@ from django.conf.urls import include from django.urls import path from rest_framework import routers +from web.views import viewsets app_name = "rest" -v1_router = routers.DefaultRouter() +router = routers.DefaultRouter() + +# web API Endpoints +router.register(r"event", viewsets.EventViewSet, "event") +router.register(r"presentationrequest", viewsets.PresentationRequestViewSet, "presentationrequest") +router.register(r"resource", viewsets.ResourceViewSet, "resource") +router.register(r"resourcecategory", viewsets.ResourceCategoryViewSet, "resourcecategory") +router.register(r"topicsuggestion", viewsets.TopicSuggestionViewSet, "topicsuggestion") urlpatterns = [ # API views - path("", include(v1_router.urls)), - path("v1/", include(v1_router.urls)), + path("", include(router.urls)), + path("v1/", include(router.urls)), ] diff --git a/src/django_project/core/urls/urls.py b/src/django_project/core/urls/urls.py index 4510b43..6cfcfe9 100644 --- a/src/django_project/core/urls/urls.py +++ b/src/django_project/core/urls/urls.py @@ -31,7 +31,7 @@ # 3rd party URLs path("handyhelpers/", include("handyhelpers.urls")), # RESTful API URLs - path("rest/", include("core.urls.rest", namespace="rest")), + path("rest/", include("core.urls.rest", namespace="")), path("rest/schema/", SpectacularAPIView.as_view(), name="schema"), path("rest/swagger/", SpectacularSwaggerView.as_view(url_name="schema"), name="swagger"), path("rest/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), diff --git a/src/django_project/web/admin.py b/src/django_project/web/admin.py index 714c02f..fb4f720 100644 --- a/src/django_project/web/admin.py +++ b/src/django_project/web/admin.py @@ -12,19 +12,19 @@ class ResourceCategoryAdmin(admin.ModelAdmin): - list_display: list[str] = ["id", "created_at", "updated_at", "name"] + list_display: list[str] = ["id", "name", "created_at", "updated_at"] search_fields: list[str] = ["id", "name"] list_filter: list[str | type[ListFilter] | tuple[str, type[ListFilter]]] = [] class ResourceAdmin(admin.ModelAdmin): - list_display: list[str] = ["id", "created_at", "updated_at", "name", "description", "url", "category"] + list_display: list[str] = ["id", "name", "description", "url", "category", "created_at", "updated_at"] search_fields: list[str] = ["id", "name", "description", "url"] list_filter: list[str] = ["category"] class TopicSuggestionAdmin(admin.ModelAdmin): - list_display: list[str] = ["id", "created_at", "updated_at", "title", "description", "skill_level", "email"] + list_display: list[str] = ["id", "title", "description", "skill_level", "email", "created_at", "updated_at"] search_fields: list[str] = ["id", "title", "description", "skill_level", "email"] list_filter: list[str] = ["skill_level"] @@ -32,13 +32,13 @@ class TopicSuggestionAdmin(admin.ModelAdmin): class PresentationRequestAdmin(admin.ModelAdmin): list_display: list[str] = [ "id", - "created_at", - "updated_at", "presenter", "email", "title", "description", "skill_level", + "created_at", + "updated_at", ] search_fields: list[str] = ["id", "presenter", "email", "title", "description", "skill_level"] list_filter: list[str] = ["skill_level"] @@ -47,13 +47,13 @@ class PresentationRequestAdmin(admin.ModelAdmin): class EventAdmin(admin.ModelAdmin): list_display: list[str] = [ "id", - "created_at", - "updated_at", "name", "start_date_time", "end_date_time", "location", "description", + "created_at", + "updated_at", ] search_fields: list[str] = ["id", "name", "location", "description"] list_filter: list[str | type[ListFilter] | tuple[str, type[ListFilter]]] = [] diff --git a/src/django_project/web/filtersets.py b/src/django_project/web/filtersets.py index 82cb49c..3446b07 100644 --- a/src/django_project/web/filtersets.py +++ b/src/django_project/web/filtersets.py @@ -1,5 +1,110 @@ -"""filtersets for applicable web models""" +"""filtersets for applicable app models""" +from rest_framework_filters.filters import BooleanFilter, RelatedFilter +from rest_framework_filters.filterset import FilterSet # import models -# from web.models import () +from web.models import ( + Event, + PresentationRequest, + Resource, + ResourceCategory, + TopicSuggestion, +) + + +class EventFilterSet(FilterSet): + """filterset class for Event""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = Event + fields = { + "created_at": "__all__", + "description": "__all__", + "end_date_time": "__all__", + "id": "__all__", + "location": "__all__", + "name": "__all__", + "start_date_time": "__all__", + "updated_at": "__all__", + "url": "__all__", + } + + +class PresentationRequestFilterSet(FilterSet): + """filterset class for PresentationRequest""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = PresentationRequest + fields = { + "created_at": "__all__", + "description": "__all__", + "email": "__all__", + "id": "__all__", + "presenter": "__all__", + "skill_level": "__all__", + "title": "__all__", + "updated_at": "__all__", + } + + +class ResourceFilterSet(FilterSet): + """filterset class for Resource""" + + category = RelatedFilter( + "ResourceCategoryFilterSet", field_name="category", queryset=ResourceCategory.objects.all() + ) + + class Meta: + """Metaclass to define filterset model and fields""" + + model = Resource + fields = { + "category": "__all__", + "created_at": "__all__", + "description": "__all__", + "id": "__all__", + "name": "__all__", + "updated_at": "__all__", + "url": "__all__", + } + + +class ResourceCategoryFilterSet(FilterSet): + """filterset class for ResourceCategory""" + + resources = RelatedFilter("ResourceFilterSet", field_name="resources", queryset=Resource.objects.all()) + has_resources = BooleanFilter(field_name="resources", lookup_expr="isnull", exclude=True) + + class Meta: + """Metaclass to define filterset model and fields""" + + model = ResourceCategory + fields = { + "created_at": "__all__", + "id": "__all__", + "name": "__all__", + "updated_at": "__all__", + } + + +class TopicSuggestionFilterSet(FilterSet): + """filterset class for TopicSuggestion""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = TopicSuggestion + fields = { + "created_at": "__all__", + "description": "__all__", + "email": "__all__", + "id": "__all__", + "skill_level": "__all__", + "title": "__all__", + "updated_at": "__all__", + } diff --git a/src/django_project/web/serializers.py b/src/django_project/web/serializers.py index 3c7a4a3..eae2af3 100644 --- a/src/django_project/web/serializers.py +++ b/src/django_project/web/serializers.py @@ -1,5 +1,107 @@ -"""DRF serailizers for applicable web models""" - +from rest_flex_fields import FlexFieldsModelSerializer +from rest_framework import serializers # import models -# from web.models import () +from web.models import ( + Event, + PresentationRequest, + Resource, + ResourceCategory, + TopicSuggestion, +) + + +class EventSerializer(FlexFieldsModelSerializer): + """serializer class for Event""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = Event + fields = [ + "created_at", + "description", + "end_date_time", + "id", + "location", + "name", + "start_date_time", + "updated_at", + "url", + ] + + +class PresentationRequestSerializer(FlexFieldsModelSerializer): + """serializer class for PresentationRequest""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = PresentationRequest + fields = [ + "created_at", + "description", + "email", + "id", + "presenter", + "skill_level", + "title", + "updated_at", + ] + + +class ResourceSerializer(FlexFieldsModelSerializer): + """serializer class for Resource""" + + category = serializers.StringRelatedField() + + class Meta: + """Metaclass to define filterset model and fields""" + + model = Resource + fields = [ + "category", + "created_at", + "description", + "id", + "name", + "updated_at", + "url", + ] + + expandable_fields = { + "category": "web.serializers.ResourceCategorySerializer", + } + + +class ResourceCategorySerializer(FlexFieldsModelSerializer): + """serializer class for ResourceCategory""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = ResourceCategory + fields = [ + "created_at", + "id", + "name", + "updated_at", + ] + + +class TopicSuggestionSerializer(FlexFieldsModelSerializer): + """serializer class for TopicSuggestion""" + + class Meta: + """Metaclass to define filterset model and fields""" + + model = TopicSuggestion + fields = [ + "created_at", + "description", + "email", + "id", + "skill_level", + "title", + "updated_at", + ] diff --git a/src/django_project/web/urls/__init__.py b/src/django_project/web/urls/__init__.py index a18d1de..c01e19a 100644 --- a/src/django_project/web/urls/__init__.py +++ b/src/django_project/web/urls/__init__.py @@ -1,6 +1,5 @@ from .gui import urlpatterns as gui_urls -from .rest import urlpatterns as rest_urls app_name = "web" -urlpatterns = rest_urls + gui_urls +urlpatterns = gui_urls diff --git a/src/django_project/web/urls/rest.py b/src/django_project/web/urls/rest.py deleted file mode 100644 index 97b6fe7..0000000 --- a/src/django_project/web/urls/rest.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.conf.urls import include -from django.urls import path -from rest_framework import routers - -router = routers.DefaultRouter() - - -# web API Endpoints -# router.register(r"model_name", rest.ModelViewSet, "model_name") - - -urlpatterns = [ - # API views - path("rest/", include(router.urls)), -] diff --git a/src/django_project/web/views/rest.py b/src/django_project/web/views/rest.py deleted file mode 100644 index 827510b..0000000 --- a/src/django_project/web/views/rest.py +++ /dev/null @@ -1,11 +0,0 @@ -"""DRF viewsets for applicable app models""" - - -# import models -# from web.models import () - -# import serializers -# from web.serializers import () - -# import filtersets -# from web.filtersets import () diff --git a/src/django_project/web/views/viewsets.py b/src/django_project/web/views/viewsets.py new file mode 100644 index 0000000..77d1f5b --- /dev/null +++ b/src/django_project/web/views/viewsets.py @@ -0,0 +1,101 @@ +"""DRF viewsets for applicable app models""" + +from rest_flex_fields import is_expanded +from rest_framework import status, viewsets +from rest_framework.decorators import action +from rest_framework.response import Response + +# import filtersets +from web.filtersets import ( + EventFilterSet, + PresentationRequestFilterSet, + ResourceCategoryFilterSet, + ResourceFilterSet, + TopicSuggestionFilterSet, +) + +# import models +from web.models import ( + Event, + PresentationRequest, + Resource, + ResourceCategory, + TopicSuggestion, +) + +# import serializers +from web.serializers import ( + EventSerializer, + PresentationRequestSerializer, + ResourceCategorySerializer, + ResourceSerializer, + TopicSuggestionSerializer, +) + + +class EventViewSet(viewsets.ModelViewSet): + """API endpoint that allows Events to be viewed""" + + model = Event + queryset = model.objects.all() + serializer_class = EventSerializer + filterset_class = EventFilterSet + + +class PresentationRequestViewSet(viewsets.ModelViewSet): + """API endpoint that allows PresentationRequests to be viewed""" + + model = PresentationRequest + queryset = model.objects.all() + serializer_class = PresentationRequestSerializer + filterset_class = PresentationRequestFilterSet + + +class ResourceViewSet(viewsets.ModelViewSet): + """API endpoint that allows Resources to be viewed""" + + model = Resource + serializer_class = ResourceSerializer + filterset_class = ResourceFilterSet + + def get_queryset(self): + queryset = self.model.objects.all().select_related( + "category", + ) + + if is_expanded(self.request, "category"): + queryset = queryset.select_related("category") + + return queryset + + +class ResourceCategoryViewSet(viewsets.ModelViewSet): + """API endpoint that allows ResourceCategorys to be viewed""" + + model = ResourceCategory + queryset = model.objects.all() + serializer_class = ResourceCategorySerializer + filterset_class = ResourceCategoryFilterSet + + @action(detail=True, methods=["get"]) + def resources(self, request, *args, **kwargs): + """get the resourcess associated with this ResourceCategory instance if available""" + instance = self.get_object() + data = instance.resources.all() + if data: + try: + serializer = ResourceSerializer(data, many=True) + return Response(serializer.data, status.HTTP_200_OK) + except Exception as err: + return Response(str(err), status.HTTP_500_INTERNAL_SERVER_ERROR) + else: + return Response("No resources available for this resourcecategory ", status.HTTP_404_NOT_FOUND) + + +class TopicSuggestionViewSet(viewsets.ModelViewSet): + """API endpoint that allows TopicSuggestions to be viewed""" + + model = TopicSuggestion + queryset = model.objects.all() + serializer_class = TopicSuggestionSerializer + filterset_class = TopicSuggestionFilterSet From 6f01a47fe901dbbdcb8bb77ec1d9614dde00587b Mon Sep 17 00:00:00 2001 From: David Slusser Date: Sat, 23 Aug 2025 15:25:41 -0700 Subject: [PATCH 2/5] adding unittests for apis --- pyproject.toml | 18 +- src/django_project/core/settings.py | 9 +- .../tests/unit/web/test_model_crud.py | 1 + .../tests/unit/web/test_viewsets.py | 749 ++++++++++++++++++ src/django_project/web/serializers.py | 4 +- 5 files changed, 769 insertions(+), 12 deletions(-) create mode 100644 src/django_project/tests/unit/web/test_viewsets.py diff --git a/pyproject.toml b/pyproject.toml index b5943eb..008968c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,11 +64,11 @@ show_missing = true branch = true source = ["src/django_project"] omit = [ - "django_project/manage.py", - "django_project/core/asgi.py", - "django_project/core/wsgi.py", - "django_project/*/scripts/*", - "django_project/tests/*", + "src/django_project/manage.py", + "src/django_project/core/asgi.py", + "src/django_project/core/wsgi.py", + "src/django_project/*/scripts/*", + "src/django_project/tests/*", ] @@ -97,7 +97,13 @@ filterwarnings = [ [tool.ruff] line-length = 120 -exclude = ["django_project/manage.py", "django_project/tests", "django_project/*/migrations", "django_project/*/scripts", "django_project/*/local_test"] +exclude = [ + "src/django_project/manage.py", + "src/django_project/tests", + "src/django_project/*/migrations", + "src/django_project/*/scripts", + "src/django_project/*/local_test" + ] [tool.setuptools.packages.find] diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index f7dcfc6..e811957 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -20,10 +20,11 @@ BASE_DIR = Path(__file__).resolve().parent.parent -ENV_PATH = os.environ.get("ENV_PATH", f"{BASE_DIR.parent}/envs/.env.local") +# ENV_PATH = os.environ.get("ENV_PATH", f"{BASE_DIR.parent}/envs/.env.local") +ENV_PATH = os.environ.get("ENV_PATH") # now load the contents of the defined .env file env = environ.Env() -if os.path.exists(ENV_PATH): +if ENV_PATH and os.path.exists(ENV_PATH): print(f"loading ENV vars from {ENV_PATH}") environ.Env.read_env(ENV_PATH) else: @@ -37,7 +38,7 @@ SECRET_KEY = env.str("SECRET_KEY", "default_key-this_is_insecure_and_should_be_changed") # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = env.bool("DEBUG", True) +DEBUG = env.bool("DEBUG", False) DEPLOYMENT_ENV = env.str("DEPLOYMENT_ENV", "local") @@ -274,7 +275,7 @@ else ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.TokenAuthentication", - "rest_framework.authentication.SessionAuthentication", + # "rest_framework.authentication.SessionAuthentication", ) if DEBUG else ("rest_framework.authentication.TokenAuthentication",), diff --git a/src/django_project/tests/unit/web/test_model_crud.py b/src/django_project/tests/unit/web/test_model_crud.py index 9752b36..4e121e7 100644 --- a/src/django_project/tests/unit/web/test_model_crud.py +++ b/src/django_project/tests/unit/web/test_model_crud.py @@ -8,6 +8,7 @@ BASE_DIR = Path(__file__).parents[4] os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +os.environ.setdefault("ENV_PATH", "../envs/.env.test") django.setup() from model_bakery import baker # noqa: E402 diff --git a/src/django_project/tests/unit/web/test_viewsets.py b/src/django_project/tests/unit/web/test_viewsets.py new file mode 100644 index 0000000..3efb183 --- /dev/null +++ b/src/django_project/tests/unit/web/test_viewsets.py @@ -0,0 +1,749 @@ +import os +from pathlib import Path + +import django + +BASE_DIR = Path(__file__).parents[4] +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") +os.environ.setdefault("ENV_PATH", "../envs/.env.test") +django.setup() + +from unittest.mock import patch + +from django.apps import apps +from django.shortcuts import reverse +from model_bakery import baker +from rest_framework import status +from rest_framework.test import APIClient, APITestCase + + +def create_custom_client(group_name): + """create a client user in a specified group and return a client for object for that user""" + user = baker.make("auth.User", username=f"{group_name}_user") + group = baker.make("auth.Group", name=group_name) + user.groups.add(group) + token = baker.make("authtoken.Token", user=user) + client = APIClient() + client.credentials(**dict(HTTP_AUTHORIZATION=f"Token {token.key}")) + return client + + +def create_client(): + """create a client without an attached user""" + client = APIClient() + return client + + +class UserSetupMixin: + def setUp(self): + self.user = baker.make("auth.User", username="tester_basic") + self.token = baker.make("authtoken.Token", user=self.user) + self.client = APIClient() + self.client.credentials(**dict(HTTP_AUTHORIZATION=f"Token {self.token.key}")) + self.unauthorized_client = APIClient() + + +class EventTests(UserSetupMixin, APITestCase): + """test API endpoints provided by the EventViewSet viewset""" + + def setUp(self): + super(EventTests, self).setUp() + self.row = baker.make("web.Event") + + def test_event_list_authorized(self): + """verify that a get request to the event-list endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:event-list") + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + self.assertGreater(len(response.json()["results"]), 0) + + def test_event_list_unauthorized(self): + """verify that a get request to the event-list endpoint for an unauthorized user returns a 401 + and the row content is not found""" + url = reverse("rest:event-list") + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_event_post_authorized(self): + """verify that a post request to the event-list endpoint returns a 200 and the row content is found""" + url = reverse("rest:event-list") + model = apps.get_model("web.Event") + client = create_custom_client("default") + prepare = baker.prepare("web.Event") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.post(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertGreater(model.objects.count(), pre_post_row_count) + + def test_event_post_unauthorized(self): + """verify that a post request to the event-list endpoint returns a 403 and the row content is not found""" + url = reverse("rest:event-list") + model = apps.get_model("web.Event") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.post(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_event_retrieve_authorized(self): + """verify that a get request to the event-detail endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], getattr(self.row, "pk")) + + def test_event_retrieve_unauthorized(self): + """verify that a get request to the event-detail endpoint for an unauthorized user returns a 401 and + the row content is not found""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_event_destroy_authorized(self): + """verify that a delete request to the event-detail endpoint for an authorized user returns a 204 and the + record is deleted""" + model = apps.get_model("web.Event") + url = reverse("rest:event-detail", args=[getattr(self.row, "id")]) + client = create_custom_client("default") + response = client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertNotIn(self.row, model.objects.all()) + + def test_event_destroy_unauthorized(self): + """verify that a delete request to the event-detail endpoint for an unauthorized user returns a 401 and the + record is not deleted""" + model = apps.get_model("web.Event") + url = reverse("rest:event-detail", args=[getattr(self.row, "id")]) + response = self.unauthorized_client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(getattr(self.row, "id"), response.json()) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") + self.assertIn(self.row, model.objects.all()) + + def test_event_patch_authorized(self): + """verify that a patch request to the event-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Event") + client = create_custom_client("default") + prepare = baker.prepare("web.Event", pk=self.row.pk) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.patch(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_event_patch_unauthorized(self): + """verify that a patch request to the event-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Event") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.patch(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_event_put_authorized(self): + """verify that a put request to the event-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Event") + client = create_custom_client("default") + prepare = baker.prepare("web.Event") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.put(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_event_put_unauthorized(self): + """verify that a put request to the event-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:event-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Event") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.put(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + +class PresentationRequestTests(UserSetupMixin, APITestCase): + """test API endpoints provided by the PresentationRequestViewSet viewset""" + + def setUp(self): + super(PresentationRequestTests, self).setUp() + self.row = baker.make("web.PresentationRequest") + + def test_presentationrequest_list_authorized(self): + """verify that a get request to the presentationrequest-list endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:presentationrequest-list") + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + self.assertGreater(len(response.json()["results"]), 0) + + def test_presentationrequest_list_unauthorized(self): + """verify that a get request to the presentationrequest-list endpoint for an unauthorized user returns a 401 + and the row content is not found""" + url = reverse("rest:presentationrequest-list") + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_presentationrequest_post_authorized(self): + """verify that a post request to the presentationrequest-list endpoint returns a 200 and the row content is found""" + url = reverse("rest:presentationrequest-list") + model = apps.get_model("web.PresentationRequest") + client = create_custom_client("default") + prepare = baker.prepare("web.PresentationRequest") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.post(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertGreater(model.objects.count(), pre_post_row_count) + + def test_presentationrequest_post_unauthorized(self): + """verify that a post request to the presentationrequest-list endpoint returns a 403 and the row content is not found""" + url = reverse("rest:presentationrequest-list") + model = apps.get_model("web.PresentationRequest") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.post(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_presentationrequest_retrieve_authorized(self): + """verify that a get request to the presentationrequest-detail endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], getattr(self.row, "pk")) + + def test_presentationrequest_retrieve_unauthorized(self): + """verify that a get request to the presentationrequest-detail endpoint for an unauthorized user returns a 401 and + the row content is not found""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_presentationrequest_destroy_authorized(self): + """verify that a delete request to the presentationrequest-detail endpoint for an authorized user returns a 204 and the + record is deleted""" + model = apps.get_model("web.PresentationRequest") + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "id")]) + client = create_custom_client("default") + response = client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertNotIn(self.row, model.objects.all()) + + def test_presentationrequest_destroy_unauthorized(self): + """verify that a delete request to the presentationrequest-detail endpoint for an unauthorized user returns a 401 and the + record is not deleted""" + model = apps.get_model("web.PresentationRequest") + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "id")]) + response = self.unauthorized_client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(getattr(self.row, "id"), response.json()) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") + self.assertIn(self.row, model.objects.all()) + + def test_presentationrequest_patch_authorized(self): + """verify that a patch request to the presentationrequest-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.PresentationRequest") + client = create_custom_client("default") + prepare = baker.prepare("web.PresentationRequest", pk=self.row.pk) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.patch(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_presentationrequest_patch_unauthorized(self): + """verify that a patch request to the presentationrequest-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.PresentationRequest") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.patch(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_presentationrequest_put_authorized(self): + """verify that a put request to the presentationrequest-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.PresentationRequest") + client = create_custom_client("default") + prepare = baker.prepare("web.PresentationRequest") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.put(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_presentationrequest_put_unauthorized(self): + """verify that a put request to the presentationrequest-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:presentationrequest-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.PresentationRequest") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.put(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + +class ResourceTests(UserSetupMixin, APITestCase): + """test API endpoints provided by the ResourceViewSet viewset""" + + def setUp(self): + super(ResourceTests, self).setUp() + self.row = baker.make("web.Resource") + + def test_resource_list_authorized(self): + """verify that a get request to the resource-list endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:resource-list") + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + self.assertGreater(len(response.json()["results"]), 0) + + def test_resource_list_unauthorized(self): + """verify that a get request to the resource-list endpoint for an unauthorized user returns a 401 + and the row content is not found""" + url = reverse("rest:resource-list") + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_resource_post_authorized(self): + """verify that a post request to the resource-list endpoint returns a 200 and the row content is found""" + url = reverse("rest:resource-list") + model = apps.get_model("web.Resource") + client = create_custom_client("default") + category = baker.make("web.ResourceCategory") + prepare = baker.prepare("web.Resource", category=category, _fill_optional=True) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_")} + data["category"] = category.pk + pre_post_row_count = model.objects.count() + response = client.post(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertGreater(model.objects.count(), pre_post_row_count) + + def test_resource_post_unauthorized(self): + """verify that a post request to the resource-list endpoint returns a 403 and the row content is not found""" + url = reverse("rest:resource-list") + model = apps.get_model("web.Resource") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.post(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resource_retrieve_authorized(self): + """verify that a get request to the resource-detail endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], getattr(self.row, "pk")) + + def test_resource_retrieve_unauthorized(self): + """verify that a get request to the resource-detail endpoint for an unauthorized user returns a 401 and + the row content is not found""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_resource_destroy_authorized(self): + """verify that a delete request to the resource-detail endpoint for an authorized user returns a 204 and the + record is deleted""" + model = apps.get_model("web.Resource") + url = reverse("rest:resource-detail", args=[getattr(self.row, "id")]) + client = create_custom_client("default") + response = client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertNotIn(self.row, model.objects.all()) + + def test_resource_destroy_unauthorized(self): + """verify that a delete request to the resource-detail endpoint for an unauthorized user returns a 401 and the + record is not deleted""" + model = apps.get_model("web.Resource") + url = reverse("rest:resource-detail", args=[getattr(self.row, "id")]) + response = self.unauthorized_client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(getattr(self.row, "id"), response.json()) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") + self.assertIn(self.row, model.objects.all()) + + def test_resource_patch_authorized(self): + """verify that a patch request to the resource-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Resource") + client = create_custom_client("default") + prepare = baker.prepare("web.Resource", pk=self.row.pk) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.patch(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resource_patch_unauthorized(self): + """verify that a patch request to the resource-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Resource") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.patch(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resource_put_authorized(self): + """verify that a put request to the resource-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Resource") + client = create_custom_client("default") + category = baker.make("web.ResourceCategory") + prepare = baker.prepare("web.Resource", category=category) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + data["category"] = category.pk + pre_post_row_count = model.objects.count() + response = client.put(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resource_put_unauthorized(self): + """verify that a put request to the resource-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:resource-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.Resource") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.put(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + +class ResourceCategoryTests(UserSetupMixin, APITestCase): + """test API endpoints provided by the ResourceCategoryViewSet viewset""" + + def setUp(self): + super(ResourceCategoryTests, self).setUp() + self.row = baker.make("web.ResourceCategory") + + def test_resourcecategory_list_authorized(self): + """verify that a get request to the resourcecategory-list endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:resourcecategory-list") + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + self.assertGreater(len(response.json()["results"]), 0) + + def test_resourcecategory_list_unauthorized(self): + """verify that a get request to the resourcecategory-list endpoint for an unauthorized user returns a 401 + and the row content is not found""" + url = reverse("rest:resourcecategory-list") + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_resourcecategory_post_authorized(self): + """verify that a post request to the resourcecategory-list endpoint returns a 200 and the row content is found""" + url = reverse("rest:resourcecategory-list") + model = apps.get_model("web.ResourceCategory") + client = create_custom_client("default") + prepare = baker.prepare("web.ResourceCategory") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.post(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertGreater(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_post_unauthorized(self): + """verify that a post request to the resourcecategory-list endpoint returns a 403 and the row content is not found""" + url = reverse("rest:resourcecategory-list") + model = apps.get_model("web.ResourceCategory") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.post(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_retrieve_authorized(self): + """verify that a get request to the resourcecategory-detail endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], getattr(self.row, "pk")) + + def test_resourcecategory_retrieve_unauthorized(self): + """verify that a get request to the resourcecategory-detail endpoint for an unauthorized user returns a 401 and + the row content is not found""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_resourcecategory_destroy_authorized(self): + """verify that a delete request to the resourcecategory-detail endpoint for an authorized user returns a 204 and the + record is deleted""" + model = apps.get_model("web.ResourceCategory") + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "id")]) + client = create_custom_client("default") + response = client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertNotIn(self.row, model.objects.all()) + + def test_test_resourcecategory_destroy_unauthorized(self): + """verify that a delete request to the resourcecategory-detail endpoint for an unauthorized user returns a 401 and the + record is not deleted""" + model = apps.get_model("web.ResourceCategory") + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "id")]) + response = self.unauthorized_client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(getattr(self.row, "id"), response.json()) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") + self.assertIn(self.row, model.objects.all()) + + def test_resourcecategory_patch_authorized(self): + """verify that a patch request to the resourcecategory-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.ResourceCategory") + client = create_custom_client("default") + prepare = baker.prepare("web.ResourceCategory", pk=self.row.pk) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.patch(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_patch_unauthorized(self): + """verify that a patch request to the resourcecategory-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.ResourceCategory") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.patch(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_put_authorized(self): + """verify that a put request to the resourcecategory-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.ResourceCategory") + client = create_custom_client("default") + prepare = baker.prepare("web.ResourceCategory") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.put(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_put_unauthorized(self): + """verify that a put request to the resourcecategory-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:resourcecategory-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.ResourceCategory") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.put(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_resourcecategory_resources_authorized(self): + """verify the resourcecategory-resources endpoint returns a 200 and the row content is found""" + resource = baker.make("web.Resource", category=self.row) + url = reverse("rest:resourcecategory-resources", args=[getattr(self.row, "pk")]) + + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertGreater(len(response.json()), 0) + self.assertIn(str(resource), response.content.decode("utf-8")) + + def test_resourcecategory_resources_unauthorized(self): + """verify the resourcecategory-resources endpoint returns a 403 and the row content is not found""" + resource = baker.make("web.Resource", category=self.row) + url = reverse("rest:resourcecategory-resources", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(str(resource), response.content.decode("utf-8")) + + def test_resourcecategory_resources_exception(self): + """verify the resourcecategory-resources endpoint returns a 500""" + from web.serializers import ResourceSerializer + + with patch.object(ResourceSerializer.Meta, "fields", ["blah"]): + baker.make("web.Resource", category=self.row) + url = reverse("rest:resourcecategory-resources", args=[getattr(self.row, "pk")]) + + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + + def test_resourcecategory_resources_no_data(self): + """verify the resourcecategory-resources endpoint returns a 404 if related data is not available""" + url = reverse("rest:resourcecategory-resources", args=[getattr(self.row, "pk")]) + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + +class TopicSuggestionTests(UserSetupMixin, APITestCase): + """test API endpoints provided by the TopicSuggestionViewSet viewset""" + + def setUp(self): + super(TopicSuggestionTests, self).setUp() + self.row = baker.make("web.TopicSuggestion") + + def test_topicsuggestion_list_authorized(self): + """verify that a get request to the topicsuggestion-list endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:topicsuggestion-list") + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + self.assertGreater(len(response.json()["results"]), 0) + + def test_topicsuggestion_list_unauthorized(self): + """verify that a get request to the topicsuggestion-list endpoint for an unauthorized user returns a 401 + and the row content is not found""" + url = reverse("rest:topicsuggestion-list") + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_topicsuggestion_post_authorized(self): + """verify that a post request to the topicsuggestion-list endpoint returns a 200 and the row content is found""" + url = reverse("rest:topicsuggestion-list") + model = apps.get_model("web.TopicSuggestion") + client = create_custom_client("default") + prepare = baker.prepare("web.TopicSuggestion") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.post(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertGreater(model.objects.count(), pre_post_row_count) + + def test_topicsuggestion_post_unauthorized(self): + """verify that a post request to the topicsuggestion-list endpoint returns a 403 and the row content is not found""" + url = reverse("rest:topicsuggestion-list") + model = apps.get_model("web.TopicSuggestion") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.post(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_topicsuggestion_retrieve_authorized(self): + """verify that a get request to the topicsuggestion-detail endpoint for an authorized user returns a 200 and + the row content is found""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + client = create_custom_client("default") + response = client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["id"], getattr(self.row, "pk")) + + def test_topicsuggestion_retrieve_unauthorized(self): + """verify that a get request to the topicsuggestion-detail endpoint for an unauthorized user returns a 401 and + the row content is not found""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + response = self.unauthorized_client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn("results", response.json()) + self.assertNotIn(str(getattr(self.row, "pk")), response.content.decode("utf-8")) + + def test_topicsuggestion_destroy_authorized(self): + """verify that a delete request to the topicsuggestion-detail endpoint for an authorized user returns a 204 and the + record is deleted""" + model = apps.get_model("web.TopicSuggestion") + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "id")]) + client = create_custom_client("default") + response = client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertNotIn(self.row, model.objects.all()) + + def test_test_topicsuggestion_destroy_unauthorized(self): + """verify that a delete request to the topicsuggestion-detail endpoint for an unauthorized user returns a 401 and the + record is not deleted""" + model = apps.get_model("web.TopicSuggestion") + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "id")]) + response = self.unauthorized_client.delete(url, pk=self.row.pk, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertNotIn(getattr(self.row, "id"), response.json()) + self.assertEqual(response.json()["detail"], "Authentication credentials were not provided.") + self.assertIn(self.row, model.objects.all()) + + def test_topicsuggestion_patch_authorized(self): + """verify that a patch request to the topicsuggestion-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.TopicSuggestion") + client = create_custom_client("default") + prepare = baker.prepare("web.TopicSuggestion", pk=self.row.pk) + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.patch(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_topicsuggestion_patch_unauthorized(self): + """verify that a patch request to the topicsuggestion-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.TopicSuggestion") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.patch(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_topicsuggestion_put_authorized(self): + """verify that a put request to the topicsuggestion-detail endpoint for an authorized user returns a 200 and + the row content is updated""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.TopicSuggestion") + client = create_custom_client("default") + prepare = baker.prepare("web.TopicSuggestion") + data = {k: v for k, v in prepare.__dict__.items() if not k.startswith("_") and v} + pre_post_row_count = model.objects.count() + response = client.put(url, data=data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(model.objects.count(), pre_post_row_count) + + def test_topicsuggestion_put_unauthorized(self): + """verify that a put request to the topicsuggestion-detail endpoint for an unauthorized user returns a 401 and + the row content is not updated""" + url = reverse("rest:topicsuggestion-detail", args=[getattr(self.row, "pk")]) + model = apps.get_model("web.TopicSuggestion") + pre_post_row_count = model.objects.count() + response = self.unauthorized_client.put(url, data={}, format="json") + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(model.objects.count(), pre_post_row_count) diff --git a/src/django_project/web/serializers.py b/src/django_project/web/serializers.py index eae2af3..300685b 100644 --- a/src/django_project/web/serializers.py +++ b/src/django_project/web/serializers.py @@ -1,5 +1,5 @@ +from handyhelpers.serializers import FkReadWriteField from rest_flex_fields import FlexFieldsModelSerializer -from rest_framework import serializers # import models from web.models import ( @@ -53,7 +53,7 @@ class Meta: class ResourceSerializer(FlexFieldsModelSerializer): """serializer class for Resource""" - category = serializers.StringRelatedField() + category = FkReadWriteField(queryset=ResourceCategory.objects.all()) class Meta: """Metaclass to define filterset model and fields""" From b308cc12fb09e49f4d094d91b31028de47ce10d3 Mon Sep 17 00:00:00 2001 From: David Slusser Date: Sat, 23 Aug 2025 15:26:19 -0700 Subject: [PATCH 3/5] adding unittests for apis --- src/django_project/core/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index e811957..ee1b8cd 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -275,7 +275,7 @@ else ("rest_framework.permissions.IsAuthenticated",), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.TokenAuthentication", - # "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.SessionAuthentication", ) if DEBUG else ("rest_framework.authentication.TokenAuthentication",), From 4459b8d5b72961b6e9e0eff6d9635b5d4ea24ed4 Mon Sep 17 00:00:00 2001 From: David Slusser Date: Sat, 23 Aug 2025 15:28:48 -0700 Subject: [PATCH 4/5] cleanup settings --- src/django_project/core/settings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/django_project/core/settings.py b/src/django_project/core/settings.py index ee1b8cd..9fef477 100644 --- a/src/django_project/core/settings.py +++ b/src/django_project/core/settings.py @@ -55,7 +55,6 @@ "django.contrib.messages", "django.contrib.staticfiles", # third party apps - "djangoaddicts.codegen", "django_extensions", "django_filters", "drf_spectacular", From 0e591c0a49a2e7165925a36237f333ec2abddcac Mon Sep 17 00:00:00 2001 From: David Slusser Date: Sat, 23 Aug 2025 15:46:05 -0700 Subject: [PATCH 5/5] moving whitenoise to main dependencies --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 008968c..9dde061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "drf-flex-fields", "drf-spectacular", "drf-renderer-xlsx", + "whitenoise", ] description = "Spokane Python Community" dynamic = ["version"] @@ -44,7 +45,6 @@ dev = [ docker = [ "gunicorn", "psycopg2-binary", - "whitenoise", ]