Skip to content

Commit c24d3e3

Browse files
Rest API Basics Tutorial
1 parent 88ab402 commit c24d3e3

File tree

10 files changed

+232
-8
lines changed

10 files changed

+232
-8
lines changed

src/cfehome/settings.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,12 @@
125125

126126
REST_FRAMEWORK = {
127127
'DEFAULT_PERMISSION_CLASSES': (
128-
'rest_framework.permissions.IsAuthenticated',
128+
'rest_framework.permissions.IsAuthenticatedOrReadOnly',
129129
),
130130
'DEFAULT_AUTHENTICATION_CLASSES': (
131131
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
132132
'rest_framework.authentication.SessionAuthentication',
133-
'rest_framework.authentication.BasicAuthentication',
133+
#'rest_framework.authentication.BasicAuthentication',
134134
),
135135
}
136136

src/cfehome/urls.py

+3
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
"""
1616
from django.conf.urls import url, include
1717
from django.contrib import admin
18+
from rest_framework_jwt.views import obtain_jwt_token
1819

1920
urlpatterns = [
2021
url(r'^admin/', admin.site.urls),
22+
url(r'^api/auth/login/$', obtain_jwt_token, name='api-login'),
2123
url(r'^api/postings/', include('postings.api.urls', namespace='api-postings')),
2224
]
25+

src/db.sqlite3

0 Bytes
Binary file not shown.

src/postings/api/notes.md

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
- Retrieve Update Delete
33
- Create & List & Search
44

5+
- Permissions???? JWT
6+
57
2. HTTP methods
68
- GET, POST, PUT, PATCH, DELETE
79

src/postings/api/permissions.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from rest_framework import permissions
2+
3+
class IsOwnerOrReadOnly(permissions.BasePermission):
4+
"""
5+
Object-level permission to only allow owners of an object to edit it.
6+
Assumes the model instance has an `owner` attribute.
7+
"""
8+
9+
def has_object_permission(self, request, view, obj):
10+
# Read permissions are allowed to any request,
11+
# so we'll always allow GET, HEAD or OPTIONS requests.
12+
if request.method in permissions.SAFE_METHODS:
13+
return True
14+
15+
# Instance must have an attribute named `owner`.
16+
return obj.owner == request.user

src/postings/api/serializers.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,31 @@
44

55

66
class BlogPostSerializer(serializers.ModelSerializer): # forms.ModelForm
7+
url = serializers.SerializerMethodField(read_only=True)
78
class Meta:
89
model = BlogPost
910
fields = [
10-
'pk',
11+
'url',
12+
'id',
1113
'user',
1214
'title',
1315
'content',
1416
'timestamp',
1517
]
18+
read_only_fields = ['id', 'user']
1619

1720
# converts to JSON
18-
# validations for data passed
21+
# validations for data passed
22+
23+
def get_url(self, obj):
24+
# request
25+
request = self.context.get("request")
26+
return obj.get_api_url(request=request)
27+
28+
def validate_title(self, value):
29+
qs = BlogPost.objects.filter(title__iexact=value) # including instance
30+
if self.instance:
31+
qs = qs.exclude(pk=self.instance.pk)
32+
if qs.exists():
33+
raise serializers.ValidationError("This title has already been used")
34+
return value

src/postings/api/tests.py

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
from rest_framework import status
2+
from rest_framework.test import APITestCase
3+
4+
from rest_framework_jwt.settings import api_settings
5+
6+
payload_handler = api_settings.JWT_PAYLOAD_HANDLER
7+
encode_handler = api_settings.JWT_ENCODE_HANDLER
8+
9+
from django.contrib.auth import get_user_model
10+
from rest_framework.reverse import reverse as api_reverse
11+
12+
# automated
13+
# new / blank db
14+
15+
from postings.models import BlogPost
16+
User = get_user_model()
17+
18+
class BlogPostAPITestCase(APITestCase):
19+
def setUp(self):
20+
user_obj = User(username='testcfeuser', email='[email protected]')
21+
user_obj.set_password("somerandopassword")
22+
user_obj.save()
23+
blog_post = BlogPost.objects.create(
24+
user=user_obj,
25+
title='New title',
26+
content='some_random_content'
27+
)
28+
29+
30+
def test_single_user(self):
31+
user_count = User.objects.count()
32+
self.assertEqual(user_count, 1)
33+
34+
def test_single_post(self):
35+
post_count = BlogPost.objects.count()
36+
self.assertEqual(post_count, 1)
37+
38+
def test_get_list(self):
39+
# test the get list
40+
data = {}
41+
url = api_reverse("api-postings:post-listcreate")
42+
response = self.client.get(url, data, format='json')
43+
self.assertEqual(response.status_code, status.HTTP_200_OK)
44+
# print(response.data)
45+
46+
def test_post_item(self):
47+
# test the get list
48+
data = {"title": "Some rando title", "content": "some more content"}
49+
url = api_reverse("api-postings:post-listcreate")
50+
response = self.client.post(url, data, format='json')
51+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
52+
53+
54+
def test_get_item(self):
55+
# test the get list
56+
blog_post = BlogPost.objects.first()
57+
data = {}
58+
url = blog_post.get_api_url()
59+
response = self.client.get(url, data, format='json')
60+
self.assertEqual(response.status_code, status.HTTP_200_OK)
61+
62+
def test_update_item(self):
63+
# test the get list
64+
blog_post = BlogPost.objects.first()
65+
url = blog_post.get_api_url()
66+
data = {"title": "Some rando title", "content": "some more content"}
67+
response = self.client.post(url, data, format='json')
68+
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
69+
response = self.client.put(url, data, format='json')
70+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
71+
72+
73+
def test_update_item_with_user(self):
74+
# test the get list
75+
blog_post = BlogPost.objects.first()
76+
#print(blog_post.content)
77+
url = blog_post.get_api_url()
78+
data = {"title": "Some rando title", "content": "some more content"}
79+
user_obj = User.objects.first()
80+
payload = payload_handler(user_obj)
81+
token_rsp = encode_handler(payload)
82+
self.client.credentials(HTTP_AUTHORIZATION='JWT ' + token_rsp) # JWT <token>
83+
response = self.client.put(url, data, format='json')
84+
self.assertEqual(response.status_code, status.HTTP_200_OK)
85+
#print(response.data)
86+
87+
def test_post_item_with_user(self):
88+
# test the get list
89+
user_obj = User.objects.first()
90+
payload = payload_handler(user_obj)
91+
token_rsp = encode_handler(payload)
92+
self.client.credentials(HTTP_AUTHORIZATION='JWT ' + token_rsp)
93+
data = {"title": "Some rando title", "content": "some more content"}
94+
url = api_reverse("api-postings:post-listcreate")
95+
response = self.client.post(url, data, format='json')
96+
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
97+
98+
99+
def test_user_ownership(self):
100+
# test the get list
101+
owner = User.objects.create(username='testuser22222')
102+
blog_post = BlogPost.objects.create(
103+
user=owner,
104+
title='New title',
105+
content='some_random_content'
106+
)
107+
108+
user_obj = User.objects.first()
109+
self.assertNotEqual(user_obj.username, owner.username)
110+
payload = payload_handler(user_obj)
111+
token_rsp = encode_handler(payload)
112+
self.client.credentials(HTTP_AUTHORIZATION='JWT ' + token_rsp)
113+
url = blog_post.get_api_url()
114+
data = {"title": "Some rando title", "content": "some more content"}
115+
response = self.client.put(url, data, format='json')
116+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
117+
118+
def test_user_login_and_update(self):
119+
data = {
120+
'username': 'testcfeuser',
121+
'password': 'somerandopassword'
122+
}
123+
url = api_reverse("api-login")
124+
response = self.client.post(url, data)
125+
self.assertEqual(response.status_code, status.HTTP_200_OK)
126+
token = response.data.get("token")
127+
if token is not None:
128+
blog_post = BlogPost.objects.first()
129+
#print(blog_post.content)
130+
url = blog_post.get_api_url()
131+
data = {"title": "Some rando title", "content": "some more content"}
132+
self.client.credentials(HTTP_AUTHORIZATION='JWT ' + token) # JWT <token>
133+
response = self.client.put(url, data, format='json')
134+
self.assertEqual(response.status_code, status.HTTP_200_OK)
135+
136+
137+
138+
139+
140+
141+
# request.post(url, data, headers={"Authorization": "JWT " + <token> })

src/postings/api/urls.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from django.conf.urls import url
22

33

4-
from .views import BlogPostRudView
4+
from .views import BlogPostRudView, BlogPostAPIView
5+
56
urlpatterns = [
7+
url(r'^$', BlogPostAPIView.as_view(), name='post-listcreate'),
68
url(r'^(?P<pk>\d+)/$', BlogPostRudView.as_view(), name='post-rud')
79
]

src/postings/api/views.py

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,50 @@
11
# generic
22

3-
4-
from rest_framework import generics
3+
from django.db.models import Q
4+
from rest_framework import generics, mixins
55

66
from postings.models import BlogPost
7+
from .permissions import IsOwnerOrReadOnly
78
from .serializers import BlogPostSerializer
89

10+
11+
class BlogPostAPIView(mixins.CreateModelMixin, generics.ListAPIView): # DetailView CreateView FormView
12+
lookup_field = 'pk' # slug, id # url(r'?P<pk>\d+')
13+
serializer_class = BlogPostSerializer
14+
#queryset = BlogPost.objects.all()
15+
16+
def get_queryset(self):
17+
qs = BlogPost.objects.all()
18+
query = self.request.GET.get("q")
19+
if query is not None:
20+
qs = qs.filter(
21+
Q(title__icontains=query)|
22+
Q(content__icontains=query)
23+
).distinct()
24+
return qs
25+
26+
def perform_create(self, serializer):
27+
serializer.save(user=self.request.user)
28+
29+
def post(self, request, *args, **kwargs):
30+
return self.create(request, *args, **kwargs)
31+
32+
def get_serializer_context(self, *args, **kwargs):
33+
return {"request": self.request}
34+
35+
936
class BlogPostRudView(generics.RetrieveUpdateDestroyAPIView): # DetailView CreateView FormView
1037
lookup_field = 'pk' # slug, id # url(r'?P<pk>\d+')
1138
serializer_class = BlogPostSerializer
39+
permission_classes = [IsOwnerOrReadOnly]
1240
#queryset = BlogPost.objects.all()
1341

1442
def get_queryset(self):
1543
return BlogPost.objects.all()
1644

45+
def get_serializer_context(self, *args, **kwargs):
46+
return {"request": self.request}
47+
1748
# def get_object(self):
1849
# pk = self.kwargs.get("pk")
1950
# return BlogPost.objects.get(pk=pk)

src/postings/models.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from django.conf import settings
22
from django.db import models
3+
from django.urls import reverse
34

5+
from rest_framework.reverse import reverse as api_reverse
46

7+
# django hosts --> subdomain for reverse
58

69
class BlogPost(models.Model):
710
# pk aka id --> numbers
@@ -11,4 +14,14 @@ class BlogPost(models.Model):
1114
timestamp = models.DateTimeField(auto_now_add=True)
1215

1316
def __str__(self):
14-
return str(self.user.username)
17+
return str(self.user.username)
18+
19+
@property
20+
def owner(self):
21+
return self.user
22+
23+
# def get_absolute_url(self):
24+
# return reverse("api-postings:post-rud", kwargs={'pk': self.pk}) '/api/postings/1/'
25+
26+
def get_api_url(self, request=None):
27+
return api_reverse("api-postings:post-rud", kwargs={'pk': self.pk}, request=request)

0 commit comments

Comments
 (0)