Django01 프로젝트 - Abstractuser로 유저 모델 바꾸기

|

Django01 프로젝트 - Abstractuser로 유저 모델 바꾸기

참고 참고 전 포스트까지만 해도 OneToOneField로 프로파일을 만들어 사용했는데, 과감하게 Abstractuser로 바꿔보기로 결심했다. Abstractuser로 상속받은 모델을 사용하려면,

  • settings파일에서 AUTH_USER_MODEL = 앱이름.클래스명으로 지정해준다. ex) accounts.User
  • 다른 모델에서 위 모델을 참조할 때 from django.conf import settings해서 settings.AUTH_USER_MODEL로 사용해야 한다. 또는, from django.contrib.auth import get_user_model메소드를 불러와 현재 사용중인 모델을 불러와서 사용하자.

Model 변경

아래와 같이 수정했다. 이메일은 기본으로 제공하나, djagno의 기본 기능 중 비밀번호 리셋이 있는데, 이는 이메일을 통해서 초기화를 진행한다. 하지만 기본옵션이 중복이 허용이기에 위 기능이 작동하지 않는 것이다. (바꾸면 되겠지만 아직 그정도 실력은 아닌지라..) 여튼 지금은 unique옵션을 통해 중복을 허용하지 않게 설정했다.

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


class User(AbstractUser):
    email = models.EmailField(max_length=254, unique=True, verbose_name='이메일')
    nickname = models.CharField(max_length=30, verbose_name='닉네임')
    phone_number = models.CharField(max_length=30, verbose_name='전화번호')

admin 등록

모델을 입력하고 makemigrations와 migrate를 진행했으면, admin페이지에 등록해보자. 필드에 우리가 추가한 칼럼들을 넣기 위해 아래처럼 입력해주면 된다.

from django.contrib import admin
from .models import User
from django.contrib.auth.admin import UserAdmin


class CustomUserAdmin(UserAdmin):
    UserAdmin.fieldsets[1][1]['fields']+=('nickname', 'phone_number')
    UserAdmin.add_fieldsets += (
        (('Additional Info'),{'fields':('nickname','phone_number')}),
    )
admin.site.register(User, CustomUserAdmin)

forms 변경

이전에 사용했던 form은 다 지워버리고 새로 만들어 준다. 다 지웠을 때 아쉬우면서 개운했다 ㅎㅎ 아래 메타클래스처럼 입력해주면 template에서 다 입력할 수 있다.

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


class CustomUserCreationForm(UserCreationForm):

    class Meta(UserCreationForm.Meta):
        model = User
        fields = UserCreationForm.Meta.fields + ('email', 'nickname', 'phone_number', )

views 변경

이전에 사용했던 것보다 훨씬 깔끔하게 사용할 수 있게 되었다.

from .forms import CustomUserCreationForm


class UserSignUpView(CreateView):
    template_name = 'accounts/signup.html'
    form_class = CustomUserCreationForm
    success_url = reverse_lazy('accounts:signup_done')

Django01 프로젝트 - accounts앱 CBV로 바꾸기

|

Django01 프로젝트 - accounts앱 CBV로 바꾸기

accounts앱은 로그인, 로그아웃, 회원가입등을 담당하는 앱이다. 예전에 OneToOneField로 Profile을 연결해서 지금까지 사용중이다. Abstractuser를 통해 Profile의 내용들을 User모델에 넣을까 하다가, 굳이 그럴 필요는 없을거 같아서 일단 두고 Django의 인증기능들을 잘 활용하기 위해 FBV만 CBV로 바꿔보기로 했다.

forms

ModelForm을 활용해서 패스워드 재확인을 위한 속성을 추가해주고, 적절하게 Attributes들을 넣어준 상태이다.

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


class SignupForm(ModelForm): #회원가입을 제공하는 class이다.
    password_check = forms.CharField(max_length=200, widget=forms.PasswordInput(attrs={
        'placeholder': '비밀번호를 한 번 더 입력해주세요.', 'class': 'form-control', 'id': 'loginPW-check'}))
    # 아쉽게도 User 모델에서는 password_check 필드를 제공해주지 않는다.
    # 따라서 따로 password_check 필드를 직접 정의해줄 필요가 있다.
    # 입력 양식은 type은 기본이 text이다. 따라서 다르게 지정해주고 싶을 경우 widget을 이용한다.
    # widget=forms.PasswordInput()은 입력 양식을 password로 지정해주겠다는 뜻이다.

    field_order = ['username', 'password', 'password_check', 'email']
    # field_order=['username','password','password_check','last_name','first_name','email']
    # field_order는 만들어지는 입력양식의 순서를 정해준다.
    # 여기서 사용한 이유는 password 바로 밑에 password_check 입력양식을 추가시키고 싶어서이다.
    # 만약 따로 field_order를 지정해주지않았다면, password_check는 맨 밑에 생성된다.
    class Meta:
        model = User
        widgets = {
            'username': forms.TextInput(attrs={'placeholder': '아이디를 입력해주세요.','id': 'loginID', 'class': 'form-control'}),
            'password': forms.PasswordInput(attrs={'placeholder': '비밀번호를 입력해주세요.', 'id': 'loginPW', 'class': 'form-control'}),
            'email' : forms.EmailInput(attrs={'placeholder': '이메일을 입력해주세요.', 'class':'form-control'})
        }
        fields = ['username', 'password', 'password_check', 'email']
#User model에 정의된 username, passwordm last_name, first_name, email을 입력양식으로

class SigninForm(ModelForm): #로그인을 제공하는 class이다.
    class Meta:
        model = User
        widgets = {
            'username': forms.TextInput(attrs={'class': 'form-control', 'id': 'loginID', 'placeholder': '아이디를 입력해주세요.'}),
            'password': forms.PasswordInput(attrs={'class': 'form-control', 'id': 'loginPW', 'placeholder': '패스워드를 입력해주세요.'})
        }
        fields = ['username', 'password']

class ProfileForm(ModelForm):
    # address = forms.CharField(widget=forms.RadioSelect())
    class Meta:
        model = Profile
        fields = ['nickname', 'phone_number',]
        widgets = {
            'nickname': forms.TextInput(attrs={'placeholder': '닉네임을 입력해주세요.', 'class': 'form-control', 'id': 'nickName'}),
            'phone_number': forms.TextInput(attrs={'placeholder': '전화번호를 입력해주세요.', 'class': 'form-control', 'id': 'phoneNumber'}),
        }

views

##로그인 먼저 로그인 기능을 살펴보겠다. ###FBV 함수로 만든 로그인 기능이다.

def signin(request): #로그인 기능
    if request.user.is_authenticated: #유저가 인증되어있으면 아래 url로 리다이렉트
        return redirect(reverse('home'))
    if request.method == "GET":
        return render(request, 'accounts/signin.html', {'f': SigninForm})
    elif request.method == "POST":
        form = SigninForm(request.POST)
        id = request.POST['username']
        pw = request.POST['password']
        u = authenticate(username=id, password=pw)
	    # authenticate를 통해 DB의 username과 password를 클라이언트가 요청한 값과 비교한다.
	    # 만약 같으면 해당 객체를 반화하고 아니라면 none을 반환한다.

        if u: # u에 특정 값이 있다면,
            login(request, user=u) # u 객체로 로그인해라
            return HttpResponseRedirect(reverse('home'))
        else:
            return render(request, 'accounts/signin.html',{'f':form, 'error':'아이디나 비밀번호가 일치하지 않습니다.'})

###CBV 클래스뷰로 바꾸면 아래처럼만 템플릿만 변경해서 해주면 된다. dispatch에서 이미 로그인한 유저면 홈으로 돌아가게끔 설정하고, 아니면 계속 실행하게 해준다.

class UserLoginView(auth_views.LoginView):
    template_name = 'accounts/signin.html'

    def dispatch(self, request, *args, **kwargs):
        if request.user.is_authenticated:
            return redirect(reverse('home'))
        return super().dispatch(request, *args, **kwargs)

##회원가입 인라인 폼을 활용해서 CBV로 구현해 보긴 했는데, 어렵고 힘들어서 FBV로 일단 다시 원복해서 사용할까 한다. ###FBV

def signup(request):  # 역시 GET/POST 방식을 사용하여 구현한다.
    if request.user.is_authenticated:
        return redirect(reverse('home'))
    if request.method == "GET":
        return render(request, 'accounts/signup.html', {'f': SignupForm(),
                                                             'ef': ProfileForm(),
                                                             })
    elif request.method == "POST":
        form = SignupForm(request.POST)
        profile_form = ProfileForm(request.POST)
        if form.is_valid() and profile_form.is_valid():
            if form.cleaned_data['password'] == form.cleaned_data['password_check']:
            # cleaned_data는 사용자가 입력한 데이터를 뜻한다.
            # 즉 사용자가 입력한 password와 password_check가 맞는지 확인하기위해 작성해주었다.
                new_user = User.objects.create_user(form.cleaned_data['username'], form.cleaned_data['email'], form.cleaned_data['password'])
                # User.object.create_user는 사용자가 입력한 name, email, password를 가지고 아이디를 만든다.
                # 바로 .save를 안해주는 이유는 User.object.create를 먼저 해주어야 비밀번호가 암호화되어 저장된다.
                # new_user.last_name = form.cleaned_data['last_name']
                # new_user.first_name = form.cleaned_data['first_name']
                # 나머지 입력하지 못한 last_name과, first_name은 따로 지정해준다.
                # new_user.save(commit=False)
                new_user.profile.nickname = profile_form.cleaned_data['nickname']
                new_user.profile.phone_number = profile_form.cleaned_data['phone_number']
                new_user.save()
                # return render(request, 'accounts/sign_finish.html', {'user_name':profile_form.cleaned_data['nickname']})
                # return render(request, 'accounts/signup_done.html', {'user_name':profile_form.cleaned_data['nickname']})
                return redirect('home')
                # return HttpResponseRedirect(reverse('home'))
            else:
                return render(request, 'accounts/signup.html', {'f': form,
                                                                'ef': profile_form,
                                                                'error': '비밀번호와 비밀번호 확인이 다릅니다.'})  # password와 password_check가 다를 것을 대비하여 error를 지정해준다.
        else:  # form.is_valid()가 아닐 경우, 즉 유효한 값이 들어오지 않았을 경우는
            return render(request, 'accounts/signup.html', {'f': form,
                                                            'ef': profile_form,
                                                            })
            # return render(request, 'accounts/signup.html', {'f': form})
            # 원래는 error 메시지를 지정해줘야 하지만 따로 지정해주지 않는다.
            # 그 이유는 User 모델 클래스에서 자동으로 error 메시지를 넘겨줌

###CBV 책에서 봤던 인라인 폼셋을 활용해보기로 했다. 어째 더 지저분해지는거 같아서 위로 돌려 놓았다.

# forms
ProfileInlineFormSet = inlineformset_factory(User, Profile,
                                             fields=['nickname','phone_number'])

# urls
path('signup/', views.UserSignUpView.as_view(), name='signup'),

# views
from django.contrib.auth import get_user_model
from django.forms.utils import ErrorList
from django import forms


class UserSignUpView(CreateView):
    model = get_user_model()
    form_class = SignupForm
    template_name = 'accounts/signup.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        if self.request.POST:
            context['formset'] = ProfileInlineFormSet(self.request.POST, self.request.FILES)
        else:
            context['formset'] = ProfileInlineFormSet()
        return context

    def form_valid(self, form):
        context = self.get_context_data()
        if form.cleaned_data['password'] == form.cleaned_data['password_check']:
            formset = context['formset']
            if formset.is_valid():
                for eform in formset:
                    nickname = eform.cleaned_data['nickname']
                    phone_number = eform.cleaned_data['phone_number']
                print(nickname)
                print(phone_number)
                new_user = User.objects.create_user(form.cleaned_data['username'], form.cleaned_data['email'], form.cleaned_data['password'])
                new_user.profile.nickname = nickname
                new_user.profile.phone_number = phone_number
                new_user.save()
                return redirect('home')
            else:
                return self.render_to_response(self.get_context_data(form=form))
        else:
            form._errors[forms.forms.NON_FIELD_ERRORS] = ErrorList([
                u'비밀번호를 확인해주세요.'
            ])
            return self.render_to_response(self.get_context_data(form=form))

###html


{% if form.non_field_errors %}
	<div class="alert alert-danger">{{ form.non_field_errors }}</div>
{% endif %}

{% if formset.errors %}
	{% for dict in formset.errors %}
		{% for k, error in dict.items %}
			<div class="alert alert-danger">{{ k }} - 필수항목입니다.</div>
		{% endfor %}
	{% endfor %}
{% endif %}
<form action="" method="POST">
	{% csrf_token %}
	<div class="form-group">
		<h1><img src="{% static 'img/logo.png' %}" class="mainLogo"></h1>
		<label for="id">아이디</label>
		{{ form.username }}
	</div>
	<div id="check-user"></div>
	{{ formset.management_form }}
	{% for form in formset %}
		{{ form.id }}
	<div class="form-group">
		<label for="nickname">닉네임</label>
		{{ form.nickname|attr:"id:nickName"|add_class:"form-control"|attr:"placeholder:닉네임을 입력해주세요." }}
	</div>
	<div id="check-nickname"></div>
	<div class="form-group">
		<label for="phonenumber">전화번호</label>
		{{ form.phone_number|add_class:"form-control"|attr:"placeholder:전화번호를 입력해주세요." }}
	</div>
	{% endfor %}
	<div class="form-group">
		<label for="password">비밀번호</label>
		{{ form.password }}
	</div>
	<div>
		<label for="re-password">재확인</label>
		{{ form.password_check }}
	</div>
	<div class="alert alert-success" id="alert-success">비밀번호가 일치합니다.</div>
	<div class="alert alert-danger" id="alert-danger">비밀번호가 일치하지 않습니다.</div>
		<button type="submit" class="btn btn-outline-primary">회원가입하기</button>
</form>
 {% endblock %}

200629-200705_TIL

|

6월 29일(월)

  • 믹스인을 사용해서 가게 게시물의 생성, 수정, 삭제는 staff만 가능하게끔 해보았다.
  • 모든 게시글 생성, 삭제 때 tag를 저장할 수 있게 했다.

6월 30일(화)

  • 어제까지 한 파일을 업로드에서 봤는데, 모바일로 볼 때 사진 사이즈가 만족스럽지 않았다. 변경해봐야겠다.
  • jquery ajax를 사용해서 회원가입 페이지에서 아이디와 닉네임 실시간 확인을 구현해 보았다.

7월 1일(수)

  • ajax를 통해 좋아요 기능을 구현해보았다.
  • css에 대해 공부하는데 좋은 사이트들을 발견햇다. 참고1 참고2

7월 2일(목)

  • 모바일에서 사진이 짤리는 현상을 얼추 해결은 했다. 레이아웃 만드는게 어렵다는걸 느끼는 하루였다.
  • 게시글에도 좋아요 기능을 구현했다. ajax 스크립트를 파일로 만들어서 로드하는 형식으로 바꾸어 보았는데, 템플릿 변수들을 사용하긴 힘들다. 그렇기에 script 태그 안에 var myGlobal = {'ur': 같이 객체로 만들어서 ajax 스크립트 내에 사용하면 된다.
  • 게시글의 형식을 바꾸기 위해 날짜 관련해서 Djagno의 humanize 기능을 사용해 보았다. naturalday, naturaltime을 선택해서 사용하면 될거 같다.

7월 3일(금)

  • 친구한테 검토를 받고 디자인을 더 고쳐보았다. CSS의 미디어 쿼리를 사용해서 화면의 늘거나 줄어들 때에 맞춰 변경해보았다. 역시 아는 것이 힘이다.

7월 4일(토)

  • 새벽에 하고있는 프로젝트의 모델을 바꿔보는 일을 했다. 원래는 OneToOneField로 프로파일 형식으로 구축했는데, 할 때마다 번거롭고 해서 Abstractuser 형태로 바꿔봤다. 이 마저도 많은 뻘짓을 했지만, 어째 됐건 구현은 해서 기분은 좋다.

7월 5일(일)

  • 사이트를 계속 다듬고 통일성을 줘보고 있다. 내일은 페이지 기능을 예쁘게 다듬고, 아이디 찾기 비번 찾기 기능을 구현해보고자 한다.

Django01 프로젝트 - 좋아요 기능 구현

|

AJAX를 사용해서 좋아요 기능을 구현해 보았다.

models

좋아요는 여러 유저가 여러 포스트에 각각 누르는 형식이니 ManyToManyField로 구성해줘야 한다. 이후 개수를 카운트 하기 위해 아래에 PositiveIntegerField를 추가했다.

like_users = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='like_boards')
like_count = models.PositiveIntegerField(default=0)

Ajax 없이 구현해보기

한 번 Ajax없이 구성도 해보았다.

urls

path('<int:pk>/like/', views.like, name='like'),

views

post.like_users를 통해 해당 게시물에 유저가 있는지 확인하고, 있으면 제거하고 없으면 넣어주는 형식이다.

def like(request, pk):
    post = get_object_or_404(Userboard, pk=pk)
    if request.user in post.like_users.all():
        post.like_users.remove(request.user)
    else:
        post.like_users.add(request.user)
    return redirect(store.get_absolute_url())

Ajax로 구현해보기

먼저 jQuery를 이용해서 스크립트를 만들어 준다. 선택자는 button태그에 like가 포함된 엘레먼트들을 고르게 해줬다. 이렇게 사용한 이유는, 아래 코드를 js파일로 저장해서 로드해서 사용하기 위해서다. 현재 내 프로젝트는 가게와 게시판 기능이 있는데, 각각 사용하기 위해서이다. 이렇게 하면 js파일에선 템플릿 변수를 사용못하기 때문에 script태그 안에 내가 사용할 변수들을 MyGlobal에 저장해 주었다.

JavaScript


<script>
    var MyGlobal = {
        url: "{% url 'board:like' %}",
    }
</script>
$("[class*=like]").click(function () {
    var pk = $(this).attr('post-id')
    $.ajax({
        type: "GET",
        url: MyGlobal.url,
        data: {'pk': pk},
        dataType: "json",
        success: function (response) {
            $("#count-" + pk).html(response.like_count + "개");
            var users = $("#like-user-" + pk).text();
            if (response.message) {
                $('#like-heart-'+pk).removeClass('far fa-heart').addClass('fas fa-heart');
            } else {
                $('#like-heart-'+pk).removeClass('fas fa-heart').addClass('far fa-heart');
            }
        },
        error: function (request, status, error) {
            alert("로그인이 필요합니다.")
            window.location.replace("/accounts/login/")
        }
    })
});

views

json 형태로 반환해주기 위해 아래의 두 모듈을 import 해준다.

from django.http import HttpResponse
import json

@login_required
def like(request):
    pk = request.GET.get('pk', None)
    board = get_object_or_404(UserBoard, pk=pk)

    if request.user in board.like_users.all():
        board.like_users.remove(request.user)
        board.like_count -= 1
        board.save()
        message = False
    else:
        board.like_users.add(request.user)
        board.like_count += 1
        board.save()
        message = True
    context = {
        'like_count': board.like_users.count(),
        'message': message,
    }
    return HttpResponse(json.dumps(context), content_type="application/json")

html


<a href="{% url 'store:like' store.pk %}">
    {% if user in store.like_users.all %}
    <a href="{% url 'store:like' store.pk %}"><i class="fas fa-heart"></i></a>
    {% else %}
    <a href="{% url 'store:like' store.pk %}"><i class="far fa-heart"></i></a>
    {% endif %}
    <p class="card-text">
        {{ store.like_users.count }} 명이 좋아합니다.
    </p>
</a>

Django01 프로젝트 - 모델 추가하기

|

Django01 프로젝트 - 모델 추가하기

친구에게 만들고 있는 사이트에 피드백을 받았는데, 많이 부족함을 느꼈다. 이 사이트를 사용하는 사용자 관점에서 바라보라고. 처음 내 사이트 의중이 동국대 주변 가게들을 소개하는 거였고, 부가적으로 자유게시판 기능을 추가해서 사람들끼리 내용 공유를 하려는 것이다. 일단 메인은 가게 소개인 것이니, 이것에 초점을 두어야 한다고 다시 생각하게 되었다. 그렇기에 모델부터 조금씩 수정해보고자 한다.

카테고리 추가

가게에도 여러 업종이 있다. 이것을 크게 ‘음식집’, ‘술집’, ‘카페’ 정도의 카테고리로 분류를 하는 것이 좋을 거 같다.

store.models 파일 수정

정해진 카테고리에서만 저장할 것이기에 choices 필드 옵션을 사용하기로 했다. 아래 CATEGORIES처럼 이중 튜플을 만들어 주고, 각 튜플마다 첫 번째는 DB에 사용될 값과, form 위젯에서 사용될 값을 적어준다. 마지막으로 맨 아래와 같이 CharField에 choices=CATEGORIES를 적어주면 된다.

class Store(models.Model):
    CATEGORIES = (
        ('restaurant', '음식집'),
        ('bar', '술집'),
        ('cafe', '카페'),
    )
    name = models.CharField(max_length=50, verbose_name="가게명")
    slug = models.SlugField('SLUG', unique=True, allow_unicode=True, help_text='one word for alias')
    location = models.CharField(max_length=100, blank=True, verbose_name="위치")
    phone_number = models.CharField(max_length=30, blank=True, verbose_name="연락처")
    description = models.TextField(blank=True, verbose_name="설명")
    store_image = models.ImageField(blank=True, upload_to="store/store_pic")
    created_dt = models.DateTimeField(auto_now_add=True)
    modified_dt = models.DateTimeField(auto_now=True)
    likes = models.IntegerField(verbose_name='좋아요', default=0)
    tags = TaggableManager(blank=True)
    category = models.CharField(max_length=10, choices=CATEGORIES)

store.urls 파일 수정

아래 내용을 urlpatterns에 추가했다.

    path('<str:category>/', views.CategoryView.as_view(), name='categories'),

store.views 파일 수정

template 파일은 기존꺼를 활용하면 된다.

class CategoryView(ListView):
    template_name = 'store/index.html'
    context_object_name = 'store_list'

    def get_queryset(self):
        return Store.objects.filter(category=self.kwargs['category'])

홈페이지 클래스 뷰 수정

카테고리별로 하나씩 보여주기 위해서 filter로 갖고 온 후, 슬라이스 처리했다.

class HomePageView(TemplateView):

    template_name = 'home.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['store_list'] = Store.objects.filter(category='restaurant')[:1] | \
                                Store.objects.filter(category='bar')[:1] | \
                                Store.objects.filter(category='cafe')[:1]

        return context