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 |