Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,14 @@ For each PR made, an entry should be added to this changelog. It should contain
- etc.

## Changelog
- 960-notifications-add-a-dropdown-with-options-on-the-feedback-form
- Description: Generate an API endpoint and publish all the dropdown options necessary as a list for LRM to consume it.
- Changes:
- Created a new model `FeedbackFormDropdown`
- Added the migration file
- Added the `dropdown_option` field to the `Feedback` model
- Updated the slack notification structure by adding the dropdown option text
- Created a new serializer called `FeedbackFormDropdownSerializer`
- Added a new API endpoint `feedback-form-dropdown-options-api/` where the list is going to be accesible
- Added a list view called `FeedbackFormDropdownListView`
- Added tests
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Generated by Django 4.2.9 on 2025-01-29 19:27

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
("feedback", "0004_contentcurationrequest_created_at_and_more"),
]

operations = [
migrations.CreateModel(
name="FeedbackFormDropdown",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("name", models.CharField(max_length=200)),
("display_order", models.PositiveIntegerField(default=1)),
],
options={
"verbose_name": "Dropdowm Option",
"verbose_name_plural": "Dropdown Options",
"ordering": ["display_order", "name"],
},
),
migrations.AddField(
model_name="feedback",
name="dropdown_option",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="feedback",
to="feedback.feedbackformdropdown",
),
),
]
26 changes: 26 additions & 0 deletions feedback/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,36 @@
from sde_collections.utils.slack_utils import send_slack_message


class FeedbackFormDropdown(models.Model):
DEFAULT_OPTIONS = [
{"name": "I need help or have a general question", "display_order": 1},
{"name": "I have a data/content question or comment", "display_order": 2},
{"name": "I would like to report an error", "display_order": 3},
{"name": "I have an idea or suggested improvement to share", "display_order": 4},
{"name": "General comment or feedback", "display_order": 5},
]

name = models.CharField(max_length=200)
display_order = models.PositiveIntegerField(default=1)

class Meta:
ordering = ["display_order", "name"]
verbose_name = "Dropdowm Option"
verbose_name_plural = "Dropdown Options"

def __str__(self):
return self.name


class Feedback(models.Model):
name = models.CharField(max_length=150)
email = models.EmailField()
subject = models.CharField(max_length=400)
comments = models.TextField()
source = models.CharField(max_length=50, default="SDE", blank=True)
dropdown_option = models.ForeignKey(
FeedbackFormDropdown, on_delete=models.SET_NULL, null=True, related_name="feedback"
)
created_at = models.DateTimeField(null=True, blank=True)

class Meta:
Expand All @@ -32,10 +56,12 @@ def format_notification_message(self):
"""
Returns a formatted notification message containing details from this Feedback instance.
"""
dropdown_option_text = self.dropdown_option.name if self.dropdown_option else "No Option Selected"
notification_message = (
f"<!here> New Feedback Received : \n" # noqa: E203
f"Name: {self.name}\n"
f"Email: {self.email}\n"
f"Dropdown Choice: {dropdown_option_text}\n"
f"Subject: {self.subject}\n"
f"Comments: {self.comments}\n"
f"Source: {self.source}\n"
Expand Down
9 changes: 8 additions & 1 deletion feedback/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from rest_framework import serializers

from .models import ContentCurationRequest, Feedback
from .models import ContentCurationRequest, Feedback, FeedbackFormDropdown


class FeedbackFormDropdownSerializer(serializers.ModelSerializer):
class Meta:
model = FeedbackFormDropdown
fields = ["id", "name"]


class FeedbackSerializer(serializers.ModelSerializer):
Expand All @@ -12,6 +18,7 @@ class Meta:
"subject",
"comments",
"source",
"dropdown_option",
"created_at",
]

Expand Down
146 changes: 146 additions & 0 deletions feedback/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# docker compose -f local.yml run --rm django pytest feedback/tests.py

import pytest
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient

from feedback.models import ContentCurationRequest, Feedback, FeedbackFormDropdown


@pytest.fixture
def api_client():
return APIClient()


@pytest.fixture
def dropdown_option(db):
return FeedbackFormDropdown.objects.create(name="I need help or have a general question", display_order=1)


@pytest.fixture
def feedback_data(dropdown_option):
return {
"name": "Test User",
"email": "test@example.com",
"subject": "Test Subject",
"comments": "Test Comments",
"source": "TEST",
"dropdown_option": dropdown_option.id,
}


@pytest.fixture
def content_curation_data():
return {
"name": "Test User",
"email": "test@example.com",
"scientific_focus": "Biology",
"data_type": "Genomics",
"data_link": "https://example.com/data",
"additional_info": "Extra details",
}


@pytest.mark.django_db
class TestFeedbackFormDropdown:
def test_dropdown_str_representation(self, dropdown_option):
"""Test string representation of dropdown options"""
assert str(dropdown_option) == "I need help or have a general question"

def test_dropdown_ordering(self):
"""Test that dropdown options are ordered by display_order"""
dropdown1 = FeedbackFormDropdown.objects.create(name="First Option", display_order=1)
dropdown2 = FeedbackFormDropdown.objects.create(name="Second Option", display_order=2)
dropdowns = FeedbackFormDropdown.objects.all()
assert dropdowns[0] == dropdown1
assert dropdowns[1] == dropdown2


@pytest.mark.django_db
class TestFeedbackAPI:
def test_get_dropdown_options(self, api_client, dropdown_option):
"""Test retrieving dropdown options"""
url = reverse("feedback:feedback-form-dropdown-options-api")
response = api_client.get(url)
assert response.status_code == status.HTTP_200_OK
assert len(response.data["results"]) == 1
assert response.data["results"][0]["name"] == dropdown_option.name

def test_create_feedback_success(self, api_client, feedback_data):
"""Test successful feedback creation"""
url = reverse("feedback:contact-us-api")
response = api_client.post(url, feedback_data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert Feedback.objects.count() == 1

def test_create_feedback_invalid_email(self, api_client, feedback_data):
"""Test feedback creation with invalid email"""
url = reverse("feedback:contact-us-api")
feedback_data["email"] = "invalid-email"
response = api_client.post(url, feedback_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "email" in response.data["error"]

@pytest.mark.parametrize("field", ["name", "email", "subject", "comments"])
def test_create_feedback_missing_required_fields(self, api_client, feedback_data, field):
"""Test feedback creation with missing required fields"""
url = reverse("feedback:contact-us-api")
feedback_data.pop(field)
response = api_client.post(url, feedback_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert field in response.data["error"]

def test_create_feedback_invalid_dropdown(self, api_client, feedback_data):
"""Test feedback creation with non-existent dropdown option"""
url = reverse("feedback:contact-us-api")
feedback_data["dropdown_option"] = 999
response = api_client.post(url, feedback_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "dropdown_option" in response.data["error"]


@pytest.mark.django_db
class TestContentCurationRequestAPI:
def test_create_request_success(self, api_client, content_curation_data):
"""Test successful content curation request creation"""
url = reverse("feedback:content-curation-request-api")
response = api_client.post(url, content_curation_data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert ContentCurationRequest.objects.count() == 1

def test_create_request_without_additional_info(self, api_client, content_curation_data):
"""Test request creation without optional additional info"""
url = reverse("feedback:content-curation-request-api")
del content_curation_data["additional_info"]
response = api_client.post(url, content_curation_data, format="json")
assert response.status_code == status.HTTP_201_CREATED
assert ContentCurationRequest.objects.first().additional_info == ""

def test_create_request_invalid_email(self, api_client, content_curation_data):
"""Test request creation with invalid email"""
url = reverse("feedback:content-curation-request-api")
content_curation_data["email"] = "invalid-email"
response = api_client.post(url, content_curation_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "email" in response.data["error"]

@pytest.mark.parametrize("field", ["name", "email", "scientific_focus", "data_type", "data_link"])
def test_create_request_missing_required_fields(self, api_client, content_curation_data, field):
"""Test request creation with missing required fields"""
url = reverse("feedback:content-curation-request-api")
content_curation_data.pop(field)
response = api_client.post(url, content_curation_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert field in response.data["error"]

@pytest.mark.parametrize(
"field,length", [("name", 151), ("data_link", 1001), ("scientific_focus", 201), ("data_type", 101)]
)
def test_create_request_field_max_lengths(self, api_client, content_curation_data, field, length):
"""Test request creation with fields exceeding max length"""
url = reverse("feedback:content-curation-request-api")
content_curation_data[field] = "x" * length
response = api_client.post(url, content_curation_data, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert field in response.data["error"]
11 changes: 10 additions & 1 deletion feedback/urls.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
from django.urls import path

from .views import ContactFormModelView, ContentCurationRequestView
from .views import (
ContactFormModelView,
ContentCurationRequestView,
FeedbackFormDropdownListView,
)

app_name = "feedback"
urlpatterns = [
path("contact-us-api/", ContactFormModelView.as_view(), name="contact-us-api"),
path(
"feedback-form-dropdown-options-api/",
FeedbackFormDropdownListView.as_view(),
name="feedback-form-dropdown-options-api",
),
path(
"content-curation-request-api/",
ContentCurationRequestView.as_view(),
Expand Down
13 changes: 11 additions & 2 deletions feedback/views.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
from rest_framework import generics

from .models import ContentCurationRequest, Feedback
from .serializers import ContentCurationRequestSerializer, FeedbackSerializer
from .models import ContentCurationRequest, Feedback, FeedbackFormDropdown
from .serializers import (
ContentCurationRequestSerializer,
FeedbackFormDropdownSerializer,
FeedbackSerializer,
)


class ContactFormModelView(generics.CreateAPIView):
queryset = Feedback.objects.all()
serializer_class = FeedbackSerializer


class FeedbackFormDropdownListView(generics.ListAPIView):
queryset = FeedbackFormDropdown.objects.all()
serializer_class = FeedbackFormDropdownSerializer


class ContentCurationRequestView(generics.CreateAPIView):
queryset = ContentCurationRequest.objects.all()
serializer_class = ContentCurationRequestSerializer