- 본 레포지토리는 백엔드 스터디 2-3주차의 과제를 위한 레포입니다.
- 따라서 해당 레포를 fork 및 clone 후 local에서 본인의 깃헙 ID 브랜치로 작업한 후 커밋/푸시하고, PR 보낼 때도
본인의 브랜치-> 본인의 브랜치
로 해야 합니다.
-
User
email
password
username
... -
Profile
nickname : 닉네임
comment : 간단한 소개
user_id : 회원 phone_num : 연락처 website : 웹사이트 img : 프로필사진 -
Post
text : 글
pub_date : 업로드날짜
author_id : 글쓴이 -
Video
video : 동영상
post_id : 소속게시물 -
Photo
image : 사진
post_id : 소속게시물
-FileField 를 통해 저장한 모든 파일을 지칭 (ImageField도 포함)
-db필드에는 저장경로를 저장
-실제 파일은 settings.MEDIA_ROOT 경로에 저장
-모델작성시에 upload_to인자로 더 자세한 저장경로를 지정가
- settings
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
- models.py
# media/image/ 아래에 저장
photo = models.ImageField(upload_to="image")
# 이미지 업로드 날짜에 따라 디렉토리에 저장 (strftime 으로 포멧팅)
photo = models.ImageField(upload_to="%Y/%m/%d")
- urls.py
# 이미지 url설정
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
- Post 모델 객체넣기
>>> user1 = User(username = 'user1', password = 'abc')
>>> user1.save()
>>> User.objects.all()
<QuerySet [<User: user1>]>
>>> post1 = Post(written = '첫글입니다.', author = user1, pub_date = timezone.now())
>>> post2 = Post(written = '한남동에서 과제중', author = user1, pub_date = timezone.now())
>>> post3 = Post(written = '오늘저녁은 수제버', author = user1, pub_date = timezone.now())
>>> post1.save()
>>> post2.save()
>>> post3.save()
- 객체 조회
>>> Post.objects.all()
<QuerySet [<Post: 첫글입니다.>, <Post: 한남동에서 과제중>, <Post: 오늘저녁은 수제버거>]>
- filter함수사용
>>> Post.objects.filter(written__startswith='한남동')
<QuerySet [<Post: 한남동에서 과제중>]>
>>> Post.objects.filter(id=3)
<QuerySet [<Post: 오늘저녁은 수제버거>]>
- relation연습
>>> post1.author
<User: user1>
>>> post1.author.username
>>> 'user1'
( Post 안에 foreign key 가 있고 그게 User로 향하고 있어서 User은 post_set을 갖는다 )
>>> user1 = User.objects.get(id=1)
>>> user1
<User: user1>
>>> user1.post_set.all()
<QuerySet [<Post: 첫글입니다.>, <Post: 한남동에서 과제중>, <Post: 오늘저녁은 수제버거>]>
- get() 과 filter()차이
get()은 객체(<User: user1>)를 반환 : 멤버에 접근가능
user1.post ( o )
filter()은 queryset(<QuerySet [<User: user1>]>)을 반환 : 멤버접근불가
user1.post ( x ), user1.username ( x )
오류 : AttributeError: 'QuerySet' object has no attribute 'username'
처음에는 모델링이 생각보다 간단할 줄 알았지만 실제로 하면서 영상과 사진 업로드를 다루는 과정이 생각보다 복잡해서 좀 애를 먹었습니다. 미리보기문제로 마크다운에 추가하진 못했지만 draw.io를 사용해서 모델링을 해보면서 원래는 노트에 하곤 했었는데 너무 편하고 좋았습니다. 앞으로 insta clone을 해서 완성될 모습이 기대가 됩니다.
선택모델 : Post
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
nickname = models.CharField(max_length=10, unique=True)
comment = models.TextField(max_length= 200, null= True, blank=True)
web_site = models.TextField(max_length= 100, null= True, blank=True)
phone_num = models.TextField(max_length= 15)
img = models.ImageField(upload_to="profile_img", null= True, blank=True)
def __str__(self):
return self.nickname
# User 와 1대다 관계
class Post(models.Model):
text = models.TextField()
author = models.ForeignKey(Profile, on_delete=models.CASCADE, related_name= 'posts')
pub_date = models.DateTimeField(auto_now_add = True)
like = models.ManyToManyField(Profile, related_name='like_posts', blank=True, null=True)
def __str__(self):
return 'post: {} by {}'.format(self.text, self.author.nickname)
def like_count(self):
return self.like.count()
데이터 조회
>>> Post.objects.all()
<QuerySet [<Post: post: 첫 번째글 by suy2on>, <Post: post: 다시글 by 포슬포슬>, <Post: post: 무야호~ by 포슬포슬>]>
api/posts (GET)
GET /api/posts
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"id": 1,
"text": "첫 번째글",
"like": [
1
],
"author_nickname": "suy2on",
"author": 2,
"photos": [],
"videos": []
},
{
"id": 2,
"text": "다시글",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 3,
"text": "무야호~",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 4,
"text": "post 이용해서 넣는 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 5,
"text": "post 이용해서 넣는 두번째 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
}
]
api/users/2/posts (GET)
[
{
"id": 1,
"text": "첫 번째글",
"like": [
1
],
"author_nickname": "suy2on"
}
]
api/posts (POST) { "text": "post 이용해서 넣는 두번째 문장", "author" : 1 }
{
"id": 5,
"text": "post 이용해서 넣는 두번째 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1
}
-
- fields = all, exclude, 직접 명시 ('id', 'name') 과 같은 방식들로 넣을 수 있음
- forignkey로 연결된 field는 넣어주어야 한다 ( 아직 정확한 이유는 찾지 못했습니다 ㅠㅠ )
- 기존에 있던 field가 아닌 걸 추가하고 싶다면 : serializer.SerializerMethodField()로 정의해준뒤
get_<field_name> 이름의 메소드를 만들어주면 된다 - serializers.StringRelatedField() 를 사용해주면 ForgeignKey로 연결된 모델의 str 메소드에서 정의한 string를 리턴
- 1:n 에서 1쪽의 serializer에서 n을 위한 변수를 만들었다면 (PostSerializer의 photos같이)
꼭 field에 넣어줘야한다
(에러: AssertionError: The field 'photos' was declared on serializer PostSerializer, but has not been included in the 'fields' option. )
-
- GET 전체조회, POST 같은 경우에는 id값을 붙여주지 않는다
- resource부분에 명사는 복수형으로 쓴다 (api/posts)
- resource와 따라오는 id가 연관되게 쓴다 ( api/posts/2 -> api/users/2/posts )
- 소문자로 작성한다
- 마지막에는 /를 추가하지 않는다
- _를 사용하지 않는 (대신 하이픈(-)을 사용해준다)
-
post의 author(작성자)를 넘겨줄 때 { 'author' : 객체 -> id }로 넘져준다
db에서 id값만 가져와서 저장하고 있기때문이다 (author_id)
평소에 rest-api를 이론으로만 봤을 때는 어렵지 않다고 생각하고 있었는데 막상 이렇게 제대로 짜보려니 생각해야할 것들이 꽤 많다고 느꼈습니다. 그래도 이전에 프로젝트에서는 url이 너무 많아지거나 일정한 규칙없이 늘어나서 가독성도 떨어지고 관리에 어려움을 겪었는데 이렇게 rest-api를 배우고 나니 그런부분들이 많이 개선될 수 있을것같아서 좋았습니다. 또 serializer 를 배우기 전에는 jsonify로 많이 응답을 보내곤 했었는데요 이렇게 뭔가 json형식으로 만들어주는 방법을 배워서 기쁘고 제네릭처럼 조금 생소하고 어려웠지만 유용하게 쓸 수 있을거같다는 생각이 들었습니다! 물론 아직 완전 기초적인 부분들만 한것 같아서 앞으로 더 rest-api에대해 알아가는 시간들을 가졌으면 좋겠고 많이 배워야겠다는 생각이 듭니다!!
GET api/contents/
[
{
"id": 1,
"text": "첫 번째글",
"like": [
1
],
"author_nickname": "suy2on",
"author": 2,
"photos": [],
"videos": []
},
{
"id": 2,
"text": "다시글",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 3,
"text": "무야호~",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 4,
"text": "post 이용해서 넣는 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 5,
"text": "post 이용해서 넣는 두번째 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 6,
"text": "또 다른 시도",
"like": [],
"author_nickname": "suy2on",
"author": 2,
"photos": [],
"videos": []
}
]
GET api/contents/2
{
"id": 2,
"text": "다시글",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
}
POST api/contents/ { "text" : "중간고사전 마지막 과제하는 중", "author" : 1 }
{
"id": 7,
"text": "중간고사전 마지막 과제하는 중",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
}
PUT api/contents/2 { "text" : "text 수정합니다" , "author" : 1 }
{
"id": 2,
"text": "text 수정합니다",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
}
DELETE api/contents/2
"DELETE /api/contents/2 HTTP/1.1" 204 0
[
{
"id": 1,
"text": "첫 번째글",
"like": [
1
],
"author_nickname": "suy2on",
"author": 2,
"photos": [],
"videos": []
},
{
"id": 3,
"text": "무야호~",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 4,
"text": "post 이용해서 넣는 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 5,
"text": "post 이용해서 넣는 두번째 문장",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 6,
"text": "또 다른 시도",
"like": [],
"author_nickname": "suy2on",
"author": 2,
"photos": [],
"videos": []
},
{
"id": 7,
"text": "중간고사전 마지막 과제하는 중",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
}
]
def get_object(self, pk):
try:
return Post.objects.get(pk=pk)
except Post.DoesNotExist:
raise Http404
- POST은 새로 추가 PUT 수정
def put(self, request, pk, format=None):
post = self.get_object(pk)
serializer = PostSerializer(post, data=request.data, partial= True)
FBV로 구현을 해보고 CBV로 옮기니까 훨씬 이해가 빠르게 되었고 , FBV보다 좀더 잘 정리된 느낌을 받았습니다
- 기존에 구현했던 API를 Viewset을 이용하여 리팩토링 해주세요!
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
class ProfileViewSet(viewsets.ModelViewSet):
serializer_class = ProfileSerializer
queryset = Profile.objects.all()
GET, POST, PUT, PATCH, DELETE 모두 잘 돌아감
class PostFilter(FilterSet):
text = filters.CharFilter(field_name="text", lookup_expr="icontains") #해당 문자열을 포함하는 queryset
is_current= filters.BooleanFilter(method='filter_is_current') # true시에 이번 달 게시물만 출력
class Meta:
model = Post
fields = ['author', 'text', 'pub_date']
def filter_is_current(self, queryset, name, value):
set1 = queryset.filter(pub_date__year=2021)
set2 = queryset.filter(pub_date__month=5)
return set1 & set2
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
filter_backends = [DjangoFilterBackend]
filter_class = PostFilter
api/contents/?text=viewset
[
{
"id": 3,
"text": "viewset해봄",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
{
"id": 8,
"text": "viewset 과제 완료 가즈",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
]
api/contents/?text=viewset&is_current=true
[
{
"id" 8,
"text": "viewset 과제 완료 가즈",
"like": [],
"author_nickname": "포슬포슬",
"author": 1,
"photos": [],
"videos": []
},
]
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
filter_backends = [DjangoFilterBackend]
filter_class = PostFilter
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
- 본인 것만 수정 및 삭제 가능 / 아니라면 읽기전 (글쓴이인지 아닌)
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
print(obj)
print(request.user)
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.author.user.id == request.user.id # obj.author -> Profile / profile.user -> User
-
로그인한 사용자만 생성/ 수정/ 삭제 가능 / 나머지는 읽기전용지 (회원인지 아닌)지
-
http://127.0.0.1:8000/api/contents/ POST -> 로그인안한상태
{ "detail": "자격 인증데이터(authentication credentials)가 제공되지 않았습니다." }
-> IsOwnerOrReadOnly는 아직 구현 못해봄.. (인증하는과정에서 계속 시도중입니다.)
import re # 정규식 쓰기위한 모듈
class ProfileSerializer(serializers.ModelSerializer):
posts = PostSerializer(many=True, read_only=True)
user_password = serializers.SerializerMethodField()
user_username = serializers.SerializerMethodField()
class Meta:
model = Profile # 사용할 모델
fields = ['id', 'nickname', 'user_username', 'user_password', 'web_site', 'phone_num', 'posts', 'comment', 'img']
def get_user_password(self, obj):
return obj.user.password
def get_user_username(self, obj):
return obj.user.username
def validate_phone_num(self, value):
pattern = '\d{3}-\d{4}-\d{4}'
phoneReg = re.compile(pattern)
print(value)
if(phoneReg.match(value) == None):
raise serializers.ValidationError("핸드폰번호는 xxx-xxxx-xxxx 형식이어야합니다.")
return value # 유효성 검사후 다시 돌려줌
http://127.0.0.1:8000/api/profiles/
phone_num = 01012345678
{
"phone_num": [
"핸드폰번호는 xxx-xxxx-xxxx 형식이어야합니다."
]
}
-
django view -> rest_framework APIView -> Generic Views -> Viewsets
-
-
Retrieve, Destory, Update -> model_name/<int:pk>/ -> pk해당 인스턴스관련
-
List, Create -> model_name/ -> 전체 get 또는 새로 post
-
-
ModelViewSet에서 기본 제공해주는 기능 외에 더 커스터마이징 하고싶은 경우 사용 자
-
import문
from rest_framework.decorators import action
-
action의 인자
- detail : Boolean 으로서 True 일 경우, pk 값을 지정해줘야하는 경우에 사용하고 False일 경우, 목록 단위로 적용
- method : request method 를 지정해줄 수 있습니다. 디폴트 값은 get
-
detail에 따른 router
(이 때 모든 이름은 소문자이며, function name의 언더바(_)는 하이픈(-) 으로 교체)-
detail=True
url : /prefix/{pk}/{function name}/
name : {model name}-{function name} -
detail=False
url : /prefix/{function name}/
name : {model name}-{function name}
-
-
예
class PostViewSet(ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer # url : post/{pk}/set_public/ @action(detail=True, methods=['patch']) def set_public(self, request, pk): instance = self.get_object() instance.is_public = True instance.save() serializer = self.get_serializer(instance) return Response(serializer.data)
-
ViewSet은 하나의 view가 아닌 view들의 set이기 때문에 apiView와 다르게 url mapping방식에 router를 사용
router가 위의 코드에서 as_view로 method마다 함수로 연결해주던 것을 대신해
-
as_view이용해서 각각 매핑
class PostViewSet(ModelViewSet): queryset = Post.objects.all() serializer_class = PostSerializer post_list = PostViewSet.as_view({ 'get': 'list', 'post': 'create', }) post_detail = PostViewSet.as_view({ 'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy', })
urlpatterns = [ path('post/', views.post_list), path('post/<int:pk>/', views.post_detail), ]
-
router를 이용하여 한번에 매핑
router = DefaultRouter() router.register(r'post',views.PostViewSet) urlpatterns = [ path('',include(router.urls)), ]
-
api/contents/?is_current=true
- params
name = is_current
value = True
이를 이용하여 method 작성시에 if문을 사용해서 더 다양한 필터링가능
- params
def filter_is_current(self, queryset, name, value):
set1 = queryset.filter(pub_date__year=2021)
set2 = queryset.filter(pub_date__month=5)
return set1 & set2
-
- list : ‘price’와 ‘release_date’필드 모두에 대해 ‘exact’ 조회를 발생시킵니다
class ProductFilter(FilterSet): class Meta: model = Product fields = ['price', 'release_date']
- dict : 복수 조회 조건설정
class ProductFilter(FilterSet): class Meta: model = Product fields = { 'price': ['lt', 'gt'], 'release_date': ['exact', 'year__gt'], }
초반에 장고튜토리얼에서 제네릭뷰만 접했을 때도 완전 신세계였는데 viewSet이 더 끝판왕이라고 느껴졌습니다 하지만 그만큼 내부에서 어떤과정으로 이루어지는지 쉽게 보이지 않기 때문에 그 과정을 잘 공부해야 나중에 필요에의해 커스터마이징을 잘 할 수 있고 더 잘 viewSet을 이용 할 수 있을 것 같습습니다. 이번 장이 서비스를 만들면서 더욱 완성도를 높여주기위해 필요한 것들이 많이 담겨있던 파트였다고 생각합니다. 그래서 처음접해보는 것이 많아 익혀야 할 내용이 좀 많았지만 시간이 들 더라도 꼼꼼히 보고 잘 숙지하려고 합니다.
- https://ssungkang.tistory.com/entry/Django-APIView-Mixins-generics-APIView-ViewSet%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90?category=366160
- https://www.django-rest-framework.org/api-guide/viewsets/
pip install djangorestframework djangorestframework-jwt
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS' : ( # query로 필터링할 때
'django_filters.rest_framework.DjangoFilterBackend',
),
'DEFAULT_PERMISSION_CLASSES': ( # 로그인 했는지
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_AUTHENTICATION_CLASSES': ( # 로그인 관련 인증
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
}
JWT_AUTH = {
'JWT_SECRET_KEY': SECRET_KEY,
'JWT_ALGORITHM': 'HS256', # jwt algo
'JWT_ALLOW_REFRESH': True, # JWT 토큰을 갱신할 수 있게 할지 여부
'JWT_EXPIRATION_DELTA': datetime.timedelta(days=7), # JWT 토큰의 유효 기간을 설정
'JWT_REFRESH_EXPIRATION_DELTA': datetime.timedelta(days=28), #JWT 토큰 갱신의 유효기간
}
# 7일마다 갱신하지 않으면 로그아웃 , 하지만 계속 갱신해도 28일 후에는 자동로그아웃
# jwt token
from rest_framework_jwt.views import obtain_jwt_token, verify_jwt_token, refresh_jwt_token
urlpatterns = [
path('admin/', admin.site.urls),
path('auth/token/', obtain_jwt_token), # JWT 토큰을 발행
path('auth/token/verify/', verify_jwt_token), # JWT 토큰이 유효한지 검증
path('au/token/refresh/', refresh_jwt_token), # JWT 토큰을 갱신할 때 사용
path('api/', include('api.urls'))
]
# auth
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
class PostViewSet(viewsets.ModelViewSet):
serializer_class = PostSerializer
queryset = Post.objects.all()
filter_backends = [DjangoFilterBackend]
filter_class = PostFilter
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
authentication_classes = [JSONWebTokenAuthentication]
-
- 토큰발급
http://localhost:8000/auth/token/에 POST로 username과 password를 설정하여 요청 (superuser)로
{ "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo5LCJ1c2VybmFtZSI6ImRvcm8iLCJleHAiOjE2MjIzNTMwMDAsImVtYWlsIjoidG53amRybXNAbmF2ZXZyLmNvbSIsIm9yaWdfaWF0IjoxNjIxNzQ4MjAwfQ.64jwlN0J2smLAcaRQiVNjwbVJnqFQEbESRYyGYf7Nwk" }
- 인증받기
header에
key : Authorization
value : jwt 발급받은 token
http://127.0.0.1:8000/api/contents/ POST 가능
참고: https://dev-yakuza.posstree.com/ko/django/jwt/
Q. superuser말고 그냥 내가 생성한 다른 user로는 jwt토큰 발급을 어떻게 받을 수 있을까??? (여기선 bad request라고 뜸) - 토큰발급