Skip to content

Commit 80fbef3

Browse files
Merge pull request #1210 from NASA-IMPACT/960-notifications-add-a-dropdown-with-options-on-the-feedback-form
960 notifications add a dropdown with options on the feedback form
2 parents 8dd24ba + f477c95 commit 80fbef3

File tree

7 files changed

+249
-4
lines changed

7 files changed

+249
-4
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ For each PR made, an entry should be added to this changelog. It should contain
1212
- etc.
1313

1414
## Changelog
15+
- 960-notifications-add-a-dropdown-with-options-on-the-feedback-form
16+
- Description: Generate an API endpoint and publish all the dropdown options necessary as a list for LRM to consume it.
17+
- Changes:
18+
- Created a new model `FeedbackFormDropdown`
19+
- Added the migration file
20+
- Added the `dropdown_option` field to the `Feedback` model
21+
- Updated the slack notification structure by adding the dropdown option text
22+
- Created a new serializer called `FeedbackFormDropdownSerializer`
23+
- Added a new API endpoint `feedback-form-dropdown-options-api/` where the list is going to be accesible
24+
- Added a list view called `FeedbackFormDropdownListView`
25+
- Added tests
1526

1627
- 1217-add-data-validation-to-the-feedback-form-api-to-restrict-html-content
1728
- Description: The feedback form API does not currently have any form of data validation on the backend which makes it easy for the user with the endpoint to send in data with html tags. We need to have a validation scheme on the backend to protect this from happening.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Generated by Django 4.2.9 on 2025-01-29 19:27
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("feedback", "0004_contentcurationrequest_created_at_and_more"),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="FeedbackFormDropdown",
16+
fields=[
17+
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
18+
("name", models.CharField(max_length=200)),
19+
("display_order", models.PositiveIntegerField(default=1)),
20+
],
21+
options={
22+
"verbose_name": "Dropdowm Option",
23+
"verbose_name_plural": "Dropdown Options",
24+
"ordering": ["display_order", "name"],
25+
},
26+
),
27+
migrations.AddField(
28+
model_name="feedback",
29+
name="dropdown_option",
30+
field=models.ForeignKey(
31+
null=True,
32+
on_delete=django.db.models.deletion.SET_NULL,
33+
related_name="feedback",
34+
to="feedback.feedbackformdropdown",
35+
),
36+
),
37+
]

feedback/models.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,36 @@
44
from sde_collections.utils.slack_utils import send_slack_message
55

66

7+
class FeedbackFormDropdown(models.Model):
8+
DEFAULT_OPTIONS = [
9+
{"name": "I need help or have a general question", "display_order": 1},
10+
{"name": "I have a data/content question or comment", "display_order": 2},
11+
{"name": "I would like to report an error", "display_order": 3},
12+
{"name": "I have an idea or suggested improvement to share", "display_order": 4},
13+
{"name": "General comment or feedback", "display_order": 5},
14+
]
15+
16+
name = models.CharField(max_length=200)
17+
display_order = models.PositiveIntegerField(default=1)
18+
19+
class Meta:
20+
ordering = ["display_order", "name"]
21+
verbose_name = "Dropdowm Option"
22+
verbose_name_plural = "Dropdown Options"
23+
24+
def __str__(self):
25+
return self.name
26+
27+
728
class Feedback(models.Model):
829
name = models.CharField(max_length=150)
930
email = models.EmailField()
1031
subject = models.CharField(max_length=400)
1132
comments = models.TextField()
1233
source = models.CharField(max_length=50, default="SDE", blank=True)
34+
dropdown_option = models.ForeignKey(
35+
FeedbackFormDropdown, on_delete=models.SET_NULL, null=True, related_name="feedback"
36+
)
1337
created_at = models.DateTimeField(null=True, blank=True)
1438

1539
class Meta:
@@ -32,10 +56,12 @@ def format_notification_message(self):
3256
"""
3357
Returns a formatted notification message containing details from this Feedback instance.
3458
"""
59+
dropdown_option_text = self.dropdown_option.name if self.dropdown_option else "No Option Selected"
3560
notification_message = (
3661
f"<!here> New Feedback Received : \n" # noqa: E203
3762
f"Name: {self.name}\n"
3863
f"Email: {self.email}\n"
64+
f"Dropdown Choice: {dropdown_option_text}\n"
3965
f"Subject: {self.subject}\n"
4066
f"Comments: {self.comments}\n"
4167
f"Source: {self.source}\n"

feedback/serializers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22

33
from rest_framework import serializers
44

5-
from .models import ContentCurationRequest, Feedback
5+
from .models import ContentCurationRequest, Feedback, FeedbackFormDropdown
6+
7+
8+
class FeedbackFormDropdownSerializer(serializers.ModelSerializer):
9+
class Meta:
10+
model = FeedbackFormDropdown
11+
fields = ["id", "name"]
612

713

814
class HTMLFreeCharField(serializers.CharField):
@@ -30,6 +36,7 @@ class Meta:
3036
"subject",
3137
"comments",
3238
"source",
39+
"dropdown_option",
3340
"created_at",
3441
]
3542

feedback/tests.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# docker compose -f local.yml run --rm django pytest feedback/tests.py
2+
3+
import pytest
4+
from django.urls import reverse
5+
from rest_framework import status
6+
from rest_framework.test import APIClient
7+
8+
from feedback.models import ContentCurationRequest, Feedback, FeedbackFormDropdown
9+
10+
11+
@pytest.fixture
12+
def api_client():
13+
return APIClient()
14+
15+
16+
@pytest.fixture
17+
def dropdown_option(db):
18+
return FeedbackFormDropdown.objects.create(name="I need help or have a general question", display_order=1)
19+
20+
21+
@pytest.fixture
22+
def feedback_data(dropdown_option):
23+
return {
24+
"name": "Test User",
25+
"email": "[email protected]",
26+
"subject": "Test Subject",
27+
"comments": "Test Comments",
28+
"source": "TEST",
29+
"dropdown_option": dropdown_option.id,
30+
}
31+
32+
33+
@pytest.fixture
34+
def content_curation_data():
35+
return {
36+
"name": "Test User",
37+
"email": "[email protected]",
38+
"scientific_focus": "Biology",
39+
"data_type": "Genomics",
40+
"data_link": "https://example.com/data",
41+
"additional_info": "Extra details",
42+
}
43+
44+
45+
@pytest.mark.django_db
46+
class TestFeedbackFormDropdown:
47+
def test_dropdown_str_representation(self, dropdown_option):
48+
"""Test string representation of dropdown options"""
49+
assert str(dropdown_option) == "I need help or have a general question"
50+
51+
def test_dropdown_ordering(self):
52+
"""Test that dropdown options are ordered by display_order"""
53+
dropdown1 = FeedbackFormDropdown.objects.create(name="First Option", display_order=1)
54+
dropdown2 = FeedbackFormDropdown.objects.create(name="Second Option", display_order=2)
55+
dropdowns = FeedbackFormDropdown.objects.all()
56+
assert dropdowns[0] == dropdown1
57+
assert dropdowns[1] == dropdown2
58+
59+
60+
@pytest.mark.django_db
61+
class TestFeedbackAPI:
62+
def test_get_dropdown_options(self, api_client, dropdown_option):
63+
"""Test retrieving dropdown options"""
64+
url = reverse("feedback:feedback-form-dropdown-options-api")
65+
response = api_client.get(url)
66+
assert response.status_code == status.HTTP_200_OK
67+
assert len(response.data["results"]) == 1
68+
assert response.data["results"][0]["name"] == dropdown_option.name
69+
70+
def test_create_feedback_success(self, api_client, feedback_data):
71+
"""Test successful feedback creation"""
72+
url = reverse("feedback:contact-us-api")
73+
response = api_client.post(url, feedback_data, format="json")
74+
assert response.status_code == status.HTTP_201_CREATED
75+
assert Feedback.objects.count() == 1
76+
77+
def test_create_feedback_invalid_email(self, api_client, feedback_data):
78+
"""Test feedback creation with invalid email"""
79+
url = reverse("feedback:contact-us-api")
80+
feedback_data["email"] = "invalid-email"
81+
response = api_client.post(url, feedback_data, format="json")
82+
assert response.status_code == status.HTTP_400_BAD_REQUEST
83+
assert "email" in response.data["error"]
84+
85+
@pytest.mark.parametrize("field", ["name", "email", "subject", "comments"])
86+
def test_create_feedback_missing_required_fields(self, api_client, feedback_data, field):
87+
"""Test feedback creation with missing required fields"""
88+
url = reverse("feedback:contact-us-api")
89+
feedback_data.pop(field)
90+
response = api_client.post(url, feedback_data, format="json")
91+
assert response.status_code == status.HTTP_400_BAD_REQUEST
92+
assert field in response.data["error"]
93+
94+
def test_create_feedback_invalid_dropdown(self, api_client, feedback_data):
95+
"""Test feedback creation with non-existent dropdown option"""
96+
url = reverse("feedback:contact-us-api")
97+
feedback_data["dropdown_option"] = 999
98+
response = api_client.post(url, feedback_data, format="json")
99+
assert response.status_code == status.HTTP_400_BAD_REQUEST
100+
assert "dropdown_option" in response.data["error"]
101+
102+
103+
@pytest.mark.django_db
104+
class TestContentCurationRequestAPI:
105+
def test_create_request_success(self, api_client, content_curation_data):
106+
"""Test successful content curation request creation"""
107+
url = reverse("feedback:content-curation-request-api")
108+
response = api_client.post(url, content_curation_data, format="json")
109+
assert response.status_code == status.HTTP_201_CREATED
110+
assert ContentCurationRequest.objects.count() == 1
111+
112+
def test_create_request_without_additional_info(self, api_client, content_curation_data):
113+
"""Test request creation without optional additional info"""
114+
url = reverse("feedback:content-curation-request-api")
115+
del content_curation_data["additional_info"]
116+
response = api_client.post(url, content_curation_data, format="json")
117+
assert response.status_code == status.HTTP_201_CREATED
118+
assert ContentCurationRequest.objects.first().additional_info == ""
119+
120+
def test_create_request_invalid_email(self, api_client, content_curation_data):
121+
"""Test request creation with invalid email"""
122+
url = reverse("feedback:content-curation-request-api")
123+
content_curation_data["email"] = "invalid-email"
124+
response = api_client.post(url, content_curation_data, format="json")
125+
assert response.status_code == status.HTTP_400_BAD_REQUEST
126+
assert "email" in response.data["error"]
127+
128+
@pytest.mark.parametrize("field", ["name", "email", "scientific_focus", "data_type", "data_link"])
129+
def test_create_request_missing_required_fields(self, api_client, content_curation_data, field):
130+
"""Test request creation with missing required fields"""
131+
url = reverse("feedback:content-curation-request-api")
132+
content_curation_data.pop(field)
133+
response = api_client.post(url, content_curation_data, format="json")
134+
assert response.status_code == status.HTTP_400_BAD_REQUEST
135+
assert field in response.data["error"]
136+
137+
@pytest.mark.parametrize(
138+
"field,length", [("name", 151), ("data_link", 1001), ("scientific_focus", 201), ("data_type", 101)]
139+
)
140+
def test_create_request_field_max_lengths(self, api_client, content_curation_data, field, length):
141+
"""Test request creation with fields exceeding max length"""
142+
url = reverse("feedback:content-curation-request-api")
143+
content_curation_data[field] = "x" * length
144+
response = api_client.post(url, content_curation_data, format="json")
145+
assert response.status_code == status.HTTP_400_BAD_REQUEST
146+
assert field in response.data["error"]

feedback/urls.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
from django.urls import path
22

3-
from .views import ContactFormModelView, ContentCurationRequestView
3+
from .views import (
4+
ContactFormModelView,
5+
ContentCurationRequestView,
6+
FeedbackFormDropdownListView,
7+
)
48

59
app_name = "feedback"
610
urlpatterns = [
711
path("contact-us-api/", ContactFormModelView.as_view(), name="contact-us-api"),
12+
path(
13+
"feedback-form-dropdown-options-api/",
14+
FeedbackFormDropdownListView.as_view(),
15+
name="feedback-form-dropdown-options-api",
16+
),
817
path(
918
"content-curation-request-api/",
1019
ContentCurationRequestView.as_view(),

feedback/views.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
from rest_framework import generics
22

3-
from .models import ContentCurationRequest, Feedback
4-
from .serializers import ContentCurationRequestSerializer, FeedbackSerializer
3+
from .models import ContentCurationRequest, Feedback, FeedbackFormDropdown
4+
from .serializers import (
5+
ContentCurationRequestSerializer,
6+
FeedbackFormDropdownSerializer,
7+
FeedbackSerializer,
8+
)
59

610

711
class ContactFormModelView(generics.CreateAPIView):
812
queryset = Feedback.objects.all()
913
serializer_class = FeedbackSerializer
1014

1115

16+
class FeedbackFormDropdownListView(generics.ListAPIView):
17+
queryset = FeedbackFormDropdown.objects.all()
18+
serializer_class = FeedbackFormDropdownSerializer
19+
20+
1221
class ContentCurationRequestView(generics.CreateAPIView):
1322
queryset = ContentCurationRequest.objects.all()
1423
serializer_class = ContentCurationRequestSerializer

0 commit comments

Comments
 (0)