Computer Science/BackEnd

점프 투 장고 #3-2 | 파이보 서비스 개발하기(회원가입, 모델 변경, 글쓴이 표시, 수정과 삭제)

토마토. 2022. 8. 22. 19:48

[참고] 3-06 회원가입 - 점프 투 장고 (wikidocs.net)

 

3-06 회원가입

`[완성 소스]` : [github.com/pahkey/jump2django/tree/3-06](https://github.com/pahkey/jump2django/tree ...

wikidocs.net

 

회원가입

- 회원가입 링크

- urls.py

- forms.py

- views.py

- signup.html

- 회원가입

 

파이보 사용자를 추가하는 회원가입 기능. 장고 django.contrib.auth 앱을 이용해서 구현할 수 있다. 

 

- 회원가입 링크

            <li>
                {%if not user.is_authenticated%}
                <a class="nav-link" href="{%url 'common:signup'%}">회원가입</a>
                {%endif%}
            </li>

navbar.html에 회원가입 링크를 삽입해준다. 회원가입 링크를 누르면, common/signup url로 연결된다.

 

- urls.py

from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

app_name = 'common'

urlpatterns = [
    path('login/', auth_views.LoginView.as_view(template_name='common/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('signup/', views.signup, name='signup'),
]

URL 매핑을 추가해준다. common/signup에 대응하는 signup/을 추가해준 것이다. 

 

- forms.py

from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class UserForm(UserCreationForm):
    email = forms.EmailField(label="이메일")
    class Meta:
        model = User
        fields = ("username", "password1", "password2", "email")

계정 생성 시 사용하는 UserForm 클래스

UserForm은 django.contrib.auth.forms 모듈에 UserCreationForm을 상속받아서 만들어준다. 

email 속성을 추가해주었다. username : 사용자 닉네임password1 : 비밀번호 입력password2 : 비밀번호 검증

 

- views.py

from django.contrib.auth import authenticate, login
from django.shortcuts import render, redirect
from common.forms import UserForm

# Create your views here.
def signup(request):
    if request.methd == "POST":
        form = UserForm(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('index')
    else:
        form = UserForm()
    return render(request, 'common/signup.html', {'form':form})

회원가입을 위한 함수

HTTP request 메소드가 POST인 경우에는,

데이터로 사용자를 생성한다.

- form.cleaned_data.get으로 username과 raw_password라는 폼 입력값을 각각 가져온다

- authenticate - 신규 사용자를 생성하고 자동 로그인되도록. 사용자 인증을 담당한다. (django.contrib.auth.authenticate, 사용자 인증)

- login - 로그인을 담당한다. (django.contrib.auth.login)

* authenticate, login 모두 django.contrib.auth 모듈의 함수다. 

 

HTTP request 메소드가 GET인 경우에는, 

회원가입 화면을 보여준다. 

 

- signup.html

{%extends "base.html"%}
{%block content%}
<div class="container my-3">
    <form method="post" action="{%url 'common:signup'%}">
        {%csrf_token%}
        {%include "form_errors.html"%}
        <div class="mb-3">
            <label for="username">사용자 이름</label>
            <input type="text" class="form-control" name="username" id="username"
                value="{{form.username.value|default_if_none:''}}">
        </div>
        <div class="mb-3">
            <label for="password1">비밀번호</label>
            <input type="password" class="form-control" name="password1" id="password1"
                value="{{form.password1.value|dafault_if_none:''}}">
        </div>
        <div class="mb-3">
            <label for="password2">비밀번호 확인</label>
            <input type="password" class="form-control" name="password2" id="password2"
                value="{{form.password2.value|default_if_none:''}}">
        </div>
        <div class="mb-3">
            <label for="email">이메일</label>
            <input type="text" class="form-control" name="email" id="email"
                value="{{form.email.value|default_if_none:''}}">
        </div>
        <button type="submit" class="btn btn-primary">생성하기</button>
    </form>
</div>
{%endblock%}

form 안에 div 네 개가 들어있다. 그 전에 form_errors.html을 포함시켜서 에러 처리를 해준다. 

div는 사용자 이름, 비밀번호, 비밀번호 확인, 이메일 항목을 만들어주고, 

button으로 생성하기를 만들어준다. 

 

- 회원가입

모델 변경

누가 글을 썼는지 알려주는 글쓴이 항목. 

Question, Answer 모델에 author 속성을 추가해주자.

 

- Question 속성 추가

- Answer 속성 추가

- author 저장

- 로그인이 필요한 함수

- next

- disabled

 

- Question 속성 추가

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)

User 모델을 ForeignKey로 적용

on_delete=models.CASCADE는 User이 삭제되면, 질문을 모두 삭제하라는 의미. 

 

모델이 변경되었으므로 makemigration, migrate로 데이터베이스를 변경해주자. 

 

$  python manage.py makemigrations

 

- Answer 속성 추가

class Answer(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)

 

이 다음에 다시

$  python manage.py makemigrations

 

마지막으로 migration 명령을 수행해준다. 

 

- author 저장

<views.py>

def answer_create(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.method=="POST":
        form = AnswerForm(request.POST)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.author = request.user
            answer.create_date = timezone.now()
            answer.question = question
            answer.save()
            return redirect('pybo:detail', question_id=question_id)

    else:
        return HttpResponseNotAllowed('Only POST is possible.')
    context = {'question':question, 'form':form}
    return render(request, 'pybo/question_detail.html', context)

views.py에 answer.author = request.user으로

author 정보를 저장해준다. 

 

 

def question_create(request):
    if request.method == 'POST':
        form = QuestionForm(request.POST)
        if form.is_valid():
            question = form.save(commit=False)
            question.author = request.user
            question.create_date = timezone.now()
            question.save()
            return redirect('pybo:index')
    else:
        form = QuestionForm()
    context = {'form':form}
    return render(request, 'pybo/question_form.html', context)

question 모델에도 마찬가지로 로그인된 계정을 저장해준다. 

 

- 로그인이 필요한 함수

@login_required으로 로그인이 필요한 함수를 의미하도록 한다. 

@login_required(login_url='common:login')

 

- next

        <input type="hidden" name="next" value="{{next}}">

next 항목의 URL로 이동할 수 있게 된다. 

 

- disabled

로그아웃 상태에서는 아예 답변 작성을 못하게 막을 수 있다. 

 

{%extends 'base.html'%}
{%block content%}
<div class="container my-3">
    <!--질문-->
    <h2 class="border-bottom py-2">{{question.subject}}</h2>
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space: pre-line;">{{question.content}}</div>
            <div class="d-flex justify-content-end">
                <div class="badge bg-light text-dark p-2">
                    {{question.create_date}}
                </div>
            </div>
        </div>
    </div>
    <!--답변 -->
    <h5 class="border-bottem my-3 py-2">{{question.answer_set.count}}개의 답변이 있습니다. </h5>
    {%for answer in question.answer_set.all%}
    <div class="card my-3">
        <div class="card-body">
            <div class="card-text" style="white-space:pre-line;">{{answer.content}}</div>
            <div class="d-flex justify-content-end">
                <div class="badge bg-light text-dark p-2">
                    {{answer.create_date}}
                </div>
            </div>
        </div>
    </div>
    {%endfor%}
    <form action="{% url 'pybo:answer_create' question.id %}" method="post" class="my-3">
        {%csrf_token%}
        {%if form.errors%}
        <div class="alert alert-danger" role="alert">
            {%for field in form%}
            {%if field.errors%}
            <div>
                <strong>{{field.label}}</strong>
                {{field.errors}}
            </div>
            {%endif%}
            {%endfor%}
        </div>
        {%endif%}
        <div class="mb-3">
            <label for="content" class="form-label">답변내용</label>
            <textarea {%if not user.is_authenticated%}disabled{%endif%}
                name="content" id="content" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="답변 등록" class="btn btn-primary">
    </form>
</div>

{%endblock%}

이제 이렇게 disabled 되어서

이렇게 disabled되어있다. 

 

글쓴이 표시

글쓴이를 표시하는 것

- 질문 목록

- 질문 상세

 

- 질문 목록

        <thread>
            <tr class="text-center table-dark">
                <th>번호</th>
                <th style="width:50%">제목</th>
                <th>글쓴이</th>
                <th>작성일시</th>
            </tr>
        </thead>

글쓴이 열을 추가해준다. 

 

            {%if question_list%}
            {%for question in question_list%}
            <tr class="text-center">
                <td>
                    <!-- 번호 -->
                    {{question_list.paginator.count|sub:question_list.start_index|sub:forloop.counter0|add:1}}
                </td>
                <td class="text-start">
                    <a href="{%url 'pybo:detail' question.id %}">{{question.subject}}</a>
                    {%if question.answer_set.count > 0%}
                    <span class="text-danger small mx-2">{{question.answer_set.count}}</span>
                    {%endif%}
                </td>
                <td>{{question.author.username}}</td>
                <td>{{question.create_date}}</td>
            </tr>
            {%endfor%}
            {%else%}
            <tr>
                <td colspan="4">질문이 없습니다.</td>
            </tr>
            {%endif%}

.

 

이렇게 수정되었다.

 

- 질문 상세

 

수정과 삭제

수정, 삭제 기능을 추가하자! 

- 질문 수정

- 질문 삭제

- 답변 수정

- 답변 삭제

- 수정일시 표시하기

 

- 수정 일시

from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class Question(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    subject = models.CharField(max_length=200)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)
    
    def __str__(self):
        return self.subject

class Answer(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    content = models.TextField()
    create_date = models.DateTimeField()
    modify_date = models.DateTimeField(null=True, blank=True)

Question, Answer 모델에 수정 일시(modify_date)를 추가한다.

null=True : null을 허용한다. 

blank=True : 입력 데이터를 검증할 때 값이 없어도 된다. 

 

모델 수정사항을 반영하기 위해 makemigrations 수행

$ python manage.py makemigrations

그 다음 migrate까지

$ python manage.py migrate

 

- 질문 수정

ㄴ 질문 수정 버튼

<question_detail.html>으로 질문 수정 버튼을 추가해준다. 

request.user == question.author 일 때만, 버튼에 노출되도록 했다.

            <div class="my-3">
                {%if request.user == question.author %}
                <a href="{% url 'pybo:question_modify' question.id %}"
                    class="btn btn-sm btn-outline-secondary">수정</a>
                {%endif%}
            </div>

 

ㄴ urls.py

추가된 URL에 맞게(pybo:question_modify) URL 매핑 규칙을 추가해주자.

from django.urls import path
from . import views

app_name = 'pybo'

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('question/create/', views.question_create, name='question_create'),
    path('answer/create/<int:question_id>/', views.answer_create, name='answer_create')
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
]

 

ㄴ views.py

views.question_modify 함수를 만들어주자. 

@login_required(login_url='common:login')
def question_modify(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '수정 권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    if request.method == 'POST':
        form = QuestionForm(request.POST, instance=question)
        if form.is_valid():
            question = form.save(commit=False)
            question.modify_date = timezone.now()
            question.save()
            return redirect('pybo:detail',question_id=question.id)
    else:
        form = QuestionForm(instance=question)
    context = {'form':form}
    return render(request, 'pybo/question_form.html', context)

question_modify 함수만약 request.user랑 question.author랑 다르다면, 수정 권한이 없다는 에러를 만든다. 이때 messages 모듈을 사용한다. messages는 넌필드 오류에 사용된다. 만약 request.method가 POST면, 질문 수정 화면에서 저장하기 버튼을 클릭한 것이다. 만약 request.method가 GET이면, 수정 버튼을 클릭해서 질문 수정 화면을 보여주는 것이다. 

 

ㄴ 오류 표시

{%extends 'base.html'%}
{%block content%}
<div class="container my-3">
    <!--messages 표시 -->
    {%if messages%}
    <div class="alert alert-danger my-3" role="alert">
    {%for message in messages %}
        <strong>{{message.tags}}</strong>
        <ul><li>{{message.message}}</li></ul>
    {%endfor%}
    </div>
    {%endif%}

 

ㄴ 질문 수정 확인

 

- 질문 삭제

                <a href="javascript:void(0)" class="delete btn btn-sm btn-outline-secondary"
                    data-uri="{% url 'pybo:question_delete' question.id %}">삭제</a>

javascript:void(0)은 링크를 클릭해도 동작을 하지 않는다.

삭제를 실행할 URL이 data-uri이고, (this.dataset.uri 로 값을 얻을 수 있다)

삭제 버튼을 눌리는 이벤트를 확인하도록 class 속성에 delete를 추가한다.

 

ㄴ 자바스크립트

삭제 버튼을 눌렀을 때 확인창을 호출하기 위함

        <script src="{%static 'bootstrap.min.js' %}"></script>
        <!--자바스크립트 start -->
        {%block script%}
        {%endblock%}
        <!--자바스크립트 end-->
    </body>
</html>

자바스크립트를 들어갈 수 있도록 script 블록을 구현

 

ㄴ question_detail.html

{%endblock%}
{%block script%}
<script type='text/javascript'>
const delete_elements = document.getElementsByClassName("delete");
Array.from(delete_elements).forEach(function(element) {
    element.addEventListener('click', function(){
        if (confirm("정말로 삭제하시겠습니까?")){
            location.href = this.dataset.uri;
        };
    });
});
</script>
{%endblock%}

 

ㄴ urls.py

from django.urls import path
from . import views

app_name = 'pybo'

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('question/create/', views.question_create, name='question_create'),
    path('answer/create/<int:question_id>/', views.answer_create, name='answer_create'),
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
    path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),
]

pybo:question_delete에 맞는 URL 매핑 규칙

 

ㄴ views.py

@login_required(login_url='common:login')
def question_delete(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    if request.user != question.author:
        messages.error(request, '삭제권한이 없습니다')
        return redirect('pybo:detail', question_id=question.id)
    question.delete()
    return redirect('pybo:index')

 

- 답변 수정

ㄴ 답변 수정 버튼

            <div class="my-3">
                {%if request.user == answer.author%}
                <a href="{%url 'pybo:answer_modify' answer.id%}"
                    class="btn btn-sm btn-outline-secondary">수정</a>
                {%endif%}
            </div>

답변 수정 버튼을 추가하기

 

ㄴ urls.py

from django.urls import path
from . import views

app_name = 'pybo'

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('question/create/', views.question_create, name='question_create'),
    path('answer/create/<int:question_id>/', views.answer_create, name='answer_create'),
    path('question/modify/<int:question_id>/', views.question_modify, name='question_modify'),
    path('question/delete/<int:question_id>/', views.question_delete, name='question_delete'),
    path('answer/modify/<int:answer_id>/', views.answer_modify, name='answer_modify'),
]

이 끝에 url 매핑 규칙을 추가해준다.

modify/int:answer_id

 

ㄴ views.py

@login_required(login_url='common:login')
def answer_modify(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '수정권한이 없습니다')
        return redirect('pybo:detail',question_id=answer.question.id)
    if request.method == "POST":
        form = AnswerForm(request.POST, instance=answer)
        if form.is_valid():
            answer = form.save(commit=False)
            answer.modify_date = timezone.now()
            answer.save()
            return redirect('pybo:detail', question_id=answer.question.id)
    else:
        form = AnswerForm(instance=answer)
    context = {'answer':answer, 'form':form}
    return render(request, 'pybo/answer_form.html', context)

 

ㄴ 답변 수정 폼

{%extends 'base.html'%}
{%block content%}
<div class="container my-3">
    <form method="post">
        {%csrf_token%}
        {%include "form_errors.html"%}
        <div class="mb-3">
            <label for="content" class="form-label">답변내용</label>
            <textarea class="form-control" name="content" id="content"
                rows="10">{{form.content.value|default_if_none:''}}</textarea>
        </div>
        <button type="submit" class="btn btn-primary">저장하기</button>
    </form>
</div>
{%endblock%}

 

ㄴ 답변 수정 확인

 

ㄴ 답변 삭제 버튼

                <a href="#" class="delete btn btn-sm btn-outline-secondary"
                    date-uri="{%url 'pybo:answer_delete' answer.id %}">삭제</a>

 

ㄴ urls.py

    path('answer/delete/<int:answer_id>/', views.answer_delete, name='answer_delete'),

pybo:answer_delete url 매핑을 추가해준다. 

 

ㄴ views.py

@login_required(login_url='common:login')
def answer_delete(request, answer_id):
    answer = get_object_or_404(Answer, pk=answer_id)
    if request.user != answer.author:
        messages.error(request, '삭제권한이 없습니다')
    else:
        answer.delete()
    return redirect('pybo:detail', question_id=answer.question.id)

 

- 수정일시 표시하기

                {%if question.modify_date%}
                <div class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div>{{question.modify_date}}</div>
                </div>
                {%endif%}
                {%if answer.modify_date%}
                <div class="badge bg-light text-dark p-2 text-start mx-3">
                    <div class="mb-2">modified at</div>
                    <div>{{answer.modify_date}}</div>
                </div>
                {%endif%}