-
$ django-admin startproject either_project $ cd either_project $ python manage.py startapp accounts $ python manage.py startapp eithers
-
either_project/settings.py
설정ALLOWED_HOSTS = ['*'] INSTALLED_APPS = [ ... 'accounts', 'eithers', 'bootstrap4', # bootstrap4 설치 => pip install django-bootstrap4 ] TEMPLAGES = [{ ... 'DIRS': [os.path.join(BASE_DIR, 'templates')] # base.html 사용하기 위해 ... }] LANGUAGE_CODE = 'ko-kr' TIME_ZONE = 'Asia/Seoul' # message framework 사용하기 위해 from django.contrib.messages import constants as messages_constants MESSAGE_LEVEL = messages_constants.DEBUG MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
-
either_project/urls.py
설정from django.contrib import admin from django.urls import path, include from eithers import views urlpatterns = [ path('', views.index), # 바로 메인 페이지로 이동 path('admin/', admin.site.urls), path('accounts/', include('accounts.urls')), path('eithers/', include('eithers.urls')), ]
-
eithers/models.py
from django.db import models from django.contrib.auth.models import User # 유저(1) - 투표(N) 관계 구현을 위한 class class Vote(models.Model): creator = models.ForeignKey(User, on_delete=models.CASCADE) title = models.CharField(max_length=30) description = models.CharField(max_length=100) category = models.CharField(max_length=30) item1 = models.CharField(max_length=100) item2 = models.CharField(max_length=100) item1_num = models.IntegerField(default=0) item2_num = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) # 투표(M) - 투표 참여자(N) 관계 구현을 위한 class class VoteSelection(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) vote = models.ForeignKey(Vote, on_delete=models.CASCADE) selection = models.IntegerField() # 유저(1) - 댓글(N), 투표(1) - 댓글(N) 관계 구현을 위한 class class Comment(models.Model): author = models.ForeignKey(User, on_delete=models.CASCADE) vote = models.ForeignKey(Vote, on_delete=models.CASCADE) selection = models.ForeignKey(VoteSelection, on_delete=models.CASCADE) content = models.CharField(max_length=200) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True)
-
User Model은 django 내부에 구현된 User Model을 사용할 것이기 때문에 별도로 생성하지 않아도 된다.
-
$ python manage.py makemigrations $ python manage.py migrate
-
eithers/forms.py
from django import forms from .models import Vote, Comment class VoteForm(forms.ModelForm): class Meta: model = Vote fields = ['title', 'description', 'category', 'item1', 'item2'] class CommentForm(forms.ModelForm): class Meta: model = Comment fields = ['content']
-
User 관련 Form의 경우, django 내부에 구현된 UserCreationForm, AuthenticationForm을 사용할 것이기 때문에 별도로 생성하지 않아도 된다.
-
eithers/admin.py
from django.contrib import admin from .models import Vote, Comment, VoteSelection admin.site.register(Vote) admin.site.register(Comment) admin.site.register(VoteSelection)
-
$ python manage.py createsuperuser
-
accounts/urls.py
from django.urls import path from . import views app_name = 'accounts' urlpatterns = [ path('signup/',views.signup, name='signup'), # 회원가입 및 form 표시 path('login/',views.login, name='login'), # 로그인 및 form 표시 path('logout/',views.logout, name='logout'), # 로그아웃 ]
-
eithers/urls.py
from django.urls import path from . import views app_name = 'eithers' urlpatterns = [ path('', views.index, name='index'), # 투표 리스트 path('create/', views.create, name='create'), # 투표 생성 path('<int:vote_pk>/', views.detail, name='detail'), # 투표 상세 내역 path('<int:vote_pk>/comments/', views.comment_create, name='comment_create'), # 투표 댓글 생성 path('random_vote/', views.random_vote, name='random_vote'), # 랜덤하게 투표 상세 내역 반환 ]
-
bootstrap4 사용 위한 setting
{% load bootstrap4 %}
{% bootstrap_css %}
{% bootstrap_javascript jquery='full' %}
-
block
<body> <!-- navbar 구현 --> {% block content %} {% endblock %} <!-- footer 구현 --> </body>
-
사용자 인증여부에 따라 분기한 UI 구성
{% if user.is_authenticated %} {% else %} {% endif %}
-
message framework 활용하기 위한 코드
{% if messages %} {% for message in messages %} <div class="row" style="margin:20px auto;"> <div class="col-sm-12"> <div class="alert alert-info"> <strong class="text-center">{{ message.message }}</strong> </div> </div> </div> {% endfor %} {% endif %} {% block content %} {% endblock %}
-
accounts/views.py
-import
from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import login as auth_login # 계정 로그인 from django.contrib.auth import logout as auth_logout # 계정 로그아웃 from django.contrib.auth.decorators import login_required # 로그인이 되어 있지 않다면 로그인 page 반환 from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib import messages # messages framework
-
회원가입
# accounts/views.py def signup(request): if request.user.is_authenticated: # 이미 인증된 사용자라면 return redirect('eithers:index') if request.method == 'POST': # 완성된 가입 form 제출 form = UserCreationForm(request.POST) if form.is_valid(): # 유효성 검증 user = form.save() auth_login(request, user) # 가입과 동시에 로그인 messages.info(request, '회원가입을 성공하셨습니다.') return redirect('eithers:index') else: # 빈 가입 form 요청 form = UserCreationForm() context = { 'form': form } return render(request, 'accounts/signup.html', context)
<!-- accounts/templates/accounts/signup.html --> {% extends 'base.html' %} {% load bootstrap4 %} {% block content %} <h1 class="text-center">Signup</h1> <hr> <form action="" method="POST"> {% csrf_token %} {% bootstrap_form form %} {% bootstrap_button "Signup" button_type="submit" button_class="btn-primary" %} </form> {% endblock %}
-
로그인
# accounts/views.py def login(request): if request.user.is_authenticated: # 이미 인증된 사용자라면 return redirect('eithers:index') if request.method == 'POST': # 완성된 form 제출 form = AuthenticationForm(request, request.POST) if form.is_valid(): auth_login(request, form.get_user()) messages.info(request, '로그인을 성공하셨습니다.') # 로그인 전 요청한 url이 있으면 해당 url로, 없다면 리뷰 목록 페이지로 이동(단축 평가 활용) return redirect(request.GET.get('next') or 'eithers:index') else: # 빈 로그인 form 요청 form = AuthenticationForm() context = { 'form': form } return render(request, 'accounts/login.html', context)
<!-- accounts/templates/accounts/login.html --> {% extends 'base.html' %} {% load bootstrap4 %} {% block content %} <h1 class="text-center">Login</h1> <hr> <form action="" method="POST"> {% csrf_token %} {% bootstrap_form form %} {% bootstrap_button "Login" button_type="submit" button_class="btn-primary" %} </form> {% endblock %}
-
로그아웃
# accounts/views.py @login_required # 로그인 안된 상태에서는 로그인 페이지로 이동 def logout(request): auth_logout(request) # GET 방식 messages.info(request, '로그아웃 되었습니다.') return redirect('eithers:index')
-
eithers/views.py
-import
from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_POST from .forms import VoteForm, CommentForm from .models import Vote, VoteSelection, Comment from django.contrib import messages import random
-
신규 투표 생성
@login_required def create(request): if request.method == 'POST': form = VoteForm(request.POST) if form.is_valid(): vote = form.save(commit=False) vote.creator = request.user vote.save() messages.info(request, '질문이 생성되었습니다.') return redirect('eithers:detail', vote.pk) else: form = VoteForm() context = { 'form':form, } return render(request, 'eithers/form.html', context)
{% extends 'base.html' %}
{% load bootstrap4 %}
{% block content %}
<h1 class="text-center">New Vote</h1>
<hr>
<form action="" method = "POST">
{% csrf_token %}
{% bootstrap_form form %}
{% bootstrap_button "Save" button_type="submit" button_class="btn-primary" %}
</form>
{% endblock %}
-
전체 투표 목록 조회
def index(request): votes = Vote.objects.all() context = { 'votes': votes, } return render(request, 'eithers/vote_list.html', context)
{% extends 'base.html' %} {% load bootstrap4 %} {% block content %} <h1 class="text-center">Votes</h1> <hr> {% if votes %} <table class="table"> <thead class="thead-dark"> <tr> <th scope="col">#</th> <th scope="col">Title</th> <th scope="col">Author</th> <th scope="col">Category</th> <th scope="col">Created_at</th> </tr> </thead> <tbody> {% for vote in votes %} <tr> <th scope="row">{{ vote.pk }}</th> <td><a href="{% url 'eithers:detail' vote.pk %}">{{ vote.title }}</a></td> <td>{{ vote.creator }}</td> <td>{{ vote.category }}</td> <td>{{ vote.created_at }}</td> </tr> {% endfor %} </tbody> </table> {% else %} <h3 class="text-center">작성된 투표가 없습니다.</h3> {% endif %} {% endblock %}
-
단일 투표 상세 조회 / 댓글 전체 조회 / 댓글 작성 form 표시
@login_required def detail(request, vote_pk): vote = get_object_or_404(Vote, pk=vote_pk) if request.method == 'POST': vs = VoteSelection() vs.vote = vote vs.user = request.user if 'item1' in request.POST: vs.selection = 1 vote.item1_num += 1 elif 'item2' in request.POST: vs.selection = 2 vote.item2_num += 1 vs.save() vote.save() messages.info(request, '투표에 성공하셨습니다!') return redirect('eithers:detail', vote.pk) else: comment_form = CommentForm() if vote.item1_num + vote.item2_num > 0: item1_ratio = round(vote.item1_num/(vote.item1_num + vote.item2_num)*100, 2) item2_ratio = round(vote.item2_num/(vote.item1_num + vote.item2_num)*100, 2) else: item1_ratio = item2_ratio = 0 try: voteselection = get_object_or_404(VoteSelection, user=request.user, vote=vote) if voteselection.selection == 1: myselection = vote.item1 else: myselection = vote.item2 except: myselection = '' context = { 'vote': vote, 'comment_form': comment_form, 'myselection': myselection, 'item1_ratio': item1_ratio, 'item2_ratio': item2_ratio, } return render(request, 'eithers/vote_detail.html', context)
{% extends 'base.html' %} {% load bootstrap4 %} {% block content %} <div class="container my-3 py-3"> <h1 class="text-center">{{ vote.title }}</h1> <hr> <ul> <li class="mb-0 "><b>작성자:</b> {{ vote.creator }}</li> <li class="mb-0 "><b>설명:</b> {{ vote.description }}</li> <li class="mb-0 "><b>카테고리:</b> {{ vote.category }}</li> <li class="mb-0 "><b>생성일시:</b> {{ vote.created_at }}</li> </ul> <hr> {% if myselection %} <h3 class="text-center">Result</h3> <h5 class="text-center">나의 선택: {{ myselection }} </h5> </h3> <div class="container"> <div class="row"> <div class="col-6 bg-primary text-white text-center p-3 rounded"> <h3 class="my-3">{{ item1_ratio }}%</h3> <h5 class="my-3">{{ vote.item1_num }}</h5> <h4 class="my-3">{{ vote.item1 }}</h4> </div> <div class="col-6 bg-danger text-white text-center p-3 rounded"> <h3 class="my-3">{{ item2_ratio }}%</h3> <h5 class="my-3">{{ vote.item2_num }}</h5> <h4 class="my-3">{{ vote.item2 }}</h4> </div> </div> </div> {% else %} <h3 class="text-center my-3">선택해주세요!</h3> <form class="container" action="{% url 'eithers:detail' vote.pk %}" method="POST"> {% csrf_token %} <div class="row"> <button class="col-6 m-0 py-5 bg-primary text-white btn-lg btn-block" type="submit" name="item1">{{ vote.item1 }}</button> <button class="col-6 m-0 py-5 bg-danger text-white btn-lg btn-block" type="submit" name="item2">{{ vote.item2 }}</button> </div> </form> {% endif %} </div> <div class="container border border-secondary rounded my-3 py-3"> <h5>댓글 | ({{ vote.comment_set.all.count }})</h4> <hr> {% if vote.comment_set.all %} <table class="table table-striped text-center"> <thead class="thead-dark"> <tr> <th scope="col">Content</th> <th scope="col">Author</th> <th scope="col">Selection</th> <th scope="col">Created_at</th> </tr> </thead> <tbody> {% for comment in vote.comment_set.all %} <tr> <td>{{ comment.content }}</td> <td>{{ comment.author }}</td> <td> {{ myselection }} </td> <td>{{ comment.created_at }}</td> </tr> {% endfor %} </tbody> </table> {% else %} <h5>댓글이 없습니다.</h5> {% endif %} {% if user.is_authenticated %} <form action="{% url 'eithers:comment_create' vote.pk %}" method="POST"> {% csrf_token %} {% bootstrap_form comment_form %} {% bootstrap_button "댓글 작성" button_type="submit" button_class="btn-primary" %} </form> {% endif %} </div> {% endblock %}
-
신규 댓글 생성
@login_required def comment_create(request, vote_pk): vote = get_object_or_404(Vote, pk=vote_pk) try: selection = get_object_or_404(VoteSelection, user=request.user, vote=vote) form = CommentForm(request.POST) if form.is_valid(): comment = form.save(commit=False) comment.author = request.user comment.vote = vote comment.selection = selection comment.save() messages.info(request, '댓글이 생성되었습니다.') except: messages.info(request, '투표를 먼저 진행해주세요.') return redirect('eithers:detail', vote.pk)
-
랜덤하게 투표 반환(해당 사용자가 아직 투표하지 않은 투표들 중에)
@login_required def random_vote(request): votes = Vote.objects.all() possible = [] for vote in votes: try: get_object_or_404(VoteSelection, user=request.user, vote=vote) except: possible.append(vote) if possible: vote = random.choice(possible) return redirect('eithers:detail', vote.pk) else: messages.info(request, '모든 질문에 답하셨네요! :)') return redirect('eithers:index')
-
messages framework 사용 방법(
base.html
)# settings.py from django.contrib.messages import constants as messages_constants MESSAGE_LEVEL = messages_constants.DEBUG MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' # views.py from django.contrib import messages ... messages.info(request, '해당 댓글을 삭제할 수 없습니다.')
<!-- 이 코드를 base.html에 한 번만 넣어주면 된다! --> {% if messages %} {% for message in messages %} <div class="row" style="margin:20px auto;"> <div class="col-sm-12"> <div class="alert alert-info"> <strong class="text-center">{{ message.message }}</strong> </div> </div> </div> {% endfor %} {% endif %}
-
M : N 관계 구현
class VoteSelection(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) vote = models.ForeignKey(Vote, on_delete=models.CASCADE) selection = models.IntegerField()
- 기존의 테이블(M, N)에서 컬럼을 추가하는 방식으로는 구현이 불가능하다.
- 결국 M 테이블과 N 테이블을 참조하는 새로운 테이블을 생성 해야한다.
- 주어진 상황 별로 서로 다른 결과값을 뱉어내는 걸 구현하는 게(코드 분기) 어려웠다. 가능하면 html 단 이전, view 혹은 model 단계에서 값을 분기하는 것이 편리한 것 같다.
- M : N 관계 구현할 때 머릿속으로 개념이 잘 잡히지 않아 애를 먹었다. 핵심은 별도의 테이블을 생성해야 한다는 것!