실용주의 프로그래머님의 인프런 강의를 듣고 작성하였습니다.
출처: https://www.inflearn.com/course/%EC%9E%A5%EA%B3%A0-%ED%95%80%ED%84%B0%EB%A0%88%EC%8A%A4%ED%8A%B8/lecture/62871?tab=note&speed=1.25
detail 페이지에 들어가면 아이디가 그대로 노출되고 있다. 보안에 취약한 부분이다. 이것을 닉네임으로 바꿔줄 것이고, 상태 메세지, 이미지를 묶어서 프로필 앱을 따로 만들어준다.
이 profile에는 delete 기능을 만들지 않을 것이다. accountapp과 1:1로 연동되어 있기 때문에 accountapp에서 delete되면 같이 delete 되도록 할 것이다. 또한 detail도 따로 필요하지 않기 때문에 만들지 않을 것이다.
profileapp 생성
python manage.py startapp profileapp
leebook/setting.py에서 app을 추가해준다.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrap4',
'accountapp',
'profileapp',
]
또 leebook/urls.py에서 profileapp으로 향하는 경로도 라우팅 해준다.
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accountapp.urls')), # /를 꼭 적어야 주소창에서 그 이후의 주소들도 제대로 인식된다.
path('profiles/', include('profileapp.urls'))
]
profileapp에서 usls.py 생성
app_name = 'profileapp'
urlpatterns= [
]
데이터를 담고자 하는 모델을 만들어줘야 한다. db와 연동하기 위해.
models.py
from django.contrib.auth.models import User
from django.db import models
# Create your models here.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
image = models.ImageField(upload_to='profile/', null=True)
nickname = models.CharField(max_length=20, unique=True, null=True)
message = models.CharField(max_length=100, null=True)
여기에서 user와 profile을 1대1 대응 시키고 user가 사라지면 같이 전부 사라질 수 있도록 설정해줬다.
또한 related_name을 정해줘서 나중에 request.user.profile 명령어로 바로 profile로 연결할 수 있도록 이름을 정해주는 것이다.
또한 이미지 필드에서 upload_to로 지정한 곳으로 이미지를 저장한다. 또한 없어도 된다는 설정까지 해준다.
닉네임은 최대 20, 중복되지 않음, 없어도 됨. 으로 설정해준다.
Accountapp에서는 Form을 기본 제공해주기 때문에 만들지 않았는데 profile은 제공해주지 않기 때문에 만들어 주어야 한다.
ModelForm: 기존에 있었던 모델 그대로 Form으로 변환해주는 기능
기존 모델이 있으면 따로 설정 필요 없이 ModelForm을 상속받은 후 어떤 model, 어떤 fields를 사용할 것인지 설정해주면 Form으로 면한다.
from django.forms import ModelForm
from profileapp.models import Profile
class ProfileCreationForm(ModelForm):
class Meta:
model = Profile
fields = {'image', 'nickname', 'message'}
DB에 만든 Form을 반영시켜준다.
python manage.py makemigrations
python manage.py migrate
이제 본격적으로 view를 작성한다.
from django.shortcuts import render
# Create your views here.
from django.urls import reverse_lazy
from django.views.generic import CreateView
from profileapp.forms import ProfileCreationForm
from profileapp.models import Profile
class ProfileCreateView(CreateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/create.html'
profileapp/templates/profileapp/create.html 생성
accountapp의 create.html파일과 거의 똑같고 form url을 바꿔주어야 한다.
또한 enctype을 명시해주어야 한다. 그래야 우리가 보낸 이미지를 받을 수 있다.
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align: center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>
Profile Create
</h4>
</div>
<form action="{% url 'profileapp:create' %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
이제 urls.py에서 경로를 추가해준다.
from django.urls import path
from profileapp.views import ProfileCreateView
app_name = 'profileapp'
urlpatterns= [
path('create/', ProfileCreateView.as_view(), name='create'),
]
이제 이 경로를 메인 페이지에서 갈 수 있도록 링크를 걸어준다.
accountapp/templates/accountapp/detail.html
{% if target_user.profile %}
<h2 style="font-family: 'NanumSquareB'">
{{ target_user.profile.nickname }}
</h2>
{% else %}
<a href="{% url 'profileapp:create' %}">
<h2 style="font-family: 'NanumSquareB'">
Create Profile
</h2>
</a>
{% endif %}
이렇게 하고 profile을 만들면 다음과 같은 오류가 생긴다.
생각해보면 아까 model에서 user라는 필드가 분명 있었는데 form에서는 image, nicknmae, message만 설정해줬다. 다른 사람이 profile을 건들지 못하게 user를 서버 내에서 구현한다. form_valid라는 것을 사용한다.
class ProfileCreateView(CreateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/create.html'
def form_valid(self, form):
temp_profile = form.save(commit=False) # 임시데이터 저장
temp_profile.user = self.request.user
temp_profile.save()
return super().form_valid(form)
views.py
class ProfileUpdateView(UpdateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/update.html'
urls.py에서 path 추가
path('update/', ProfileUpdateView.as_view(), name='update'),
profileapp/templates/profileapp/update.html 생성
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<div style="text-align: center; max-width: 500px; margin: 4rem auto">
<div class="mb-4">
<h4>
Update Profile
</h4>
</div>
<form action="{% url 'profileapp:update' pk=target_profile.pk %}" method="post" enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form form %}
<input type="submit" class="btn btn-dark rounded-pill col-6 mt-3">
</form>
</div>
{% endblock %}
이제 accountapp/templates/accountapp/detail.html에서 라우팅해준다.
{% if target_user.profile %}
<h2 style="font-family: 'NanumSquareB'">
{{ target_user.profile.nickname }}
<a href="{% url 'profileapp:update pk=target_user.profile.pk %}">
edit
</a>
</h2>
image도 이제 보일 수 있도록 해보자
detail.py 추가
<img src="{{ target_user.profile.image.url }}" alt="" style="height: 12rem; width: 12rem; border-radius: 20rem; margin-bottom: 2rem;">
그리고 leebook/urls.py에서 image에 대한 경로도 추가해주어야 한다.
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accountapp.urls')), # /를 꼭 적어야 주소창에서 그 이후의 주소들도 제대로 인식된다.
path('profiles/', include('profileapp.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
이제 상태메세지까지 보이도록 하자
<h5 style="margin-bottom: 4rem">
{{ target_user.profile.message }}
</h5>
account에서 했던 것 처럼 decorator를 만들어서 view에 적용시켜준다.
profileapp/decorators.py
from django.http import HttpResponseForbidden
from profileapp.models import Profile
def profile_ownership_required(func):
def decorated(request, *args, **kwargs):
profile = Profile.objects.get(pk=kwargs['pk'])
if not profile.user == request.user:
return HttpResponseForbidden()
return func(request, *args, **kwargs)
return decorated
profileapp/views.py
@method_decorator(profile_ownership_required, 'get')
@method_decorator(profile_ownership_required, 'post')
class ProfileUpdateView(UpdateView):
model = Profile
context_object_name = 'target_profile'
form_class = ProfileCreationForm
success_url = reverse_lazy('accountapp:hello_world')
template_name = 'profileapp/update.html'
create view와 update view 모두 success_url이 기존에는 hello_world 즉 메인 페이지로 설정되어 있었다. 이것을 상세 페이지 즉 detail 페이지로 설정하려면 get_success_url이라는 메소드를 사용해야 한다. detail에는 pk값이 필요하기 때문이다.
def get_success_url(self):
return reverse('accountapp:detail', kwargs={'pk':self.object.user.pk})
위의 메소드를 create, update view에 적용한다.
현재는 로그아웃을 해도 누구나 edit 버튼이 보인다. 이 edit 버튼도 로그인한 상태에서만 보이도록 설정한다.
detail.html
{% if target_user.profile %}
<img src="{{ target_user.profile.image.url }}" alt="" style="height: 12rem; width: 12rem; border-radius: 20rem; margin-bottom: 2rem;">
<h2 style="font-family: 'NanumSquareB'">
{{ target_user.profile.nickname }}
{% if target_user == user %}
<a href="{% url 'profileapp:update' pk=target_user.profile.pk %}">
edit
</a>
{% endif %}
</h2>
<h5 style="margin-bottom: 4rem">
{{ target_user.profile.message }}
</h5>
{% else %}
{% if target_user == user %}
<a href="{% url 'profileapp:create' %}">
<h2 style="font-family: 'NanumSquareB'">
Create Profile
</h2>
</a>
{% else %}
<h2> 닉네임 미설정 </h2>
{% endif %}
{% endif %}