Skip to content

Commit 9a64344

Browse files
committed
Add FAQ app
- Includes API and tests for the API - TODO: add UI for FAQ
1 parent c53b4b7 commit 9a64344

File tree

13 files changed

+279
-1
lines changed

13 files changed

+279
-1
lines changed

config/settings/base.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,9 @@
4141
"django.contrib.staticfiles",
4242
"rest_framework",
4343
"nerd_herder",
44-
"nerd_herder.code_of_conduct",
4544
"nerd_herder.users",
45+
"nerd_herder.code_of_conduct",
46+
"nerd_herder.faq",
4647
"nerd_herder.talk_proposals",
4748
"nerd_herder.slack",
4849
# "nerd_herder.speakers",

config/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@
2828
include("nerd_herder.talk_proposals.urls", namespace="talk_proposals"),
2929
),
3030
path("api/v1/slack/", include("nerd_herder.slack.urls", namespace="slack")),
31+
path("api/v1/faq/", include("nerd_herder.faq.urls", namespace="faq")),
3132
]

nerd_herder/faq/__init__.py

Whitespace-only changes.

nerd_herder/faq/admin.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

nerd_herder/faq/apps.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class FaqConfig(AppConfig):
5+
name = 'nerd_herder.faq'
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 2.0.1 on 2018-07-07 20:27
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
initial = True
9+
10+
dependencies = [
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='Entry',
16+
fields=[
17+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('created', models.DateTimeField(auto_now_add=True)),
19+
('updated', models.DateTimeField(auto_now=True)),
20+
('position', models.IntegerField()),
21+
('question', models.TextField()),
22+
('answer', models.TextField()),
23+
],
24+
options={
25+
'abstract': False,
26+
},
27+
),
28+
]

nerd_herder/faq/migrations/__init__.py

Whitespace-only changes.

nerd_herder/faq/models.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.db import models
2+
3+
from nerd_herder.models import TimeStampedModel
4+
5+
6+
class Entry(TimeStampedModel):
7+
position = models.IntegerField()
8+
question = models.TextField()
9+
answer = models.TextField()

nerd_herder/faq/serializers.py

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from rest_framework.serializers import ModelSerializer
2+
3+
from .models import Entry
4+
5+
6+
class EntrySerializer(ModelSerializer):
7+
class Meta:
8+
model = Entry
9+
fields = ('id', 'position', 'question', 'answer')

nerd_herder/faq/tests/__init__.py

Whitespace-only changes.

nerd_herder/faq/tests/test_faq_api.py

+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import json
2+
3+
import pytest
4+
from django.test import Client
5+
6+
from nerd_herder.faq.models import Entry
7+
8+
BASE_URL = "/api/v1/faq/"
9+
REQUIRED_ERROR = ["This field is required."]
10+
NULL_ERROR = ["This field may not be null."]
11+
FIELDS = ["question", "answer", "position"]
12+
ENTRIES = [
13+
{
14+
"question": "What version of Python should I use?",
15+
"answer": "Python 3 or above",
16+
"position": 0,
17+
},
18+
{
19+
"question": "Flask or Django?",
20+
"answer": "Whatever floats your boat",
21+
"position": 1,
22+
},
23+
]
24+
25+
26+
@pytest.mark.django_db
27+
def test_add_entry(client: Client):
28+
entry = ENTRIES[0]
29+
payload = json.dumps(entry)
30+
r = client.post(BASE_URL, payload, content_type="application/json")
31+
32+
assert 201 == r.status_code
33+
body = r.json()
34+
assert entry["question"] == body["question"]
35+
assert entry["answer"] == body["answer"]
36+
assert entry["position"] == body["position"]
37+
assert 1 == len(Entry.objects.all())
38+
39+
40+
@pytest.mark.django_db
41+
@pytest.mark.parametrize(
42+
"payload,expected",
43+
[
44+
({}, {field: REQUIRED_ERROR for field in FIELDS}),
45+
({field: None for field in FIELDS}, {field: NULL_ERROR for field in FIELDS}),
46+
({**ENTRIES[0], "question": False}, {"question": ["Not a valid string."]}),
47+
({**ENTRIES[0], "answer": False}, {"answer": ["Not a valid string."]}),
48+
(
49+
{**ENTRIES[0], "position": False},
50+
{"position": ["A valid integer is required."]},
51+
),
52+
],
53+
)
54+
def test_add_entry_bad(client: Client, payload, expected):
55+
req = client.post(BASE_URL, json.dumps(payload), "application/json")
56+
57+
assert 400 == req.status_code
58+
assert expected == req.json()
59+
60+
61+
@pytest.mark.django_db
62+
def test_update_entry(client: Client):
63+
entry = Entry.objects.create(**ENTRIES[0])
64+
question = "I am a new title"
65+
url = f"{BASE_URL}{entry.id}"
66+
r = client.put(
67+
url,
68+
json.dumps({**ENTRIES[0], "question": question}),
69+
content_type="application/json",
70+
)
71+
assert 200 == r.status_code
72+
assert {**ENTRIES[0], "id": entry.id, "question": question} == r.json()
73+
74+
75+
@pytest.mark.django_db
76+
@pytest.mark.parametrize("field", FIELDS)
77+
def test_update_entry_bad(client: Client, field):
78+
entry = Entry.objects.create(**ENTRIES[0])
79+
url = f"{BASE_URL}{entry.id}"
80+
81+
r = client.put(
82+
url, json.dumps({**ENTRIES[0], field: None}), content_type="application/json"
83+
)
84+
85+
assert 400 == r.status_code
86+
87+
copy = {**ENTRIES[0]}
88+
copy.pop(field)
89+
r = client.put(url, json.dumps(copy), content_type="application/json")
90+
91+
assert 400 == r.status_code
92+
93+
r = client.put(
94+
url, json.dumps({**ENTRIES[0], field: False}), content_type="application/json"
95+
)
96+
97+
assert 400 == r.status_code
98+
99+
100+
@pytest.mark.django_db
101+
def test_delete_entry(client: Client):
102+
entry = Entry.objects.create(**ENTRIES[0])
103+
url = f"{BASE_URL}{entry.id}"
104+
r = client.delete(url)
105+
106+
assert 204 == r.status_code
107+
108+
r = client.get(url)
109+
110+
assert 404 == r.status_code
111+
112+
113+
@pytest.mark.django_db
114+
def test_get_entries(client: Client):
115+
for entry in ENTRIES:
116+
Entry.objects.create(**entry)
117+
118+
r = client.get(BASE_URL, content_type="application/json")
119+
120+
assert r.status_code == 200
121+
body = r.json()
122+
assert 2 == len(body)
123+
124+
for idx, entry in enumerate(body):
125+
assert ENTRIES[idx]["question"] == entry["question"]
126+
assert ENTRIES[idx]["answer"] == entry["answer"]
127+
assert ENTRIES[idx]["position"] == entry["position"]
128+
129+
130+
@pytest.mark.django_db
131+
def test_change_entry_order(client: Client):
132+
entries = []
133+
134+
for entry in ENTRIES:
135+
entries.append(Entry.objects.create(**entry))
136+
137+
payload = {
138+
'order': {
139+
str(entries[0].id): 1,
140+
str(entries[1].id): 0,
141+
}
142+
}
143+
144+
r = client.put(BASE_URL, json.dumps(payload), content_type='application/json')
145+
146+
assert 200 == r.status_code
147+
assert payload == r.json()
148+
149+
r = client.get(BASE_URL)
150+
151+
assert 200 == r.status_code
152+
body = r.json()
153+
assert entries[0].id == body[1]['id']
154+
assert entries[1].id == body[0]['id']
155+
156+
157+
@pytest.mark.django_db
158+
def test_change_entry_order_bad(client: Client):
159+
entries = []
160+
161+
for entry in ENTRIES:
162+
entries.append(Entry.objects.create(**entry))
163+
164+
payload = {
165+
'order': {
166+
str(entries[0].id): 1,
167+
}
168+
}
169+
170+
r = client.put(BASE_URL, json.dumps(payload), content_type='application/json')
171+
172+
assert 400 == r.status_code
173+
assert {'order': [f'Missing position value for entry with id "{entries[1].id}"']} == r.json()
174+

nerd_herder/faq/urls.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from django.urls import path
2+
3+
from .views import EntriesList, EntryDetail
4+
5+
app_name = 'faq'
6+
7+
urlpatterns = [
8+
path('', EntriesList.as_view()),
9+
path('<int:pk>', EntryDetail.as_view()),
10+
]

nerd_herder/faq/views.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import logging
2+
3+
from rest_framework import status
4+
from rest_framework.generics import ListCreateAPIView, RetrieveUpdateDestroyAPIView
5+
from rest_framework.response import Response
6+
7+
from .models import Entry
8+
from .serializers import EntrySerializer
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class EntriesList(ListCreateAPIView):
14+
queryset = Entry.objects.all().order_by('position')
15+
serializer_class = EntrySerializer
16+
17+
def put(self, request):
18+
try:
19+
new_order = request.data['order']
20+
except KeyError:
21+
return Response({'order': ['This field is required.']},
22+
status=status.HTTP_400_BAD_REQUEST)
23+
24+
for entry in Entry.objects.all():
25+
try:
26+
entry.position = new_order[str(entry.id)]
27+
except KeyError:
28+
error = f'Missing position value for entry with id "{entry.id}"'
29+
return Response({'order': [error]}, status=status.HTTP_400_BAD_REQUEST)
30+
31+
entry.save()
32+
33+
return Response({'order': new_order}, status=status.HTTP_200_OK)
34+
35+
36+
class EntryDetail(RetrieveUpdateDestroyAPIView):
37+
queryset = Entry.objects.all()
38+
serializer_class = EntrySerializer

0 commit comments

Comments
 (0)