본문 바로가기
FrameWork/pinterest clone

7. accountapp 만들기

by mansoorrr 2023. 7. 20.

app은 기본적으로 CRUD로 이루어진다.

 

accountapp은 계정관련된 app으로 여기서 CRUD 내용은 다음과 같다.

 

Create: 계정 만들기(회원가입)

Read(Detail): 계정확인

Update: 계정 수정

Delete: 계정삭제

 

<CRUD를 위해 django에서 기본적으로 제공하는 View>

Create: CreateView

Read: DetailView

Update: UpdateView

Delete: DeleteView

 

모든 앱 만드는 순서: 해당앱 models.py에  class베이스 모델 설정 > migrations > urls.py에 route설정 > 해당앱 views.py에 class베이스 view설정

 

account(계정)의 경우는 특수한 경우로 django에서 User모델을 제공한다.

User모델은 AbsractUser를 상속받고 반환하는 객체로는 username, first_name, last_name, email등을 갖는다.

class User(AbstractUser):
    class Meta(AbstractUser.Meta):
        swappable = "AUTH_USER_MODEL"

class AbstractUser(AbstractBaseUser, PermissionsMixin):
    username_validator = UnicodeUsernameValidator()
    username = models.CharField(
        _("username"),
        max_length=150,
        unique=True,
        help_text=_(
            "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
        ),
        validators=[username_validator],
        error_messages={
            "unique": _("A user with that username already exists."),
        },
    )
    first_name = models.CharField(_("first name"), max_length=150, blank=True)
    last_name = models.CharField(_("last name"), max_length=150, blank=True)
    email = models.EmailField(_("email address"), blank=True)
    is_staff = models.BooleanField(
        _("staff status"),
        default=False,
        help_text=_("Designates whether the user can log into this admin site."),
    )
    is_active = models.BooleanField(
        _("active"),
        default=True,
        help_text=_(
            "Designates whether this user should be treated as active. "
            "Unselect this instead of deleting accountapp."
        ),
    )
    date_joined = models.DateTimeField(_("date joined"), default=timezone.now)

    objects = UserManager()

    EMAIL_FIELD = "email"
    USERNAME_FIELD = "username"
    REQUIRED_FIELDS = ["email"]

python manage.py migrate를 통해 기본으로 갖춰져야 할 모델들을 migrate한다.

아래와같이 여러가지 모델들이 migrate된다.

No changes detected
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

 

1. Create

  <urls.py>

  create/로 분기하면, AccountCreateView를 실행한다, 실행하는 이름은 create다 라는 뜻

  클래스 베이스 뷰를 이용하기 때문에 as_view()를 입력해 줘야 한다.

from accountapp.views import home, AccountCreateView
from django.urls import path

app_name = 'accountapp'

urlpatterns = [
    path('home/', home, name='home'),
    path('create/', AccountCreateView.as_view(), name="create")
]

 

  <views.py>  

from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import CreateView


# Create your views here.
def home(request):
    return render(request, 'accountapp/home.html')

class AccountCreateView(CreateView): 
    model = User #User라는 모델을 사용한다
    form_class = UserCreationForm #User를 Create할 때 사용할 폼
    template_name = 'accountapp/create.html' # 회원가입할때 사용할 템플릿
    success_url = reverse_lazy('accountapp:home') # 회원가입후 돌아갈 라우팅

 

<create.html>

base.html을 그대로 가져와 쓸 것이고 {% block content %} 부분을 변경한다는 뜻.

views.py에서 form_class를 지정해 줬기 때문에 해당 html에는 {{ form }}으로 적어주면 자동 반영된다.

{% extends 'base.html' %}

{% block content %}

<div>
  {{ form }}
</div>

{% endblock %}

 

<실행화면>

<style 적용>

  1) 부트스트랩 적용 및 css 추가

.account_create {
    text-align:center;
    max-width: 400px;
    margin:4rem auto;
}

  2) create.html 수정

  form: accountapp:create(urls.py -> create/)로 데이터를 보낸다. 보내는 방식은 post

  post로 숨겨서 정보를 보낼 시 {% csrf_token %}을 걸어줘야함

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}

<div class="account_create">
  <form action="{% url 'accountapp:create' %}" method="post">
    {% csrf_token %}
    {% bootstrap_form form %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

<완성된 화면>

<로그인할 수 있는 링크 만들기>

현재는 accounts/create/ 라는 url을 입력해야 현재 페이지로 들어가진다.

accounts/home 화면에서 login할 수 있는 링크를 만들어 accounts/create로 향할 수 있게 세팅

이를위해 header.html수정

 

<header.html>

<div style="margin:2rem 0">
  <div class="pinterest_logo mb-3">
    <h2>Pinterest Clone</h2>
  </div>
  <div class="pinterest_header_btn">
    <span>nav1</span> |
    <span>nav2</span> |
    <span>nav3</span> |
    <span>nav4</span> |
    <a href="{% url 'accountapp:create' %}">
      <span>Sign Up</span>
    </a>
  </div>
</div>

 

2. login / logout

accountapp은 특수한 경우로 회원가입을 했으면 로그인을 해야한다. 그리고 logout도 해야한다.

django는 이를 위한 LoginView와 LogoutView를 제공한다.

 

LoginView와 LogoutView는 뷰 실행 이후 이동할 페이지를 next로(settings.py에) 지정해 줘야 한다.

LOGIN_REDIRECT_URL = reverse_lazy('accountapp:home') 
LOGOUT_REDIRECT_URL = reverse_lazy('accountapp:login')

<urls.py>

LoginView / LogoutView 설정: views.py로 가지 않고 urls.py에서 직접 입력해서 끝냄

from django.contrib.auth.views import LoginView, LogoutView

from accountapp.views import home, AccountCreateView
from django.urls import path

app_name = 'accountapp'

urlpatterns = [
    path('home/', home, name='home'),
    path('create/', AccountCreateView.as_view(), name="create"),
    path('login/', LoginView.as_view(template_name="accountapp/login.html"), name="login"),
    path('logout/', LogoutView.as_view(), name="logout.html"),
]

 

<login.html>

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}

<div class="account_create">
  <div>
    <h4>Login</h4>
  </div>
  <form action="{% url 'accountapp:login' %}" method="post">
    {% csrf_token %}
    {% bootstrap_form form %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

<login과 logout할 수 있도록 링크 설정: header.html>

  {% if user.is_authenticated %}를 통해 로그인 되었을때와 안되어있을때 구분

  login하게 되면 django에서는 기존 url이후 '?next=' 를 통해 다음 페이지로 갈 수 있게 구분한다.

  이는 settings.py에 설정했던 것과는 별개로 어떤 페이지에서건 로그인 했을때 돌아갈 페이지로 향하는 것을 의미한다.

  따라서 href속성에 ?next={{ request.path }}를 추가적으로 입력해주어 로그인 후 로그인을 클릭한 시점의 페이지로 돌아갈 수 있게 한다.

 

<div style="margin:2rem 0">
  <div class="pinterest_logo mb-3">
    <h2>Pinterest Clone</h2>
  </div>
  <div class="pinterest_header_btn">
    <span>nav1</span> |
    <span>nav2</span> |
    <span>nav3</span> |
    <span>nav4</span> |
    {% if user.is_authenticated %}
    <a href="{% url 'accountapp:logout' %}?next={{ request.path }}">
      <span>Logout</span>
    </a>

    {% else %}    
    <a href="{% url 'accountapp:login' %}?next={{ request.path }}">
      <span>Login</span>
    </a>
    <a href="{% url 'accountapp:create' %}">
      <span>Sign Up</span>
    </a>
    {% endif %}
  </div>
</div>

 

 

4. Detail

로그인 후 개인의 정보를 확인할 수 있는 app을 생성한다. 생성과정은 CreateView와 동일하다.

django에서 제공하는 DetailView를 사용한다.

 

<urls.py>

DetailView는 pk를 인자로 받게 되어있다. 따라서 url지정시 <int:pk>를 작성해 주어야 한다.

from django.contrib.auth.views import LoginView, LogoutView

from accountapp.views import home, AccountCreateView, AccountDetailView
from django.urls import path

app_name = 'accountapp'

urlpatterns = [
    path('home/', home, name='home'),
    path('create/', AccountCreateView.as_view(), name="create"),
    path('login/', LoginView.as_view(template_name="accountapp/login.html"), name="login"),
    path('logout/', LogoutView.as_view(), name="logout"),
    path('detail/<int:pk>', AccountDetailView.as_view(), name='detail'),
]

 

<views.py>

단지 개인의 페이지를 보는 것이기 때문에 success_url, form_class는 없다.

class AccountDetailView(DetailView):
    model = User
    context_object_name = 'target_user'
    template_name = 'accountapp/detail.html'

 

<detail.html>

username만 가져와서 보여줄 것이다.

{% extends 'base.html' %}

{% block content %}

<div class="account_create">
  {{ target_user.username }}
</div>

{% endblock %}

<결과화면>

<header.html>

마이페이지를 확인할 수 있도록 연결해준다.

a태그를 통해 만들어주고 url에 pk를 넣게 되어있으므로 pk=user.pk를 설정해준다.

user.pk는 user모델의 pk이다. pk는 별도로 지정하지 않으면 생성한 순서 번호로 만들어진다.

 

{% if user.is_authenticated %}
<a href="{% url 'accountapp:detail' pk=user.pk%}"> #detail로 향하는 버튼 만들기
  <span>MyPage</span> |
</a>
<a href="{% url 'accountapp:logout' %}?next={{ request.path }}">
  <span>Logout</span>
</a>

{% else %}
<a href="{% url 'accountapp:login' %}?next={{ request.path }}">
  <span>Login</span>
</a>
<a href="{% url 'accountapp:create' %}">
  <span>Sign Up</span>
</a>

{% endif %}

 

로그인, 로그아웃, 마이페이지 들어가서 보는거 다 잘된다.

 

5. Update

내 계정 정보를 수정할 수 있는 UpdateView를 만든다.

 

<urls.py>

DetailView와 마찬가지로 pk를 인자로 받는다.

from django.contrib.auth.views import LoginView, LogoutView

from accountapp.views import home, AccountCreateView, AccountDetailView, AccountUpdateView
from django.urls import path

app_name = 'accountapp'

urlpatterns = [
    path('home/', home, name='home'),
    path('create/', AccountCreateView.as_view(), name="create"),
    path('login/', LoginView.as_view(template_name="accountapp/login.html"), name="login"),
    path('logout/', LogoutView.as_view(), name="logout"),
    path('detail/<int:pk>', AccountDetailView.as_view(), name='detail'),
    path('update/<int:pk>', AccountUpdateView.as_view(), name='update'),
]

 

<views.py>

CreateView와 동일하게 작성한다.

class AccountUpdateView(UpdateView):
    model = User
    form_class = UserCreationForm
    context_object_name = 'target_user'
    template_name = 'accountapp/update.html'
    success_url = reverse_lazy('accountapp:home')

 

<update.html>

create.html과 동일하고 내용만 조금 변경해 준다.

{% extends 'base.html' %}
{% load bootstrap4 %}

{% block content %}

<div class="account_create">
  <div>
    <h4>
      Update Account # 내용 변경
    </h4>
  </div>
  <form action="{% url 'accountapp:update' pk=target_user.pk%}" method="post"> # 내용변경
    {% csrf_token %}
    {% bootstrap_form form %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

 

<detail.html>

계정 수정은 mypage를 통해 수정하는 페이지로 들어가야 한다. mypage에 수정하는 곳으로 들어갈 수 있게 버튼을 생성한다. mypage에 접속한 유저와 로그인한 유저가 같을때만 수정버튼을 누를 수 있도록 한다.

{% extends 'base.html' %}

{% block content %}

<div class="account_create">
  
  {{ target_user.username }}
  
  {% if target_user == user %} #이페이지의 유저와 로그인한 유저가 같을때
  <div>
    <a href="{% url 'accountapp:update' pk=target_user.pk %}" class="btn btn-dark rounded-pill mt-3">
      Change Account
    </a>
  </div>
  {% endif %}
  
</div>

{% endblock %}

 

<접속시 화면>

 

<수정할점>

  • form 변경: 계정 수정시 id를 변경할 수 있도록 되어있다. 계정아이디는 수정할 수 없도록 변경한다.
  • success_url 변경: 제출시 home으로 돌아간다. 상식적으로 mypage로 다시 돌아가는 것이 맞다.
  • decorator 설정: accounts/update/pk로 직접 들어가면 다른사람의 정보도 수정할 수 있게 되어있다.

 

<views.py>수정: success_url변경 및 form 적용

class AccountUpdateView(UpdateView):
    model = User
    form_class = AccountUpdateForm #forms.py 생성
    context_object_name = 'target_user'
    template_name = 'accountapp/update.html'
    success_url = reverse_lazy('accountapp:home')

    def get_success_url(self): # 돌아갈 url설정
        return reverse('accountapp:detail', kwargs={'pk': self.object.pk})

 

<forms.py>: form 변경

form을 새로 만들어준다. 원래 UserCreationForm에서는 username, password1, password2 세가지 변수를 받게 되어있다. class Meta를 통해 username은 아예 나타나지 않도록 한다.

강의에서는 user_name자체를 disabled처리하여 block처리 하지만  나는 애초에 나타나지 않는것이 직관적이라고 생각하여 이렇게 진행하였다. 

class AccountUpdateForm(UserCreationForm):
    class Meta:
        model = User
        fields = ['password1', 'password2']

 

<수정 후 화면>

username을 수정하는 것이 사라지고 제출 후에도 mypage로 돌아가는 것을 확인할 수 있다.

데코레이터를 설정하기 전에 상황을 파악한다.

 

문제: url로 직접 접속하면 다른사람의 password를 변경할 수 있다.

해결방안

  - 로그인 되어 있으면 직접 접속 접속 가능하다.

  - 직접 접속을 요청한 유저가 모델 유저와 같으면 접속 가능하다.

<views.py> 를 변경하고 확인한다.

class AccountUpdateView(UpdateView):
    model = User
    form_class = AccountUpdateForm
    context_object_name = 'target_user'
    template_name = 'accountapp/update.html'
    success_url = reverse_lazy('accountapp:home')

    def get_success_url(self):
        return reverse('accountapp:detail', kwargs={'pk': self.object.pk})


	#----------------인증과정 추가
    def get(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().get(*args, **kwargs)
        else:
            return HttpResponseForbidden()
    def post(self, *args, **kwargs):
        if self.request.user.is_authenticated and self.get_object() == self.request.user:
            return super().get(*args, **kwargs)
        else:
            return HttpResponseForbidden()

다른 사람의 pk로 접근하면 엑세스 거부됐다고 나온다!

<decorators.py> 데코레이터 설정

위에서 추가했던 인증과정을 데코레이터로 만들어 적용한다. decorators.py파일을 추가한다.

위의 인증과정에서 self.request.user.is_authenticated의 경우 django에서 제공하는 login_required로 대체 가능하다. 그렇다면 self.get_object() == request.user를 적용할 수 있는 데코레이터를 만들어야 한다.

 

from django.contrib.auth.models import User
from django.http import HttpResponseForbidden


def account_ownership_required(func):
    def decorated(request, *args, **kwargs):
        if User.objects.get(pk=kwargs['pk']) == request.user:
            return func(*args, **kwargs)
        return HttpResponseForbidden()
    return decorated

 

<views.py>수정

클래스에 데코레이터를 제공하기 위해서는 @method_decorator를 사용해야 한다.

login_required와 위에서 만든 account_ownership_required 두개 함수를 데코레이터로 적용해야 하므로 has_ownership이라는 변수를 만들어 주고 변수자체를 method_decorator안에 넣어주면 두개 모두 적용된다.

has_ownership = [
    login_required,
    account_ownership_required,
]

@method_decorator(has_ownership, 'get')
@method_decorator(has_ownership, 'post')
class AccountUpdateView(UpdateView):
    model = User
    form_class = AccountUpdateForm
    context_object_name = 'target_user'
    template_name = 'accountapp/update.html'
    success_url = reverse_lazy('accountapp:home')

 

6. Delete

마지막으로 회원 탈퇴를 위한 DeleteView를 만든다.

 

<urls.py>

from django.contrib.auth.views import LoginView, LogoutView

from accountapp.views import home, AccountCreateView, AccountDetailView, AccountUpdateView, AccountDeleteView
from django.urls import path

app_name = 'accountapp'

urlpatterns = [
    path('home/', home, name='home'),
    path('create/', AccountCreateView.as_view(), name="create"),
    path('login/', LoginView.as_view(template_name="accountapp/login.html"), name="login"),
    path('logout/', LogoutView.as_view(), name="logout"),
    path('detail/<int:pk>', AccountDetailView.as_view(), name='detail'),
    path('update/<int:pk>', AccountUpdateView.as_view(), name='update'),
    path('delete/<int:pk>', AccountDeleteView.as_view(), name='delete'),
]

 

<views.py>

@method_decorator(has_ownership, 'get')
@method_decorator(has_ownership, 'post')
class AccountDeleteView(DeleteView):
    model = User
    context_object_name = 'target_user'
    template_name = 'accountapp/delete.html'
    success_url = reverse_lazy('accountapp:login')

<delete.html>

{% extends 'base.html' %}

{% block content %}

<div class="account_create">
  <div>
    <h4>Quit: {{ target_user.username }}</h4>
  </div>
  <form action="{% url 'accountapp:delete' pk=target_user.pk %}" method="post">
    {% csrf_token %}
    <input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
  </form>
</div>

{% endblock %}

<detail.html>

{% extends 'base.html' %}

{% block content %}

<div class="account_create">
  
  {{ target_user.username }}
  
  {% if target_user == user %} 
  <div>
    <a href="{% url 'accountapp:update' pk=target_user.pk %}" class="btn btn-dark rounded-pill mt-3">
      Change
    </a>
    #요기! 추가
    <a href="{% url 'accountapp:delete' pk=target_user.pk %}" class="btn btn-danger rounded-pill mt-3">
      Quit
    </a>
  </div>
  {% endif %}
  
</div>

{% endblock %}

 

데코레이터까지 추가하면 잘 동작하는 것을 볼 수 있다.

'FrameWork > pinterest clone' 카테고리의 다른 글

9. articleapp만들기  (0) 2023.07.22
8. profileapp 만들기  (0) 2023.07.22
6. 기초 스타일링(2)  (0) 2023.07.19
5. 기초 스타일링(1)  (0) 2023.07.19
4. 뼈대만들기2  (0) 2023.07.17