21 Jun 2019
|
Django
팔로우 기능, 유저모델(AbstractUser) 상속 (1)
Django 21 M:N 관계설정 포스팅에서 좋아요 기능을 통해 M:N 관계를 설정하는 코드를 작성해보았음. M:N 관계설정을 이용하여 인스타그램의 팔로우 기능을 구현하고 Django 내장 유저모델을 상속받아보도록 하자.
Django user 모델 확장의 4가지 방법
- Proxy Model: 기존의 User 모델을 그대로 사용하되, 일부 동작을 변경하는데만 사용
- User Profile: 하나의 새로운 모델을 정의한 후, User 모델과 1:1 관계설정(프로필 모델 참조)
- AbstractBaseUser: 완전한 새로운 User 모델을 만들때 사용
- AbstractUser: 기존의 User 모델을 사용하되, 추가적인 정보를 더 넣고 싶을 때 사용. 2번 방법은 추가로 클래스를 생성하지만, 이 방법의 경우 추가로 클래스를 생성하지는 않음.
관련 내용 참조
accounts/models.py
- Django 내장 유저 모델을 상속받아 사용
- 기존의 User 모델을 사용하면서 추가적인 정보만 넣으므로 4번 방법(
AbstractUser
) 을 이용
- 사용을 위해 import
from django.conf import settings
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
followers = models.ManyToMnayField(settings.AUTH_USER_MODEL, related_name='followings')
settings.py
- accounts/models.py 내 AbstractUser를 상속받은 User 모델을 새로 만들었으므로, 이 모델을 유저모델로 사용하기 위해 settings.py 내 추가 설정
- 유저모델을 변경하였으므로, DB & migration 파일을 삭제 하여 갈아엎은 후, 마이그레이션 재설정
AUTH_USER_MODEL = 'accounts.User'
현재 상태에서 회원가입을 진행할 시에는 아래와 같이 에러가 발생한다.
AttributeError at /accounts/signup/
Manager isn’t available; ‘auth.User’ has been swapped for ‘accounts.User’)
views.py/signup 함수에서 사용하는 UserceationForm
이 문제인데, 이 폼은 장고에서 기본적으로 제공하는 폼으로, 장고 기본 유저모델을 기반으로 만들어져있음. 그러나 settings.py 에서 커스텀 유저모델을 기본 모델로 설정을 변경하였기 때문에 에러가 발생. 따라서, 우리가 만든 유저모델을 기반으로 사용하기위해서는 해당 폼을 커스터마이징할 필요가 있음.
UserCreationForm
을 상속받아 CustomUserCreationForm
이라는 커스터마이징된 폼 생성
get_user_model()
은 settings.py 에서 설정한 유저모델을 가져옴.
- fields에는
UserCreationForm.Meta.fields
를 입력
from django.contrib.auth.forms import UserCreationForm
class CustomUserCreationForm(UserCreationForm):
class Meta(UserCreationForm.Meta):
models = get_user_model()
fields = UserCreationForm.Meta.fields
views.py
- signup 함수 내에서 사용하는
UserCreationForm
을 CustomUserCreationForm
으로 변경
from .forms import CustomUserCreationForm
def signup(request):
if request.user.is_authenticated:
return redirect('posts:list')
if request.method == 'POST':
signup_form = CustomUserCreationForm(request.POST)
if signup_form.is_valid():
user = signup_form.save()
Profile.objects.create(user=user)
auth_login(request, user)
return redirect('posts:list')
else:
signup_form = CustomUserCreationForm()
return render(request, 'accounts/signup.html', {'signup_form':signup_form})
urls.py
urlpatterns = [
path('<int:user_id>/follow/', views.follow, name='follow'),
]
20 Jun 2019
|
Django
1:1관계 설정 (프로필 모델 구현)
유저모델과 1:1로 대응하는 프로필 모델을 구현해보도록 하자. 하나의 유저는 하나의 프로필만을 가지므로 1:1의 관계를 띄는 것이 당연할 것임. 프로필 모델은 유저 모델을 기반으로 프로필 정보를 입력하는 모델로, 인스타그램의 프로필정보와 같은 역할을 하게끔 코드를 작성할 예정임.
accounts/models.py
- user모델과 1:1관계를 형성해야하므로
OneToOneField
사용
- 그외 별명 & 자기소개 & 프로필 사진에 대한 필드를 추가
- 이미지 필드를 사용하기 위해
ProcessedImageField
& ResizeToFill
import
- 프로필 필드의 경우, 값이 반드시 있어야 하는게 아니라 비어있어 괜찮다는 옵션 적용:
blank = True
- 작성 후 마이그레이션 재 설정
from imagekit.models import ProcesssedImageField
from imagekit.processors import ResizeToFill
form django.conf import settings
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
nickname = models.CharField(max_length=40, blank =True)
introduction = models.TextField(blank=True)
image = ProcessedImageField(
blank = True,
upload_to = 'profile/images',
processors = [ResizeToFill(300, 300)],
format = 'JPEG',
options = {'quality':90},
)
views.py (프로필 생성)
유저와 프로필은 1:1관계이므로 유저가 존재할 경우 프로필도 반드시 존재해야함. 따라서 유저가 회원가입하면 프로필도 바로 생성되게 하자.
- signup (회원가입) 함수에 유저가 생성되는 코드 바로 아래에 프로필을 생성하는 코드 작성
profile
모델 import
- 기존에 회원가입이 이미 완료된 유저들의 경우 프로필을 생성 할 수 없으므로 DB를 밀어서 초기화 진행
- DB 초기화 경우, db.sqlite3 파일과 migrations폴더 내 0001, 00002 등으로 시작되는 파일을 삭제하면 됨.
from .models import Profile
def signup(request):
if request.user.is_authenticated:
return redirect('posts:list')
if request.method == 'POST':
signup_form = UserCreationForm(request.POST)
if signup_form.is_valid():
user = signup_form.save()
Profile.objects.create(user=user) #프로필 생성
auth_login(request, user)
return redirect('posts:list')
else:
signup_form = UserCreationForm()
return render(request, 'accounts/signup.html', {'signup_form': signup_form})
프로필 수정
기본 MTV 패턴 구현
유저가 존재하는 이상 프로필은 삭제할 필요가 없으므로 delete 로직은 불필요함. signup 함수를 통해서 create로직은 이미 구현되어있으므로 update 로직에 대한 코드만 작성하면 됨.
#views.py
def profile_update(request):
return render(request, 'accounts/profile_update.html')
#urls.py
urlpatterns = [
path('profile/', views.profile_update, name='profile_update'),
]
{% extends 'base.html' %}
{% block container %}
<h1>Profile Update</h1>
{% endblock %}
- 프로필 모델 폼 생성 (프로필 모델 import)
from .models import Profile
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['nickname', 'introduction', 'image',]
views.py
- forms.py 에서 새로 생성한 프로필 모델폼 import
- 요청을 보내는(현재 로그인된) 유저의 프로필(
request.user.profile
) 을 profile
이라는 변수에 저장
- HTTP methods(GET/POST)에 따라, 프로필 폼을 넘기거나, 사용자가 html 문서에서 새로 입력한 프로필 정보를 받아와 내용을 업데이트 & DB에 저장하게 코드를 작성
- POST방식의 경우, 이미지 파일이 있으므로
request.FILES
를 인자로 넘겨줄 것.
from .forms import ProfileForm
def profile_update(request):
profile = request.user.profile
if request.method == 'POST':
profile_form = ProfileForm(request.POST, request.FILES, instance=profile)
if profile_form.is_valid():
profile_form.save()
return redirect('accounts:people', request.user.username)
else:
profile_form = ProfileForm(instance=profile)
return render(request, 'accounts/profile_update.html', {
'profile_form':profile_form
})
profile_update.html
- 프로필 폼을 부트스트랩 양식으로 나타냄
- 이미지 파일이 있으므로,
enctype
속성값 적용
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>Profile Update</h1>
<form method='POST' enctype="multipart/form-data">
{% csrf_token %}
{% bootstrap_form profile_form %}
<input type="submit" value="Submit"/>
</form>
{% endblock %}
people.html
- 프로필에 대한 정보를 people.html에 표시
- 조건문을 사용하여 프로필 이미지가 있는 경우, 프로필 이미지를 보여줌
- bootstrap - gridsystem을 사용하여 적절히 영역 분배
- 프로필 수정 링크를 a태그로 연결
{% extends 'base.html' %}
{% load bootstrap 4 %}
{% block container %}
<div class="container">
<div class="row">
<div class="col-3">
<h1>
{% if people.profile.image %}
<img src="{{ people.profile.image.url }}" width= 70,
alt="{{ people.profile.image}}">
{% endif %}
{{ people.username }}
</h1>
</div>
<div class="col-9">
<div>
<strong>{{ people.profile.nickname }}</strong>
</div>
<div>
{{ people.profile.introduction }}
</div>
</div>
</div>
{% if user == people %}
<div>
<a href="{% url 'accounts:profile_update' %}">프로필 수정</a>
_post.html
- 리스트 페이지의 각 게시글에 작성자의 프로필 이미지가 보이도록 코드 작성
<div class="card" style="width: 18rem;">
<div class="card-header">
{% if post.user.profile.image %}
<img src="{{ post.user.profile.image.url }}" width="25" alt="">
{% endif %}
19 Jun 2019
|
Django
유저 정보 수정 & 삭제
유저 정보 수정하기
기본 MTV 패턴 구현
- User에 대한 정보를 수정하므로 accounts 앱 내에서 MTV 패턴을 구현
- Django 내장 모델폼인
UserChangeForm
사용을 위해 import
- posts/views.py/update함수와 마찬가지로, 유저정보에 대한 수정이므로 instance 값에
request.user
을 입력하여, 유저 정보가 담긴 모델폼을 변수에 저장한 후 템플릿 변수로 뿌림.
# views.py
from django.contrib.auth.forms import UserChangeForm
def update(request):
user_change_form = UserChangeForm(instance = request.user)
return render(request, 'accounts/update.html', {'user_change_form':user_change_form})
# urls.py
urlpatterns = [
path('update/', views.update, name='update'),
]
- update 함수로 넘겨진
user_change_form
을 bootstrap 양식에 맞춰서 html에 보여줌
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>User Edit</h1>
<form method="POST">
{% csrf_token %}
{% bootstrap_form user_change_form %}
<input type="submit" value="Submit"/>
</form>
{% endblock %}
장고 내장 모델폼인 UserChangeForm
에는 사용자가 업데이트할 필요가 없는 정보들도 함께 많이 담겨 있음. 따라서 많은 데이터 중, Username, Firs tname, Last Name, Email Address에 대한 정보만 수정하게끔 코드를 다듬어보자.
- accounts/forms.py 파일 생성
UserChangeForm
을 상속한 CustomUserChangeForm
을 만들어 필요한 필드만 가져옴
UserChangeForm
사용을 위해 import
- 모델폼에서 연결해주는 모델의 경우
User()
이라고 설정해도 되나, global settiings 오버라이딩을 통해서 인증 User 모델을 다른 모델로 변경할 수도 있음. 따라서 settings.py에서 설정된 User 모델을 갖고오는 get_user_model()
을 이용하여 사용하자.
get_user_model()
사용을 위해 import
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth import get_user_model
class CustomUserChangeForm(UserChangeForm):
class Meta:
model = get_user_model()
fields = ['username', 'email', 'first_name','last_name']
views.py 재 수정
- views.py 내 유저정보수정하는 모델폼을 forms.py에서 새로 정의한
CustomChangeForm
으로 변경
- 사용을 위해
CustomUserChangeForm
import 해올 것
- POST 방식인 경우, 수정한 내용으로 업데이트를 하고, GET 방식인 경우, 기존의 데이터를 instance에 담은 채, 폼을 html으로 넘김
- 로그인이 된 경우에만 해당 함수가 실행될 수 있도록 데코레이터 설정
from .forms import CustomUserChangeForm
from django.contrib.auth.decorators import login_required
@login_required
def update(request):
if request.method == 'POST':
user_change_from = CustomUserChangeForm(request.POST, instance=request.user)
if user_change_form.is_valid():
user_change_form.save()
return redirect('accounts:people', request.user.username)
else:
user_change_form = CustomUserChangeForm(instance = request.user)
return render(request, 'accounts/update.html', {
'user_change_form':user_change_form
})
people.html
- 유저정보를 수정하는 링크 추가
- 현재 로그인된
user
가 people
과 동일한 경우에만 유저 정보 수정 링크가 보이도록 조건문 적용
{% extends 'base.html' %}
{% block container %}
<div class="container">
<h1> {{ people.username }}</h1>
{% if user == people %}
<div>
<a href="{% url 'accounts:update' %}">계정정보수정</a>
</div>
{% endif %}
<div class="row">
{% for post in people.post_set.all %}
<div class="col-4">
<img src= "{{ post.image_set.first.file.url }}" class="img-fluid"/>
</div>
{% endfor %}
</div>
</div>
{% endblock %}
유저 정보 삭제하기 (회원 탈퇴)
기본 MTV 패턴 구현
- POST 방식인 경우, DB에서 유저 정보를 삭제하고, GET 방식인 경우, 삭제 페이지로 넘김
- 로그인이 된 경우에만 해당 함수가 실행 될 수 있또록 데코레이터 설정
# views.py
from django.contrib.auth.decorators import login_reqiured
@login_required
def delete(request):
if request.method == 'POST':
request.user.delete()
return redirect('posts:list')
return render(request, 'accounts/delete.html')
# urls.py
urlpatterns = [
path('delete', views.delete, name='delete'),
]
{% extends 'base.html' %}
{% block container %}
<h1>User delete</h1>
<form method="POST">
{% csrf_token %}
<p>정말로 탈퇴하시겠습니까?</p>
<input type="submit" value="탈퇴"/>
</form>
{% endblock %}
- update.html
- 유저정보 삭제 링크를 update.html에 표기
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>User Edit</h1>
<form method="POST">
{% csrf_token %}
{% bootstrap_form user_change_form %}
<input type="submit" value="Submit"/>
</form>
<a href="{% url 'accounts:delete' %}" class="btn btn-danger">회원탈퇴</a>
{% endblock %}
비밀번호 변경하기
accounts/update.html에서 사용하는 UserChangeForm
으로는 비밀번호 수정이 불가능함. 비밀번호는 암호화가 되어 DB에 저장되므로, 단순히 User edit form에서 알아볼 수 있는 문자로는 수정이 불가능하며 암호화 과정이 필요함. 따라서 비밀번호 변경을 위한 별도의 폼을 사용해야한다.
views.py & urls.py
- 비밀번호 수정을 위한 Django 내장 폼인
PasswordChangeForm
을 사용 & import
- 앞서 반복했던 방식과 마찬가지로 POST 방식인 경우, 수정된 비밀번호로 변경시키고, GET 방식인 경우에는 비밀번호 변경 폼을 password.html에 뿌려줌
- 해당 폼의 경우 들어오는 인자의 순서가 조금 다름.
- 첫번째 인자: request.user (유저에 대한 정보)
- 두번째 인자: request.POST (비밀번호에 대한 정보)
- 혹은 키워드 인자명을 함께 써줘도 됨.
# views.py
from django.contrib.auth.forms import PasswordChangeForm
@login_required
def password(request):
if request.method == 'POST':
password_change_form = PasswordChangeForm(request.user, request.POST)
# 키워드인자명을 함께 써줘도 가능
# password_change_form = PasswordChangeForm(user=request.user, data=request.POST)
if password_change_form.is_valid():
password_cchange_form.save()
return redirect('accounts:people', request.user.username)
else:
password_change_form = PasswordChangeForm(request.user)
return render(request, 'accounts/password.html',{
'password_change_form':password_change_form
})'
# urls.py
urlpatterns = [
path('password/', views.password, name='password'),]
password.html & update.html
{% extends 'base.html' %}
{% block container %}
<h1>User password change</h1>
<form method="POST">
{% csrf_token %}
{{ password_change_form }}
<input type="submit" value="submit"/>
</form>
{% endblock %}
- update.html
- 비밀번호 변경 링크를 update.html에 표기
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block container %}
<h1>User Edit</h1>
<form method="POST">
{% csrf_token %}
{% bootstrap_form user_change_form %}
<input type="submit" value="Submit"/>
</form>
<a href="{% url 'accounts:delete' %}" class="btn btn-danger">회원탈퇴</a>
<a href="{% url 'accounts:password' %}">비밀밀호 변경</a>
{% endblock %}
views.py (2)
세션에는 비밀번호를 포함하여 유저의 정보가 담겨있음. 비밀번호가 바뀔경우, 기존의 세션에 담긴 유저의 비밀번호와 일치하지 않게 되고, 세션이 만료되어 로그인이 풀리게 됨. 따라서 비밀번호를 변경하고도 로그인을 유지하게 하기 위해서는 추가적인 코드 작성이 필요함.
update_session_auth_hash(request, user)
- 세션에 있는 로그인 정보 해쉬를 업데이트할 때 사용하는 메서드
- 새로 설정된 비밀번호의 정보는
user
에 담겨있음.
reqeuest
인자를 통해 session에 정보를 업데이트
- 사용을 위해 import 해올 것
- 공식문서 참조
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth import update_session_auth_hash
@login_required
def password(request):
if request.method == 'POST':
password_change_form = PasswordChangeForm(request.user, request.POST)
if password_change_form.is_valid():
# 추가된 부분
user = password_cchange_form.save()
update_session_auth_hash(request, user)
# 끝
return redirect('accounts:people', request.user.username)
else:
password_change_form = PasswordChangeForm(request.user)
return render(request, 'accounts/password.html',{
'password_change_form':password_change_form
})'
16 Jun 2019
|
Django
이미지 다루기 2 (인스타그램)
Django17 미디어 파일 관리 및 업로드 구현 포스팅에서 이미지(미디어)파일을 어떻게 업로드하여 관리하고, 재가공하는지에 대해 정리해보았다. 추가로, 하나의 게시글(Post)에 여러 사진을 올리고, 또한 그사진들을 횡으로 보여지도록, 마치 인스타그램 처럼 기능하게끔 내용을 확장시켜보도록 하자.
사진 슬라이드 넘기기(Carousel 적용)
_post.html
인스타그램처럼 여러장의 사진을 하나의 템플릿안에서 슬라이드 처럼 횡으로 넘길 수 있게 코드를 구현해보자. 이를 위해서 부트스트랩의 carousel을 적절히 활용하면 됨.
- 첫번째 사진만 active 속성을 부여하기 위해 아래의 장고템플릿 사용
- ` <div class="carousel-item {% if forloop.counter == 1 %} active {% endif %}">`
- 반복문(forloop)의 횟수(counter)가 1일경우, 즉 첫번째 사진인 경우 active 속성이 적용됨.
- 어떤 사진을 보여주고 있고, previous/Next 에 이전/다음 사진이 가는지에 대한 기준을 설정해줘야함. 이러한 부분을 id 속성에 적용시킴.
- id는 중복되면 안되는 고유의 값이 들어가야하므로
post_id
를 활용하여 고유값 부여
- 이전/다음 사진으로 이동하는 기능을 하는 하단의 a 태그의 href에는 #를 꼭 붙여 줄 것.
<a class="carousel-control-prev" href="#post_{{ post.id }}"
- id값에
{{ post_id }}
만 입력할 경우(숫자만 들어간 경우)는 작동하지 않으므로 유의할 것.
<div class="card" style="width: 18rem;">
<div class="card-header">
<span> <a href="{% url 'accounts:people' post.user.username %}">
{{ post.user.username }} </a></span>
</div>
<div id="post_{{ post.id }}" class="carousel slide" data-ride="carousel">
<div class="carousel-inner">
{% for image in post.image_set.all %}
<div class="carousel-item {% if forloop.counter == 1 %} active {% endif %}">
<img src="{{ image.file.url }}" class="card-img-top"
alt="{{ image.file.image }}">
</div>
{% endfor %}
</div>
<a class="carousel-control-prev" href="#post_{{ post.id }}"
role="button" data-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="sr-only">Previous</span>
</a>
<a class="carousel-control-next" href="#post_{{ post.id }}"
role="button" data-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="sr-only">Next</span>
</a>
</div>
accounts/people.html
- people 페이지에 들어갈 경우, 여러 사진 중, 가장 최근의 첫번째 사진만 뜨게 코드 작성
post.image_set.first.file.url
이용(.first
)
<div class="container">
<h1> {{ people.username }}</h1>
<div class="row">
{% for post in people.post_set.all %}
<div class="col-4">
<img src= "{{ post.image_set.first.file.url }}" class="img-fluid"/>
</div>
{% endfor %}
</div>
Views.py - Update 함수 수정
ImageFormSet
관련 코드를 Create 함수에 넣어서 수정한 것과 마찬가지로, update 함수도 이미지폼셋 관련 코드를 추가로 삽입하고 다듬어보도록 하자.
- Get 방식인 경우
ImageFormSet
을 불러온 후, instance에 부모클래스를 넣음.
- 이후, 선언한 변수를 템플릿 변수로 넘김
- Post 방식인 경우
- 기본적인 CRUD의 Update와 동일한 방식으로 코드 구현
PostForm
의 두번째 인자에 있던 request.FILES
는 ImageFormSet
의 인자로 이동되었으므로 삭제
- 유효성 검증 후 DB에 저장
- Update 로직 구현 시에는, 이미
post
가 생성되어있으므로 transaction 코드를 필요 없음.
@login_required
def update(request, post_id):
post = get_object_or_404(Post, id=post_id)
if post.user != request.user:
return redirect('posts:list')
if request.method == 'POST':
post_form = PostForm(request.POST, instance=post)
image_formset = ImageFormSet(request.POST, request.FILES, instance=post)
if post_form.is_valid() and image_formset.is_valid():
post_form.save()
image_formset.save()
return redirect('posts:list')
else:
post_form = PostForm(instance=post)
image_formset = ImageFormSet(instance=post)
return render(request, 'posts/form.html', {
'post_form': post_form,
'image_formset' : image_formset,
})