[link][https://www.erdcloud.com/d/g3qNePNiy2dTTPBRq]
class Profile(AbstractUser):
nickname = models.CharField(max_length=15);
user_mbti = models.CharField(max_length=4);
def __str__(self):
return self.nickname
- 장고에서 기본으로 제공하는 AUTH를 이용해 User를 구성(기본 : username , password1 , password2 )
- 다른 항목들을 추가하기 위해, AbstractUser를 이용함.
- 한 user의 profile에 접근했을 때 닉네임으로 표기해 주기 위해 def str 을 사용했음.
path('signup/', views.signup, name='signup')\
- 보이는 그대로.
def signup(request):
if request.method == "POST":
form = Signupform(request.POST)
if form.is_valid():
form.save()
username = form.cleaned_data.get('username')
raw_password = form.cleaned_data.get('password1')
user = authenticate(username=username, password=raw_password)
login(request, user)
return redirect('MBTIAPP:main')
else:
form = Signupform()
return render(request, 'MBTIAPP/signup.html', {'form' : form})
-
POST 방식의 요청을 받으면 , 미리 작성해둔 "Signupform"을 form으로 정의해줌 => 그 후, form을 검사하고 , 저장. 저장한 데이터들을 변수들에 담음(cleaned_data.get)
-
그 후 회원가입이 완료되면 자동으로 로그인 되게 처리 후 main 페이지로 보냄
-
GET 방식일때는 (else문) signup.html을 그냥 form을 담아서 보여줌 (당연히 가입하려고 회원가입을 누른 상황이니까)
class Signupform(UserCreationForm):
nickname = forms.CharField(max_length=15 ,label="닉네임", required=True)
class Meta:
model = Profile
fields = ("username", "password1" , "password2", "nickname")
-
여기서 AbstractUser로 구현했기에, 연결하느라 좀 애를 먹음.
-
nickname 은 기본 AUTH USER에 없기에 따로 추가해줬음.
-
form을 템플릿 상에서 보여줄 때, 어떤 필드를 보여줄지를 나열함.
<form method="post" action="{% url 'MBTIAPP:signup' %}">
{% csrf_token %}
{% include "MBTIAPP/form_errors.html" %}
-
템플릿은 너무 길어서 핵심만 첨부.
-
회원가입을 하고 정보를 보내니, POST 방식으로, signup의 이름을 가진 url으로 보냄.
-
보안을 위해 csrf_token , form_errors.html 만들어서 오류처리 (장고 내장기능) => 점프 투 장고 참고
path('login/', auth_views.LoginView.as_view(template_name='MBTIAPP/login.html'),name='login'),
- 장고에 내장되어 있는, LoginView를 import해서 사용.
- LoginView가 해줌..
- 뻔한 내용이라 생략
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
- 로그인과 동일하게 LogoutView 가 해줌.
{% if user.is_authenticated %}
{% csrf_token %}
<a href="{% url 'MBTIAPP:logout' %}">로그아웃</a>
<a href="{% url 'MBTIAPP:mbtitest' %}">MBTI테스트 하기</a>
<a href="{% url 'MBTIAPP:blog' %}">MBTI 게시판으로</a>
{% endif %}
{% if not user.is_authenticated %}
{% csrf_token %}
<a href="{% url 'MBTIAPP:login' %}">로그인</a>
<a href="{% url 'MBTIAPP:signup' %}">회원가입</a>
{% endif %}
-
User가 로그인이 되어있지 않은데, MBTI검사를 시킬 순 없으므로 "user.is_authenticated" 를 사용하여,
-
로그인이 안된 상태라면, "로그인과 회원가입" 을.
-
로그인이 된 상태라면, "로그아웃/ MBTI테스트url / MBTI게시판url" 을 보여주게 설정 하였다.
- 뻔함
class MbtiForm(forms.Form):
EI = forms.ChoiceField(choices=[('E','네'),('I','아니오')], widget=forms.RadioSelect)
NS = forms.ChoiceField(choices=[('N','네'),('S','아니오')], widget=forms.RadioSelect)
TF = forms.ChoiceField(choices=[('F','네'),('T','아니오')], widget=forms.RadioSelect)
PJ = forms.ChoiceField(choices=[('J','네'),('P','아니오')], widget=forms.RadioSelect)
-
어려웠음. MBTI 테스트를 하는 템플릿에서 Form형식으로 직접 코드를 짜려했지만, 그 결과를 조합해서 하나의 MBTI를 만들자니, 모든 경우를 if 문으로 처리하는 노가다를 해야할 것 같아서 하기 싫었음.
-
[링크][https://www.geeksforgeeks.org/choicefield-django-forms/]
-
Form의 사용법을 잘 몰랐기에 배울 겸 알아보았는데 위의 링크에서 ChoiceField를 form에서 지정해줄 수 있다고 알려줌.
def result(request):
if request.method == 'POST':
form = MbtiForm(request.POST)
if form.is_valid():
EI = form.cleaned_data['EI']
NS = form.cleaned_data['NS']
TF = form.cleaned_data['TF']
PJ = form.cleaned_data['PJ']
arr=[EI, NS, TF, PJ]
my_mbti = ''.join(arr)
user = request.user
user.user_mbti = my_mbti
user.save()
context = {'mymbti':my_mbti}
return render(request, 'MBTIAPP/result.html', context)
else:
form = MbtiForm()
context = {'form': form}
return render(request, 'MBTIAPP/result.html',context)
-
일단 POST 방식일때, if문 진입. 폼 검사 후, EI 등등의 변수( 이름 마음대로 만들어도 됨 ) 에 아까 회원가입할 때 배운 form.cleaned_data로 form에 입력된 값들을 집어 넣는다.
-
그 다음 arr이란 리스트에 mbti들을 넣음, 그 후, join을 하여 my_mbti라는 변수를 만들어 거기에 리스트 요소들을 합쳐서 하나의 문자열로 만듬
-
그 후 저장. 결과 창으로 정보(my_mbti) 담아서 이동
-
뭔가 점프 투 장고같은 곳에 나오는 뻔한 내용이 아닌 내용을 내손으로 구현해보니 가장 재미있었던 부분.
<h1>결과창임</h1>
{{ mymbti }}
<a href="{% url 'MBTIAPP:blog' %}">mbti 게시판으로 이동하기</a>
- 그냥 아까 만든 mymbti를 보여줌..
def result(request):
if request.method == 'POST':
form = MbtiForm(request.POST)
if form.is_valid():
EI = form.cleaned_data['EI']
NS = form.cleaned_data['NS']
TF = form.cleaned_data['TF']
PJ = form.cleaned_data['PJ']
arr=[EI, NS, TF, PJ]
my_mbti = ''.join(arr)
user = request.user
user.user_mbti = my_mbti
user.save()
context = {'mymbti':my_mbti}
return render(request, 'MBTIAPP/result.html', context)
else:
form = MbtiForm()
context = {'form': form}
return render(request, 'MBTIAPP/result.html',context)
-
보면 알겠지만, 아까 MBTI테스트의 view 코드이다.
-
어떠한 기능을 구현하는데에 있어 중요한 내용 같아서 따로 씀.
-
user = request.user user.user_mbti = my_mbti user.save()
-
❗️ "user" 를 "request.user" 로 지정하여, 현재 이 동작을 수행한 "request.user" 정의 하기.
-
❗️ 아까 Abstractuser로 만든 Profile이라는 모델에 user_mbti라는 필드를 추가했었다. 여기서 user.user_mbti라고 표기할 수 있는 이유는 Abstractuser는 AUTH로 만든 User를 상속하기 때문.
-
❗️ 그 후 , 아직 빈칸인 user.user_mbti 에 my_mbti를 집어 넣어줌. (재검사 일시, 새로운 값으로 대체) => 저장함.
class Post(models.Model):
title =models.CharField(max_length=50)
poster = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE ,default=1)
content = models.TextField()
def __str__(self):
return self.title
-
중요한 부분 : Poster라는 필드는 아까 만든 Profile모델을 참조해야함. 그런데, 그냥 Profile을 참조한다고 하면, 어느 부분이 계속 작동을 안해서 ,
-
❓settings.AUTH_USER_MODEL (AbstractUser를 사용할 때 setting.py에 지정해줌.) 을 가리키면 잘 작동함 (아직까지 이유 모름)
def new(request):
return render(request, 'MBTIAPP/postcreate.html')
def postcreate(request):
if(request.method == 'POST'):
post = Post()
post.title=request.POST['title']
post.content=request.POST['content']
post.poster = request.user
post.save()
return redirect('MBTIAPP:blog')
-
사실 보편적인 기능이라, 구글에 잘 나와있음.
-
postcreate 에서 "post" 를 Post(모델)을 가리키게 하고,
-
Post의 필드들에 POST방식으로 받은 정보들을 저장해준 후,
-
❗️ Post모델의 poster는 아직 비어있으므로, "request.user" 을 불러와, 지금 이 동작을 수행한 user를 poster에 넣어준다
-
저장하기!
def blog(request):
posts = Post.objects.all()
context = {'posts':posts}
return render(request, 'MBTIAPP/blog.html', context)
def detail(request, pk):
post_detail = get_object_or_404(Post, pk=pk)
comments = post_detail.comment_set.all()
context = {
'post' : post_detail,
'comments' : comments
}
return render(request, 'MBTIAPP/detail.html', context)
-
게시글 목록을 보는 기능과, 상세페이지 구현.
-
먼저, blog 함수 : "posts" 변수에 Post모델 담음. => blog.html로 정보 보냄, 끝.
-
다음, detail 함수 : "post_detail" 변수 만들고 Post모델 가져옴
-
여기엔 곧 설명할 댓글도 들어가므로, "comments" 라는 변수에 Comment 모델도 가져온다.
-
render로 context에 담은 정보들 보냄.
def edit(request, pk):
post = Post.objects.get(pk=pk)
if request.method == 'POST':
post.title=request.POST['title']
post.content=request.POST['content']
post.save()
return redirect('MBTIAPP:detail', pk)
else:
context = {
'post' : post
}
return render(request, 'MBTIAPP/edit.html', context)
-
"post" 라는 변수에 Post모델 가져옴.
-
POST 방식일 떄 (수정하기 버튼을 눌러 제출할 떄)는, post의 객체들을 그냥 바꿔주고 저장!!
-
GET 방식일 때 (수정하려고 들어왔을 때)는, 원래 쓰여있던 제목/글 들 (post) 를 보여줌.
def delete(request, pk):
post = Post.objects.get(pk=pk)
post.delete()
return redirect('MBTIAPP:blog')
- 간단하다. post의 pk 를 받아와서, post 지정해주고 그냥 지워주면 됨..
<form action="{% url 'MBTIAPP:postcreate' %}" method="POST">
{% csrf_token %}
<label for="title">제목</label><br>
<input type="text" name="title", id="title"><br>
<label for="content">글 내용</label><br>
<textarea name="content" id="content" cols="40" rows="20"></textarea><br>
<input type="submit" value="작성하기">
</form>
-
form 태그를 이용해 정보 입력 후 제출
-
❗️ input의 name이 중요함. 이 name으로 view에서 POST방식으로 전달됨. ❗️
{% for post in posts %}
<div class="posts">
<h2>글 제목</h2>
<a href="{% url 'MBTIAPP:detail' post.pk %}">{{post.title}}</a>
<h2>작성자</h2>
<p>{{post.poster.nickname}}<span>({{ post.poster.user_mbti }})</span></p>
</div>
{% endfor %}
-
게시글 리스트 페이지, for문을 돌려 post라는 변수를 이용해 posts안에 담긴 게시글의 수만큼 반복 됨.
<h1>디테일페이지</h1> <div class="details"> <a href="{% url 'MBTIAPP:blog' %}">게시판으로 돌아가기</a><br> <h2>제목</h2> <p>{{ post.title }}</p> <h2>내용</h2> <p>{{ post.content }}</p> <h2>작성자</h2> <span>{{ post.poster }}({{ post.poster.user_mbti }})</span><br><br><br> <a href="{% url 'MBTIAPP:edit' post.pk %}">수정하기</a> <a href="{% url 'MBTIAPP:delete' post.pk %}">삭제하기</a> </div>
-
Detail 페이지 인데, 게시글 제목, 내용 보여주고 작성자 닉네임, mbti를 보여줌.
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE)
commenter = models.ForeignKey(settings.AUTH_USER_MODEL , on_delete=models.CASCADE)
content = models.CharField(max_length=150)
def __str__(self):
return self.content
-
Post와 비슷하다.
-
다른점 : Comment에서는 어느 Post에 달린 댓글인지를 구분해야 하기 때문에, post라는 필드를 만들어 Post모델을 외래키로 참조한다.
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
exclude = ('post','commenter')
- 댓글을 작성할 때, 댓글 내용만 입력받으면 되니, Comment모델에서 post와 commenter는 exlude로 빼줌.
- form태그로 html상에서 처리할 수 있을 것 같지만 Form.py를 이용하는 개념이 애매해서 공부할 겸 form으로 만듬.
path('<int:pk>/comment/', views.comment_create, name='comment_create'),
path('<int:post_pk>/comment/<int:comment_pk>/delete/', views.comment_delete, name='comment_delete'),
path('<int:post_pk>/comment/<int:comment_pk>/edit/', views.comment_edit, name='comment_edit'),
-
❗️ 중요한듯 하다. 보면, comment_create의 url은 pk/comment 형식 . pk는 post의 pk이다.
-
❗️ 핵심은 , delete와 edit은 댓글을 건드리는 url들 인데, 어느 게시물의 댓글인지 구분해야 하므로, post의 pk 와 comment의 pk를 둘다 url에 집어 넣어야 한다.
def comment_create(request, pk):
if request.user.is_authenticated:
post = get_object_or_404(Post, pk=pk)
form = CommentForm()
if request.method == 'POST':
comment_form = CommentForm(request.POST)
if comment_form.is_valid():
comment = comment_form.save(commit=False)
comment.post = post
comment.commenter = request.user
comment.save()
return redirect('MBTIAPP:detail', pk)
else:
form = CommentForm()
return render(request, 'MBTIAPP/detail.html', {'post':post, 'form': form})
-
헷갈려서 시간이 꽤나 걸림.. pk를 주고 받는 동작에 대한 공부가 잘 되었음.
-
먼저 is_authenticated (로그인상태) 일때, "post"라는 변수에 Post모델 가져옴.
-
처음 POST방식 으로 들어갔을 때, "comment_form"이라는 변수에, CommentForm을 가져옴. => 폼 검사 후,
-
comment의 필드들에 필요한 정보들을 담는다.
-
comment.post에는 이 글이 어떤 post인지 알기 위해 post를 담아줌
-
commenter 에는 이 댓글을 단 사람이 누구인지 DB에 넣어주기 위해 request.user를 저장함.
-
GET방식 일땐 빈폼을 보여줌.
-
❓ 의문인점) 위에 있는 form = CommentForm() 을 쓰기전에는 오류가 떠서 알아보니, if문 (POST방식)으로 들어가기 전에 form을 정의해주어야 한다고 함.
-
❓ 그건 else문안의 GET방식에서 form = CommentForm() 입력해 두었으니 GET으로 받고 입력하고 POST보내도 되는거 아닌가?...
def comment_delete(request, post_pk, comment_pk):
if request.user.is_authenticated:
comment = get_object_or_404(Comment, pk=comment_pk)
if request.user == comment.commenter:
comment.delete()
return redirect('MBTIAPP:detail', post_pk)
-
post의 pk 와 comment의 pk 를 동시에 생각하느라 공부가 되었다.
-
어느 게시물의 어느댓글을 지울건지 알아야 하기 때문에, post와 comment의 pk를 둘다 인자로 받음.
-
"comment"라는 변수에 Comment모델 받아옴, 당연히 이때 pk는 해당 댓글의 pk
-
if 문으로 들어가서, 지금 이 동작을 수행하는 user와 comment를 작성한 user가 같다면, 지우게 실행. (근데 이건 어차피 html상에서 본인이 쓴 댓글이 아니면 삭제버튼 자체가 안뜨게 구현해놔서 큰 의미는 없지만 그냥 작성했음)
-
그리고 동작이 끝나면, detail페이지로 이동.
-
❗️ detail페이지로 이동할 땐 post의 디테일 페이지 이므로, post_pk를 보내준다 ! comment_pk는 필요없음!
<h3>댓글들</h3>
{% if comments %}
<p>{{ comments|length }}개의 댓글이 있어요</p>
{% endif %}
<ul>
{% for comment in comments %}
<li>
{{ comment.content }} - {{ comment.commenter }}<span>({{ comment.commenter.user_mbti }})</span>
{% if user == comment.commenter %}
<form action="{% url 'MBTIAPP:comment_delete' post.pk comment.pk %}">
{% csrf_token %}
<br><div class="buttons"><input type="submit" value="댓글삭제">
</div>
</form>
<a href="{% url 'MBTIAPP:comment_edit' post.pk comment.pk %}">수정하기</a>
{% endif %}
</li>
{% empty %}
<p>No 댓글~</p>
{% endfor %}
</ul>
-
아까 comment라는 변수에 정보들을 담았으므로, if문을 이용해, comments에 접근.
-
length를 이용해 comments 안에 담긴 댓글 개수를 추출.
-
for문을 돌려, comments에 담긴 댓글 수 만큼 반복.
-
댓글 내용 , 닉네임 , mbti 표기.
-
if문으로 들어가서, user가 댓글 작성자와 같을 경우, 댓글삭제와 수정하기 버튼이 보이게 함.
-
구글링하다보니, empty 라는 것도 있길래 써봄.
-
❗️ delete와 edit은 아까 말했듯이, post와 comment의 pk 둘다 필요로 하므로, 둘다 인자로 보내줘야함.
URL1 | URL2 | METHOD | 설명 |
---|---|---|---|
/main | GET | 메인 페이지 | |
/signup | POST | 회원가입 | |
/login | POST | 로그인 | |
/logout | POST | 로그아웃 | |
/form_errors | GET | 인증에러 | |
/mbti_test | POST | mbti검사페이지 | |
/result | GET | 검사결과페이지 | |
/postcreate | POST | 게시글 작성 | |
/blog | GET | 게시글 리스트 | |
/new | GET | 게시글 작성 페이지로 이동 | |
/int:pk | GET | 게시글 상세페이지 | |
/int:pk | /edit | POST | 게시글 수정페이지 |
/int:pk | /delete | POST | 게시글 삭제 |
/int:pk | /comment | POST | 댓글 작성 |
/int:post_pk/comment | /int:comment_pk/delete | POST | 댓글 삭제 |
/int:post_pk/comment | /int:comment_pk/edit | POST | 댓글 수정 |
그럴듯한 기능들을 직접 만들어보니, 내가 장고를 얼마나 얕게 알고 있었는지 깨닫게 되었고, 이 과제 하나로 엄청난 양의 공부를 하게 되었고, readme 적으면서 또 한번 정리하니 좋았다.
-
XSS 보호
=> XSS공격이란, 공격자가 악성 스크립트를 다른 사용자의 브라우저에 삽입하는 것을 말합니다.
그래서 , 장고의템플릿에는 대부분의 XSS공격으로 부터 보호할 수 있는 기능이 있기에, 특수한 상황을 제외하고는 안전하다고 합니다. -
SQL 주입 보호
=> SQL주입 공격은, 악의적 사용자가 데이터베이스에서 임의로 sql코드를 실행할 수 있는 공격유형입니다.
그래서, 장고는 쿼리 매개변수화를 사용해서 쿼리를 구성하여, SQL주입으로 부터 보호된다고 합니다. -
클릭재킹 보호
=> 클릭재킹 공격은, 악의적인 사이트가 다른 사이트를 프레임으로 감싸는 공격입니다. 이공격을 통해, 일반사용자가 대상 사이트에서 의도치않은 작업을 수행하도록 속일 수 잇다. 그래서 , 장고는 클릭재킹방지 기능을 브라우저에 제공합니다. -
여기까진 3개의 대표적인 장고의 내장된 기능들이고 , 개발을 하며 저희가 보안에 신경쓸 수 있는점은,
파이썬 코드가 웹서버의 루트 외부에 있어야되므로 확인해야합니다.
=> 외부에 있어야 코드가 일반 텍스트로 제공되거나 실수로 실행되는 사고를 막을 수 있습니다 -
또, 장고는 사용자 인증 요청을 조절하지 않기 때문에, 인증 시스템에 대한 공격으로 부터 보호하기 위해 다른 웹서버 인증 보안 모듈을 사용해야 한다고 합니다.
그래서 그 예시를 찾아봤는데 , 아파치 보안 모듈 , nginx 보안 모듈, HAProxy 모듈등을 이용한 예시가 존재합니다.
- 이 외에도 쉘로 DB를 조작할 수 있는 => python manage.py shell,
정적 파일들을 하나의 경로에 모아주는 => python manage.py collectstatic,
migration들을 보여주는 => python.manage.py showmigrations,
이외에도 더 존재하지만, 알아보니 좀 딥한 내용들이라 여기까지만 알아보았습니다.
-
저번시간에 준혁/승환님 팀에서 설명해 주셨는데,
-
Pk 는 primary key의 줄임말로, 기본키라고 부릅니다. 모델에서 장고는 따로 pk를 정의하지 않아도 id필드를 기본키로 사용합니다. 물론 따로 지정해줄 수 도 있습니다.
-
pk는 주로 url매개변수에 사용되는데, 해당 기본키에 해당하는 페이지를 호출함으로써, 키별로 다른 페이지를 호출할 수 있습니다
-
그래서 궁금해진게 결국 id랑 pk는 pk를 따로 바꿔주지 않는 이상 동일한데 왜 굳이 pk를 사용하여 url매개변수로 이용하나 싶어서 알아보았는데,
-
id를 사용해도 문제는 없지만, 장고 모델에 대한 일관성있는 코드를 위해 pk를 사용한다고 합니다.