diff --git a/app/content/urls.py b/app/content/urls.py index 536f8fb71..ef01a5330 100644 --- a/app/content/urls.py +++ b/app/content/urls.py @@ -21,6 +21,7 @@ register_with_feide, send_email, ) +from app.files.views.upload import delete, upload router = routers.DefaultRouter() @@ -51,6 +52,8 @@ urlpatterns = [ re_path(r"", include(router.urls)), path("accept-form/", accept_form), + path("upload/", upload), + path("delete-file///", delete), path("send-email/", send_email), path("feide/", register_with_feide), re_path(r"users/(?P[^/.]+)/events.ics", UserCalendarEvents()), diff --git a/app/feedback/factories/bug_factory.py b/app/feedback/factories/bug_factory.py index 4ff95a210..3b8c61af7 100644 --- a/app/feedback/factories/bug_factory.py +++ b/app/feedback/factories/bug_factory.py @@ -11,3 +11,7 @@ class Meta: title = factory.Sequence(lambda n: f"Bug{n}") author = factory.SubFactory(UserFactory) + description = factory.Faker("text") + url = factory.Faker("url") + platform = factory.Faker("word") + browser = factory.Faker("word") diff --git a/app/feedback/factories/idea_factory.py b/app/feedback/factories/idea_factory.py index c923c4710..463f92c33 100644 --- a/app/feedback/factories/idea_factory.py +++ b/app/feedback/factories/idea_factory.py @@ -11,3 +11,4 @@ class Meta: title = factory.Sequence(lambda n: f"Idea{n}") author = factory.SubFactory(UserFactory) + description = factory.Faker("text") diff --git a/app/feedback/filters/feedback.py b/app/feedback/filters/feedback.py new file mode 100644 index 000000000..9df76817e --- /dev/null +++ b/app/feedback/filters/feedback.py @@ -0,0 +1,46 @@ +from django.db.models import Q +from django_filters import rest_framework as filters +from django_filters.rest_framework import OrderingFilter + +from app.feedback.models import Feedback +from app.feedback.models.bug import Bug +from app.feedback.models.idea import Idea + + +class FeedbackFilter(filters.FilterSet): + feedback_type = filters.CharFilter( + method="filter_feedback_type", label="List of feedback type" + ) + + status = filters.CharFilter( + method="filter_status", + label="List of feedback status", + ) + + ordering = OrderingFilter( + fields=( + "created_at", + "updated_at", + ) + ) + + def filter_feedback_type(self, queryset, _name, feedback_type): + if feedback_type == "Idea": + return queryset.filter(Q(instance_of=Idea)) + elif feedback_type == "Bug": + return queryset.filter(Q(instance_of=Bug)) + else: + return queryset + + def filter_status(self, queryset, _name, value): + return queryset.filter(status=value) + + class Meta: + model = Feedback + fields = [ + "title", + "author", + "status", + "created_at", + "updated_at", + ] diff --git a/app/feedback/migrations/0002_alter_feedback_options_bug_browser_bug_platform_and_more.py b/app/feedback/migrations/0002_alter_feedback_options_bug_browser_bug_platform_and_more.py new file mode 100644 index 000000000..1b4150808 --- /dev/null +++ b/app/feedback/migrations/0002_alter_feedback_options_bug_browser_bug_platform_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.5 on 2024-10-21 18:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("feedback", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="feedback", + options={"ordering": ("-created_at",)}, + ), + migrations.AddField( + model_name="bug", + name="Browser", + field=models.CharField(default="", max_length=200), + ), + migrations.AddField( + model_name="bug", + name="Platform", + field=models.CharField(default="", max_length=200), + ), + migrations.AddField( + model_name="bug", + name="Url", + field=models.URLField(blank=True, null=True), + ), + ] diff --git a/app/feedback/migrations/0003_rename_browser_bug_browser_and_more.py b/app/feedback/migrations/0003_rename_browser_bug_browser_and_more.py new file mode 100644 index 000000000..9ffbc39e7 --- /dev/null +++ b/app/feedback/migrations/0003_rename_browser_bug_browser_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.5 on 2024-10-21 22:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("feedback", "0002_alter_feedback_options_bug_browser_bug_platform_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="bug", + old_name="Browser", + new_name="browser", + ), + migrations.RenameField( + model_name="bug", + old_name="Platform", + new_name="platform", + ), + migrations.RenameField( + model_name="bug", + old_name="Url", + new_name="url", + ), + ] diff --git a/app/feedback/models/bug.py b/app/feedback/models/bug.py index 39962659d..f979d5f29 100644 --- a/app/feedback/models/bug.py +++ b/app/feedback/models/bug.py @@ -1,5 +1,9 @@ +from django.db import models + from app.feedback.models.feedback import Feedback class Bug(Feedback): - pass + url = models.URLField(max_length=200, blank=True, null=True) + browser = models.CharField(max_length=200, default="") + platform = models.CharField(max_length=200, default="") diff --git a/app/feedback/models/feedback.py b/app/feedback/models/feedback.py index 13d698db2..528eb28f9 100644 --- a/app/feedback/models/feedback.py +++ b/app/feedback/models/feedback.py @@ -26,7 +26,7 @@ def __str__(self): return f"{self.title} - {self.status}" class Meta: - ordering = ("created_at",) + ordering = ("-created_at",) @classmethod def has_read_permission(cls, request): diff --git a/app/feedback/serializers/__init__.py b/app/feedback/serializers/__init__.py index 25f74a27d..d788cd869 100644 --- a/app/feedback/serializers/__init__.py +++ b/app/feedback/serializers/__init__.py @@ -1,3 +1,11 @@ -from app.feedback.serializers.bug import BugListSerializer -from app.feedback.serializers.idea import IdeaListSerializer +from app.feedback.serializers.bug import ( + BugDetailSerializer, + BugCreateSerializer, + BugUpdateSerializer, +) +from app.feedback.serializers.idea import ( + IdeaDetailSerializer, + IdeaCreateSerializer, + IdeaUpdateSerializer, +) from app.feedback.serializers.feedback import FeedbackListPolymorphicSerializer diff --git a/app/feedback/serializers/bug.py b/app/feedback/serializers/bug.py index 068c52fc6..60d85c2dc 100644 --- a/app/feedback/serializers/bug.py +++ b/app/feedback/serializers/bug.py @@ -3,7 +3,7 @@ from app.feedback.models.bug import Bug -class BugListSerializer(BaseModelSerializer): +class BugSerializer(BaseModelSerializer): author = SimpleUserSerializer(read_only=True) class Meta: @@ -16,3 +16,49 @@ class Meta: "author", "description", ) + + +class BugCreateSerializer(BaseModelSerializer): + class Meta: + model = Bug + fields = ( + "title", + "description", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["author"] = user + + return super().create(validated_data) + + +class BugUpdateSerializer(BaseModelSerializer): + class Meta: + model = Bug + fields = ( + "title", + "description", + "status", + ) + + def update(self, instance, validated_data): + return super().update(instance, validated_data) + + +class BugDetailSerializer(BaseModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Bug + fields = ( + "id", + "title", + "description", + "status", + "created_at", + "author", + "url", + "platform", + "browser", + ) diff --git a/app/feedback/serializers/feedback.py b/app/feedback/serializers/feedback.py index ae1842645..c0228e210 100644 --- a/app/feedback/serializers/feedback.py +++ b/app/feedback/serializers/feedback.py @@ -2,15 +2,15 @@ from app.common.serializers import BaseModelSerializer from app.feedback.models import Bug, Feedback, Idea -from app.feedback.serializers import BugListSerializer, IdeaListSerializer +from app.feedback.serializers import BugDetailSerializer, IdeaDetailSerializer class FeedbackListPolymorphicSerializer(PolymorphicSerializer, BaseModelSerializer): resource_type_field_name = "feedback_type" model_serializer_mapping = { - Bug: BugListSerializer, - Idea: IdeaListSerializer, + Bug: BugDetailSerializer, + Idea: IdeaDetailSerializer, } class Meta: @@ -23,59 +23,3 @@ class Meta: "author", "description", ) - - -class IdeaCreateSerializer(BaseModelSerializer): - class Meta: - model = Idea - fields = ( - "title", - "description", - ) - - def create(self, validated_data): - user = self.context["request"].user - validated_data["author"] = user - - return super().create(validated_data) - - -class BugCreateSerializer(BaseModelSerializer): - class Meta: - model = Bug - fields = ( - "title", - "description", - ) - - def create(self, validated_data): - user = self.context["request"].user - validated_data["author"] = user - - return super().create(validated_data) - - -class IdeaUpdateSerializer(BaseModelSerializer): - class Meta: - model = Feedback - fields = ( - "title", - "description", - "status", - ) - - def update(self, instance, validated_data): - return super().update(instance, validated_data) - - -class BugUpdateSerializer(BaseModelSerializer): - class Meta: - model = Feedback - fields = ( - "title", - "description", - "status", - ) - - def update(self, instance, validated_data): - return super().update(instance, validated_data) diff --git a/app/feedback/serializers/idea.py b/app/feedback/serializers/idea.py index d4add474e..6fe8b9aef 100644 --- a/app/feedback/serializers/idea.py +++ b/app/feedback/serializers/idea.py @@ -3,7 +3,7 @@ from app.feedback.models.idea import Idea -class IdeaListSerializer(BaseModelSerializer): +class IdeaSerializer(BaseModelSerializer): author = SimpleUserSerializer(read_only=True) class Meta: @@ -16,3 +16,46 @@ class Meta: "author", "description", ) + + +class IdeaCreateSerializer(BaseModelSerializer): + class Meta: + model = Idea + fields = ( + "title", + "description", + ) + + def create(self, validated_data): + user = self.context["request"].user + validated_data["author"] = user + + return super().create(validated_data) + + +class IdeaUpdateSerializer(BaseModelSerializer): + class Meta: + model = Idea + fields = ( + "title", + "description", + "status", + ) + + def update(self, instance, validated_data): + return super().update(instance, validated_data) + + +class IdeaDetailSerializer(BaseModelSerializer): + author = SimpleUserSerializer(read_only=True) + + class Meta: + model = Idea + fields = ( + "id", + "title", + "description", + "status", + "created_at", + "author", + ) diff --git a/app/feedback/views/feedback.py b/app/feedback/views/feedback.py index 0ccbc653c..6501059ee 100644 --- a/app/feedback/views/feedback.py +++ b/app/feedback/views/feedback.py @@ -1,11 +1,13 @@ -from rest_framework import status +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import filters, status from rest_framework.response import Response from app.common.pagination import BasePagination from app.common.permissions import BasicViewPermission from app.common.viewsets import BaseViewSet +from app.feedback.filters.feedback import FeedbackFilter from app.feedback.models.feedback import Feedback -from app.feedback.serializers.feedback import ( +from app.feedback.serializers import ( BugCreateSerializer, BugUpdateSerializer, FeedbackListPolymorphicSerializer, @@ -20,6 +22,14 @@ class FeedbackViewSet(BaseViewSet): pagination_class = BasePagination permission_classes = [BasicViewPermission] + filter_backends = [DjangoFilterBackend, filters.SearchFilter] + filterset_class = FeedbackFilter + search_fields = [ + "title", + "author__first_name", + "author__last_name", + ] + def create(self, request, *_args, **_kwargs): data = request.data diff --git a/app/files/urls.py b/app/files/urls.py index 02655adc3..27da1ac43 100644 --- a/app/files/urls.py +++ b/app/files/urls.py @@ -2,7 +2,6 @@ from rest_framework import routers from app.files.views.file import FileViewSet -from app.files.views.upload import delete, upload from app.files.views.user_gallery import UserGalleryViewSet router = routers.DefaultRouter() @@ -11,7 +10,5 @@ router.register("user_gallery", UserGalleryViewSet, basename="user_gallery") urlpatterns = [ - path("upload/", upload), - path("delete-file///", delete), path("files/", include(router.urls)), ] diff --git a/app/feedback/tests/__init__.py b/app/tests/feedback/__init__.py similarity index 100% rename from app/feedback/tests/__init__.py rename to app/tests/feedback/__init__.py diff --git a/app/tests/index/test_feedback_integration.py b/app/tests/feedback/test_feedback_integration.py similarity index 50% rename from app/tests/index/test_feedback_integration.py rename to app/tests/feedback/test_feedback_integration.py index 890c8ad45..fd8686dcb 100644 --- a/app/tests/index/test_feedback_integration.py +++ b/app/tests/feedback/test_feedback_integration.py @@ -2,6 +2,9 @@ import pytest +from app.feedback.enums import Status +from app.feedback.factories.bug_factory import BugFactory +from app.feedback.factories.idea_factory import IdeaFactory from app.util.test_utils import get_api_client FEEDBACK_BASE_URL = "/feedbacks/" @@ -16,11 +19,12 @@ def get_data(type): @pytest.mark.django_db -def test_list_feedback_with_both_bug_and_idea_as_member( - member, feedback_bug, feedback_idea -): +def test_list_feedback_with_both_bug_and_idea_as_member(member): """All members should be able to list all types of feedbacks.""" + BugFactory(author=member) + IdeaFactory(author=member) + url = FEEDBACK_BASE_URL client = get_api_client(member) response = client.get(url) @@ -32,8 +36,8 @@ def test_list_feedback_with_both_bug_and_idea_as_member( assert response.status_code == status.HTTP_200_OK assert data["count"] == 2 - assert bug_type - assert idea_type + assert len(bug_type) == 1 + assert len(idea_type) == 1 @pytest.mark.django_db @@ -147,9 +151,6 @@ def test_destroy_your_own_feedback_as_member(member, type): initial_response = client.post(url, data=data) - print(initial_response.data) - print(f"Author: {initial_response.data['author']}") - feedback_id = initial_response.data["id"] url = f"{FEEDBACK_BASE_URL}{feedback_id}/" @@ -172,3 +173,123 @@ def test_destroy_feedback_as_anonymous_user(default_client, type): response = default_client.delete(url, data=data) assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_retrieve_details_bug_as_member(member): + """A member should be able to retrieve a bug feedback""" + feedback_bug = BugFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_bug.id}/" + client = get_api_client(member) + response = client.get(url) + + assert response.data["feedback_type"] == "Bug" + assert response.data["url"] == feedback_bug.url + assert response.data["browser"] == feedback_bug.browser + assert response.data["platform"] == feedback_bug.platform + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_retrieve_details_idea_as_member(member): + """A member should be able to retrieve an idea feedback""" + feedback_idea = IdeaFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_idea.id}/" + client = get_api_client(member) + response = client.get(url) + + assert response.data["feedback_type"] == "Idea" + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_retrieve_details_bug_as_anonymous_user(default_client, member): + """Non TIHLDE users should not be able to retrieve bug feedbacks""" + feedback_bug = BugFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_bug.id}/" + response = default_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_retrieve_details_idea_as_anonymous_user(default_client, member): + """Non TIHLDE users should not be able to retrieve idea feedbacks""" + feedback_idea = IdeaFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_idea.id}/" + response = default_client.get(url) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_retrieve_details_bug_as_index_user(index_member, member): + """An Index user should be able to retrieve bug feedbacks from members""" + feedback_bug = BugFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_bug.id}/" + client = get_api_client(user=index_member) + response = client.get(url) + + assert response.data["feedback_type"] == "Bug" + assert response.data["url"] == feedback_bug.url + assert response.data["browser"] == feedback_bug.browser + assert response.data["platform"] == feedback_bug.platform + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_retrieve_details_idea_as_index_user(index_member, member): + """An Index user should be able to retrieve idea feedbacks from members""" + feedback_idea = IdeaFactory(author=member) + + url = f"{FEEDBACK_BASE_URL}{feedback_idea.id}/" + client = get_api_client(user=index_member) + response = client.get(url) + + assert response.data["feedback_type"] == "Idea" + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +@pytest.mark.parametrize("feedback_type", ["Bug", "Idea"]) +def test_filter_feedback_type_as_member(member, feedback_type): + """A member should be able to filter feedbacks by type""" + + BugFactory() + IdeaFactory() + + url = f"{FEEDBACK_BASE_URL}?feedback_type={feedback_type}" + client = get_api_client(member) + response = client.get(url) + + data = response.data + results = data["results"] + + assert response.status_code == status.HTTP_200_OK + assert data["count"] == 1 + assert results[0]["feedback_type"] == feedback_type + + +@pytest.mark.django_db +def test_status_filter_as_member(member): + """A member should be able to filter feedbacks by status""" + + BugFactory(author=member, status=Status.OPEN) + IdeaFactory(author=member, status=Status.CLOSED) + + url = f"{FEEDBACK_BASE_URL}?status={Status.OPEN}" + client = get_api_client(member) + response = client.get(url) + + data = response.data + + results = data["results"] + + assert response.status_code == status.HTTP_200_OK + assert data["count"] == 1 + assert results[0]["status"] == Status.OPEN diff --git a/app/tests/index/__init__.py b/app/tests/index/__init__.py deleted file mode 100644 index e69de29bb..000000000