본문 바로가기
Web/backend

Django polls 앱

by yongmin.Lee 2020. 10. 24.

1. 프로젝트 생성

$ django-admin startproject mysite

실행하면 다음과 같은 구조의 디렉토리(프로젝트) 생성

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py
  • manage.py: Django 프로젝트와 다양한 방법으로 상호작용 하는 커맨드라인의 유틸리티
  • mysite/ 디렉토리 내부에는 프로젝트를 위한 실제 Python 패키지들이 저장
  • mysite/__init__.py: Python으로 하여금 이 디렉토리를 패키지처럼 다루라고 알려주는 용도의 단순한 빈 파일
  • mysite/settings.py: 현재 Django 프로젝트의 환경 및 구성을 저장
  • mysite/urls.py: 현재 Django project 의 URL 선언을 저장합니다. Django 로 작성된 사이트의 "목차" 
  • mysite/asgi.py: An entry-point for ASGI-compatible web servers to serve your project. 
  • mysite/wsgi.py: 현재 프로젝트를 서비스하기 위한 WSGI 호환 웹 서버의 진입점

 

2. 서버 실행

$ python manage.py runserver

웹 브라우져에서 http://127.0.0.1:8000/ 을 통해 접속할 수 있습니다

 

3. App 생성

- app : Web application that does something

- project : collection of configuration and apps for a particular website.

# 투표 app 생성
# python manage.py startapp <app이름>
$ python manage.py startapp polls

3.1 실행하면 다음과 같으 구조의 디렉토리(app) 생성

3.2 App 등록

main project 폴더에서 settings.py의 INSTALLED_APPS에 생성한 app 이름을 등록한다.

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'polls.apps.PollsConfig'  # polls app 등록
]

 

4. View 작성

* 데이터를 적절히 가공하여 database에 저장하거나 template에 reponse 전송

4.1 "polls/view.py"를 열어 다음과 같은 파이썬 코드를 입력

# polls/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("<h1>Hello, world. You're at the polls index.</h1>")

4.2 뷰 호출

뷰를 호출하려면 이와 연결된 URL 이 있어야 하는데, 이를 위해 URLconf가 사용

polls 디렉토리에서 URLconf를 생성하려면, urls.py라는 파일을 생성하고 다음과 같이 작성

# polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

4.3 최상위 URLconf 에서 polls.urls 모듈을 바라보게 설정.

mysite/urls.py 파일을 열고, django.urls.include를 import 하고, urlpatterns 리스트에 include() 함수를 다음과 같이 추가

# mysite/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]
# path() 함수에는 2개의 필수 인수인 route 와 view, 2개의 선택 가능한 인수로 kwargs 와 name 까지 모두 4개의 인수가 전달

# include() 함수는 다른 URLconf들을 참조할 수 있도록 도와줍니다. 
# Django가 함수 include()를 만나게 되면, URL의 그 시점까지 일치하는 부분을 잘라내고, 
# 남은 문자열 부분을 후속 처리를 위해 include 된 URLconf로 전달합니다.

4.4 서버 실행

$ python manage.py runserver

브라우저에서 http://localhost:8000/polls/를 입력하면 다음과 같은 화면 확인 가능

 

5. Model 작성

- model이란 부가적인 메타데이터를 가진 데이터베이스의 구조(layout).

- models.py에서 Database와 관련된 다양한 역할을 수행

- 모델의 변경을 만드는 3 단계

  1. models.py 에서 모델을 변경하고,
  2. $ python manage.py makemigrations을 통해 이 변경사항에 대한 마이그레이션 생성
  3. $ python manage.py migrate 명령을 통해 변경사항을 데이터베이스에 적용

5.1 db구조 정의

#polls/models.py
from django.db import models

# 데이터베이스의 각 필드는 Field 클래스의 인스턴스로서 표현
# 이것은 각 필드가 어떤 자료형을 가질 수 있는지를 Django 에게 말해준다
# CharField 는 문자(character) 필드를 표현
# DateTimeField 는 날짜와 시간(datetime) 필드를 표현

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')
    
    # __str__() 메소드를 추가하는것은 객체의 표현을 대화식 프롬프트에서 편하게 보려는 이유
	# + Django 가 자동으로 생성하는 관리 사이트 에서도 객체의 표현이 사용
    def __str__(self):
        return self.question_text

class Choice(models.Model):
	# ForeignKey 를 사용한 관계설정
    # Choice 가 하나의 Question 에 관계된다는 것을 Django 에게 알려줌
    question = models.ForeignKey(Question, on_delete=models.CASCADE) 
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)
    
    def __str__(self):
        return self.choice_text
   
# 위와 같이 작성하면
# Django는 이 정보를 가지고 다음과 같은 일을 수행
# 1. 이 앱을 위한 데이터베이스 스키마 생성(CREATE TABLE 문)
# 2. Question과 Choice 객체에 접근하기 위한 Python 데이터베이스 접근 API를 생성

5.2 모델 활성화

$ python manage.py makemigrations polls

makemigrations 을 실행시킴으로서, 당신이 모델을 변경시킨 사실과(이 경우에는 새로운 모델 생성), 해당 변경사항을 migration으로 저장시키고 싶다는 것을 Django에게 알려준다

# migrate 명령어: migration들을 실행시켜주고, 자동으로 데이터베이스 스키마를 관리
$ python manage.py migrate

데이터베이스에 모델과 관련된 테이블을 생성

migrate 명령은 아직 적용되지 않은 마이그레이션을 모두 수집해 이를 실행하며(Django는 django_migrations 테이블을 두어 마이그레이션 적용 여부를 추적

이 과정을 통해 모델에서의 변경 사항들과 데이터베이스의 스키마의 동기화가 이루어진다.

5.3 쉘에서 데이터베이스에 데이터 저장

>>> from polls.models import Choice, Question  # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

 

6. Admin

6.1 관리자 생성

# createsuperuser 명령어 실행
$ python manage.py createsuperuser

# 관리자계정 생성
$ Username: admin
$ Email address: admin@example.com
$ Password: **********
$ Password (again): *********

# Superuser created successfully.

6.2 admin site 접속

$ python manage.py runserver

http://127.0.0.1:8000/admin/으로 접속하고 슈퍼유저(superuser) 계정으로 로그인

6.3 관리 사이트에서 poll app 연결

# polls/admin.py
from django.contrib import admin
from .models import Question

admin.site.register(Question)

모델에 대한 관리용 인터페이스를 모두 자동으로 생성

 

7. View 추가

7.1 polls/views.py 에 뷰를 추가

# polls/views.py 
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

7.2 새로운 뷰를 polls.urls 모듈로 연결

# polls/urls.py
from django.urls import path
from . import views

urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

# 브라우저에 "http://127.0.0.1:8000/polls/2/vote/" 를 입력하면
# mysite.urls에서 urlpatterns라는 변수를 찾고, 순서대로 패턴을 따라갑니다. 
# 'polls/'를 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 
# 남은 텍스트인 "2/"를 'polls.urls' URLconf로 전달하여 남은 처리를 진행합니다. 
# 거기에 '<int:question_id>/'와 일치하여, 결과적으로 detail() 뷰 함수가 호출

7.3 view에 로직 구현

★ 뷰에서 할 수 있는 일

요청된 페이지의 내용이 담긴 HttpResponse 객체를 반환

- Http404 같은 예외를 발생

- 데이터베이스의 레코드를 읽기

- Django나 Python에서 서드파티로 제공되는 템플릿 시스템을 사용

- etc

# example : 데이터베이스의 리코드 읽어서 해당 내용을 reponse로 전달하기
# polls/views.py
from django.http import HttpResponse
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

http://127.0.0.1:8000/polls/ 에 접속하면 데이터베이스 리코드를 출력한다

8. Template

8.1 template이 필요한 이유

뷰에서 페이지의 디자인을 코딩한 경우, 페이지가 보여지는 방식을 바꾸고 싶을 때마다 views.py에서 해당 Python 코드를 직접 편집해야만 하는 번거로움이 생긴다.

Django에서는 이 문제를 Template으로 해결하는데,  뷰에서 사용할 수 있는 Template을 따로 작성하면 , Python 코드로부터 디자인을 분리할 수 있다.

DjangoTemplates은 각 INSTALLED_APPS 디렉토리의 "templates" 하위 디렉토리를 탐색하여 기술된 렌더링 방식에 따라 페이지를 렌더링

8.2 polls 디렉토리에 templates라는 디렉토리생성 및 template 작성

# polls/templates/polls/index.html
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

8.3 템플릿을 이용하여 polls/views.py index 뷰를 업데이트

# polls/views.py

from django.http import HttpResponse
from django.template import loader
from .models import Question

# polls/index.html 템플릿을 불러온 후, context를 전달
# context는 템플릿에서 쓰이는 변수명과 Python 객체를 연결하는 사전형 값

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    template = loader.get_template('polls/index.html')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))
    
# 위의 코드를 render() 이용해 다음과 같이 작성 가능

from django.shortcuts import render

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

# ... detail, results, vote

http://127.0.0.1:8000/polls/ 에 접속하면 다음과 같이 템플릿이 적용됨을 확인 가능

8.4 detail템플릿 작성 및 404 error 발생시키기

현재 구현된 뷰는 요청된 질문의 ID 가 없어도 Http404 예외를 발생시키지 않는다

다음과 같이 구현하면 404예외 발생

# polls.views.py
from django.http import HttpResponse
from django.template import loader
from .models import Question
from django.http import Http404

def detail(request, question_id):
    try:
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

# 위 코드를 get_object_or_404()를 이용하여 다음과 같이 작성 가능

def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

detail 템플릿 작성

<!-- polls/templates/polls/detail.html -->
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

존재하는 id가 전달되는 경우

존재하지 않는 id가 전달되는 경우 : 404 ERROR 발생

8.5 템플릿에서 하드코딩된 URL 제거 

url 설정에 정의된 특정한 URL 경로들은 템플릿이 과다해질 경우, 수 많은 템플릿을 가진 프로젝트들의 URL을 바꾸는 게 어려운 일이 된다. 따라서 polls.urls 모듈의 path() 함수에서, {% url %} template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성을 제거해야 한다.

<!-- polls/templates/polls/index.html -->
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

8.6 namespace 설정

Django 프로젝트에서 많은 App을 가지고 있고 각각의 App의 모든 view들이 template에서 {% url %} template 태그를 사용하는 경우, 한 프로젝트에서 서로 다른 App들간의 어떤 view들의 이름이 동일하여 Django가 {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 알 수없는 문제가 존재한다!

따라서 URLconf에 namespace을 추가해야한다! polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 namespace 설정

# polls.urls.py
from django.urls import path
from . import views

app_name = 'polls' # polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 namespace 설정
urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),
    # ex: /polls/5/results/
    path('<int:question_id>/results/', views.results, name='results'),
    # ex: /polls/5/vote/
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

polls/index.html 템플릿의 기존 내용을 namespace로 나눠진 상세 뷰를 가리키도록 변경

{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

 

9. 제출된 데이터 처리하기

9.1 polls/detail.html을 수정하여, 템플릿에서 설문조사를 할 수 있는 form 작성

<!-- polls/template/polls.detail.html -->
<h1>{{ question.question_text }}</h1>

{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">
{% csrf_token %}
{% for choice in question.choice_set.all %}
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>

라디오 버튼 중 하나를 선택하여 폼을 제출하면, POST 데이터 인 choice = #( #은 선택한 항목의 ID)을 polls/vote에 전송

9.2 polls/views.py의 vote를 다음과 같이 정의하여 제출된 choice데이터를 이용하여 설문기능을 구현하고 설문조사 결과 페이지로 리다이렉트

# polls/views.py
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from .models import Choice, Question

# ...

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
	# request.POST 는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체
	# 이 경우, request.POST['choice'] 는 선택된 설문의 ID를 문자열로 반환 (request.POST 의 값은 항상 문자열)
        
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
        # 만약 POST 자료에 choice 가 없으면, request.POST['choice'] 는 KeyError 발생
        # 위의 코드는 KeyError 를 체크하고, choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시보여줌
        
        
    else:
        selected_choice.votes += 1
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
	# 설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환
	# HttpResponseRedirect 는 하나의 인수를 받습니다: reverse() 함수
	#  reverse() 함수는 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킴
  

9.3 Redirectoin

redirection되는 result 의 view

# polls/views.py
from django.shortcuts import get_object_or_404, render

def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': question})

redirection되는 result 의 template

<!-- polls/template/polls/results.html -->
<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

 

10. Generic views

위와 같은 view는 URL에서 전달 된 매개 변수에 따라 데이터베이스에서 데이터를 가져 오는 것과 템플릿을 로드하고 렌더링 된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타낸다

Django는 이런 매우 일반적인 view를 위해 "Generic VIew" 제공.

Generic VIew는 일반적인 패턴을 추상화하여 앱을 작성하기 위해 Python 코드를 작성하지 않아도 된다

10.1 views 수정 : 이전의 index, detail, results뷰를 제거하고 장고의 generic view 사용

# polls/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from .models import Choice, Question

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'


class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'


def vote(request, question_id):
    ... # same as above, no changes needed.
    
# ListView와 DetailView의 두 가지 제너릭 뷰를 사용
#각각 "개체 목록 표시" 및 "특정 개체 유형에 대한 세부 정보 페이지 표시" 개념을 추상화

DetailView 제너릭 뷰는 <app name>/<model name>_detail.html 템플릿을 사용

ListView 제네릭 뷰는 <app name>/<model name>_list.html 템플릿을 기본으로 사용

10.2 URLconf 수정

# polls/urls.py
from django.urls import path
from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

# polls/urls.py URLconf를 열어 두 번째와 세 번째 패턴의 경로 문자열에서 일치하는 패턴들의 이름이 
# <question_id> 에서 <pk> 로 변경

DetailView 제너릭 뷰는 URL에서 캡쳐 된 기본 키 값이``"pk"\ 라고 기대하기 때문에 ``question_id를 제너릭 뷰를 위해 pk로 변경합니다.

 

'Web > backend' 카테고리의 다른 글

Node.js - introduction  (0) 2020.10.24
인증(Authentication) & 인가(Authorization)  (0) 2020.10.24
HTTP, RESTful API  (0) 2020.10.24
Django 기본 개념  (0) 2020.10.24
Node.js 기본 개념 및 간단한 웹서버 구현  (0) 2020.10.24