내가 한 노력들

[ python ] django로 웹만들기 본문

IT 공부/python

[ python ] django로 웹만들기

JONGI-N CHOI 2021. 1. 1. 20:54

django를 사용하기 위해선 우선 당연히 python이 설치 되어있어야 한다. 

 

Environments and Packages

그리고 가상환경을 만들어서 그곳에서 django의 환경을 만들어서 사용하면, 기존에 python환경에는 영향을 주지 않고 사용할 수 있다. 

 

가상환경은 가상환경만다 다른 개발환경을 구축할 수 있다. 

 

myenv 라는 파일을 생성해서 가상 개발환경을 구축하고, 그곳에서 django를 이용한다. 

 

 

그렇기 때문에 기존의 환경에서는 django version을 확인해봐도 No module named dijango로 나온다. 

하지만 , 가상환경을 들어가 보면 

 

이렇게 3.1.4 버전이라고 나오는 것을 확인해 볼 수 있다. 

 

 

django cycle

web server

클라이언트는 우리가 만든 django 사이트에 접속하기 위해서 web server를 거치게 된다. 

종류로는 Nginx, Apache 가 있다. 

django에는 개발을 하기 위한 경량 web server가 있다. 

이 것을 상용화 하기위해서는 위의 Nginx라던가 Apache를 사용한다. 

 

WSGI

웹서버와 장고 프레임웍을 연결하기 위해 사용

 

REQUEST -> URL RESOLUTION 

bill1224.tistory.com/manage/newpost/?type=post&returnURL=%2Fmanage%2Fposts%

클라이언트는 요청을 하기위해서 위의 URL처럼 보내는데 이것을 잘게 나누는 과정을 거친다 .

이런 것을 파싱(Parsing)이라고 한다. 

 

VIEW

잘게 나눠진 주소들은 역할에 맞게 VIEW에 이동한다 .

웹 어플리케이션에 각각 맞는 작업을 하기 위한 , 작성해놓은 코드에 맞게 동작한다. 

DB작업등등 여러가지 작업이 행해진다.

 

TEMPALTE 

디자인 과정

 

RESPONSE 

다시 서버를 거쳐서 클라이언트에게 전달해주게 된다. 


프로젝트 만들기 

django-admin startproject mysite

원하는 위치에 프로젝트 파일이 생기게되고, 그 안에있는 파일들은 


개발 서버

python manage.py runserver

이 명령어를 입력하게 되면 서버가 구동되고, 주소가 나온다. 

그 주소를 입력하면 내가 만드는 django 페이지가 나오게 된다. 

 


앱 만들기 

python manage.py startapp polls

이 명령어를 입력하게 되면 해당 프로젝트에 polls라는 앱을 생성하게 된다. 


첫 번째 뷰 작성하기

polls/ views.py

from django.http import HttpResponse


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

 

polls / urls.py

from django.urls import path

from . import views

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

 

 

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),
]

다음 단계는, 최상위 URLconf 에서 polls.urls 모듈을 바라보게 설정합니다. mysite/urls.py 파일을 열고, django.urls.include를 import 하고, urlpatterns 리스트에 include() 함수를 다음과 같이 추가합니다.

 

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

include()에 숨은 아이디어 덕분에 URL을 쉽게 연결할 수 있습니다. polls 앱에 그 자체의 URLconf(polls/urls.py)가 존재하는 한, 《/polls/》, 또는 《/fun_polls/》, 《/content/polls/》와 같은 경로, 또는 그 어떤 다른 root 경로에 연결하더라도, 앱은 여전히 잘 동작할 것입니다.

 

 

동작순서

 

"http://127.0.0.1:8000/polls/" 주소가 입력되면 , 파싱을 하여 polls/를 감지하면 , polls.urls로 보낸다.

 

polls.urls 내부

path('', views.index, name='index')

polls/-> 뒤에 아무런 내용이  없을 때에, views.index로 view 내부로 연결을  시킨다. 

 

views 내부 

return HttpResponse("Hello, world. You're at the polls index."

Hello, world. You're at the polls index.라는 문구를 클라이언트에게 Response해주는 것이다. 

 

 


DB 설치 

django에는 기본적인 경량 DB인 sqlite을 제공하고 있다. 

 

기본 어플리케이션들 중 몇몇은 최소한 하나 이상의 데이터베이스 테이블을 사용

python manage.py migrate

데이터베이스 테이블을 만들기 위한 명령어 

 

 

데이터베이스 모델 만들기 

설문 조사 앱에서 두 가지 모델, Question Choice. A Question에 질문과 게시 날짜가 있습니다. A Choice에는 선택 텍스트와 투표 집계라는 두 개의 필드가 있습니다. 각각 Choice Question.

이러한 개념은 Python 클래스로 표현됩니다. polls/models.py다음과 같이 파일을 편집하십시오 .

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

여기에서 각 모델은 django.db.models.Model. 각 모델에는 여러 클래스 변수가 있으며, 각 변수는 모델의 데이터베이스 필드를 나타냅니다.

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

각각의 Field 인스턴스의 이름(question_text 또는 pub_date)은 기계가 읽기 좋은 형식(machine-friendly format)의 데이터베이스 필드 이름입니다. 이 필드명을 Python 코드에서 사용할 수 있으며, 데이터베이스에서는 컬럼명으로 사용할 것입니다.

Field 클래스의 생성자에 선택적인 첫번째 위치 인수를 전달하여 사람이 읽기 좋은(human-readable) 이름을 지정할 수도 있습니다. 이 방법은 Django 의 내부를 설명하는 용도로 종종 사용되는데, 이는 마치 문서가 늘어나는 것 같은 효과를 가집니다. 만약 이 선택적인 첫번째 위치 인수를 사용하지 않으면, Django 는 기계가 읽기 좋은 형식의 이름을 사용합니다. 이 예제에서는, Question.pub_date 에 한해서만 인간이 읽기 좋은 형태의 이름을 정의하겠습니다. 그 외의 다른 필드들은, 기계가 읽기 좋은 형태의 이름이라도 사람이 읽기에는 충분합니다.

몇몇 Field 클래스들은 필수 인수가 필요합니다. 예를 들어, CharField 의 경우 max_length 를 입력해 주어야 합니다. 이것은 데이터베이스 스키마에서만 필요한것이 아닌 값을 검증할때도 쓰이는데, 곧 보게 될것입니다.

또한 Field 는 다양한 선택적 인수들을 가질 수 있습니다. 이 예제에서는, default 로 하여금 votes 의 기본값을 0 으로 설정하였습니다.

마지막으로, ForeignKey 를 사용한 관계설정에 대해 설명하겠습니다. 이 예제에서는 각각의 Choice 가 하나의 Question 에 관계된다는 것을 Django 에게 알려줍니다. Django 는 다-대-일(many-to-one), 다-대-다(many-to-many), 일-대-일(one-to-one) 과 같은 모든 일반 데이터베이스의 관계들를 지원합니다

 

 

 

모델을 작성을 해놨다.  migrations라는 장소에 이 모델들을 db내에 테이블을 생성할 수 있도록 설계도를 생성하는 작업

migrations라는 파일이 생성 

 

 

db내에 실제 테이블을 생성하는 작업 

python manage.py migrate

 


API 가지고 놀기

이제, 대화식 Python 쉘에 뛰어들어 Django API를 자유롭게 가지고 놀아봅시다. Python 쉘을 실행하려면 다음의 명령을 입력합니다

python manage.py shell

개발자가 필요로하는 데이터를 뽑아낼 수 있도록 만들어낸 함수 서버에게 데이터베이스에게 데이터를 입력할 수 있도록 만들어 놓은 함수 

 

from polls.models import Choice, Question

choice, question 모델을 사용을 하겠다. 

Question.objects.all()

현재 Question내에 모든 데이터를 가져와라라는 의미고 현재에는 아무런 데이터가 없기 때문에 빈 []가 나온다. 

 

Question 모델 안에는 게시 날짜가 있기 때문에 등록하기 위해서는 timezone를 import한다. 

from django.utils import timezone

 

q = Question(question_text="What's new?", pub_date=timezone.now())

질문을 하나 등록 한다. 

q.save()

질문을 저장하고 

q.id

id를 검색해보면 1이 나온다.  id 값은 입력하지 않아도 django에서 자동으로 매겨주는 숫자 

q.question_text

q에 입력된 text인 "what's new?"가 나온다. 

q.question_text = "What's up?"

text를 변경할 수 있다. 

Question.objects.all()

으로 데이터 갯수를 확인해보면 

<QuerySet [<Question: Question object (1)>]>

1개가 있다고 나온다. 

 

 

사용자가 볼 때에는 어떤 데이터인지 잘 구분이 되지않는다. 

여기서 잠깐. <Question: Question object (1)>은 이 객체를 표현하는 데 별로 도움이 되지 않습니다. (polls/models.py 파일의) Question 모델을 수정하여, __str__() 메소드를 Question Choice에 추가해 봅시다.

-> 그러면 보고싶은 문구를 볼 수 있다. 

 

polls/models.py

from django.db import models

class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

당신의 모델에 __str__() 메소드를 추가하는것은 객체의 표현을 대화식 프롬프트에서 편하게 보려는 이유 말고도, Django 가 자동으로 생성하는 관리 사이트 에서도 객체의 표현이 사용되기 때문입니다

 

polls/models.py

import datetime

from django.db import models
from django.utils import timezone


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

import datetime은 Python의 표준 모듈인 datetime 모듈을, from django.utils import timezone은 Django의 시간대 관련 유틸리티인 django.utils.timezone을 참조하기 위해 추가한 것입니다. 만약 Python에서 시간대를 조작하는 방법에 대해 익숙하지 않다면, 시간대 지원 문서에서 더 많은 것을 배울 수 있습니다.

 

 

변경된 사항을 저장하고, python manage.py shell를 다시 실행

 

from polls.models import Choice, Question
Question.objects.all()

을 입력하게 되면 전에는  <QuerySet [<Question: Question object (1)>]> 이렇게 나왔지만, 

<QuerySet [<Question: What's up?>]>

이런 식으로 text의 내용을 보여주게 된다. 

 

쿼리문을 사용해서 원하는 값을 가져올 수 있다. 

Question.objects.filter(id=1)

id값이 1인 Question 의 데이터값 

Question.objects.filter(question_text__startswith='What')

text가 what으로 시작하는 question의 데이터값 


admin

 

 

아이디 생성후 

 

$ python manage.py runserver

서버를 구동시킨다. 

 

 http://127.0.0.1:8000/admin/ 에 접속로 접속하면 된다. 

 

 

현재의 admin에는 생성했던 모델이 안보인다. 

연동해주기 위해서 아래의 코딩을 입력하면 된다. 

 

from django.contrib import admin

from .models import Question

admin.site.register(Question)

 


템플릿 

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))

 

template = loader.get_template('polls/index.html')

템플릿을 로드해서 리스폰스해주면된다. 

 

context = {
        'latest_question_list': latest_question_list,
    }

여기서 context를 통해서 템플릿의 데이터를 전달해준다 .

 

latest_question_list으로 데이터를 템플렛에다가 전달을하고 템플릿에서 이 latest_question_list 데이터를 사용한다. 

 

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 %}

템플릿을 만들경우에는 해당 앱 폴더를 다시 만들고 그 안에 만들어야지만 템플릿가 인식이 가능하다. 

따로 앱의 폴더명을 만들어주지 않으면 어떤 앱에 대한 템플릿인지 인식을 못한다.

 

{% if latest_question_list %}

이 데이터를 가지고 

<ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>

리스트를 만드는 작업을 하는 것이다 .

 

 

render()를 이용해서 더 간단히 사용

polls/views.py

from django.shortcuts import render

from .models import Question


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)

render() 함수는 request 객체를 첫번째 인수로 받고, 템플릿 이름을 두번째 인수로 받으며, context 사전형 객체를 세전째 선택적(optional) 인수로 받습니다. 인수로 지정된 context로 표현된 템플릿의 HttpResponse 객체가 반환됩니다.

 

 

404에러 일으키기 

개발에서 에러를 일으키는 작업은 매우 중요하다. 

 

polls/views.py

from django.http import Http404
from django.shortcuts import render

from .models import Question
# ...
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})

polls/templates/polls/detail.html

{{ question }}

 

Qustion_id 값이 존재하지 않는 Question을 가져올려고 할 경우에는 에러를 띄우게 된다. 

 

예를 들어서 Question모델안에 현재 q라는 1개의 데이터가 존재하는 경우에는 http://127.0.0.1:8000/polls/1/

의 주소를 입력하면 detail.html 템플릿을 통해서 question의 데이터를 볼 수 있다. 

 

하지만, 데이터가 1개 밖에 없는 상황에서 http://127.0.0.1:8000/polls/1/ 를 입력하게 되면 오류처리를 해서 

raise Http404("Question does not exist")

위의 오류 메세지를 템플릿의 question을 통해서 오류메세지를 전달한다. 

 

 

get_object_or_404()를 이용해서 간략하게 오류 만들기 

from django.shortcuts import get_object_or_404, render

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

또한, get_object_or_404() 함수처럼 동작하는 get_list_or_404() 함수가 있습니다. get() 대신 filter() 를 쓴다는 것이 다릅니다. 리스트가 비어있을 경우, Http404 예외를 발생시킵니다.

 

render()처럼 좀 더 간략하게 위의 코딩 처럼 같은 동작을 할 수 있게 한다. 

 

 

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>
question.choice_set.all

우리는 question 모델과 choice 모델 두개를 만들었고, choice 모델은 question 모델의 외래키를 받고 있는 상태이다.

위 코딩은, question 오브젝트에 속해있는 choice들을  모두 가져오라는 의미이다. 

 

<li>{{ choice.choice_text }}</li>

그래서 가져온 choice들의 text 내용을 list를 만들어서 보여주게 된다. 


polls/index.html 템플릿에 링크를 적으면, 이 링크는 다음과 같이 부분적으로 하드코딩된다

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

이러한 강력하게 결합되고 하드코딩된 접근방식의 문제는 수 많은 템플릿을 가진 프로젝트들의 URL을 바꾸는 게 어려운 일이 된다는 점입니다. 그러나, polls.urls 모듈의 path() 함수에서 인수의 이름을 정의했으므로, {% url %} template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성을 제거할 수 있습니다.

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

장고에서는 url마다 이름을 명시해줄 수 있다. 

템플릿에 그 이름을 써주게되면 하드코딩된 url이 변경되더라도 고유 이름을 가지고 있기 때문에 템플릿 url내에서 소스코딩 변경을 하지 않아도 된다. 

 

polls/urls.py

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

단, name이름 중에서 다른 앱에서도 detail이라는 이름을 사용할 수 도 있다. 그렇게 이름이 겹치는 경우를 방지하기 위해서 app의 url 이름을 정의해주는 방법이 있다. 

app_name = 'polls'

app_name을 만들어주고

<li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>

위와같이 코딩을 변경해주면 된다. 


vote기능을 추가 

<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>

{{forloop.counter}}는 for 태그가 반복한 횟수를 말한다.

 

장고 홈페이지에서 보이는 모습

{% 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>

form 태그를 이용해서 클라이언트가 전송하는 데이터를 받아온다.

{% csrf_token %}

 

해킹방지  사이트간 위조방지를 위해서 form태그를 사용할 때마다 이것을 사용하면 된다 

 

{% for choice in question.choice_set.all %}

기존과 마찬가지로 question을 외래키로 갖는 모든 choice를 가져와서 for문을 돌린다. 

<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">

input 태그를 통해서 radio를 만들고 이름은 "choice"라고 설정, 그리고 value 값이 존재한다 . 

이 value값은 submit 되었을 때 이 정보가 서버로 날라간다. 

 

<label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>

label태그를 이용해서 choice의 text 내용을 보여준다. 

 

<input type="submit" value="Vote">

submit 버튼이 있고 이름은 vote로 한다

<form action="{% url 'polls:vote' question.id %}" method="post">

이 submit 버튼이 눌리게되면 form 태그에서 지정된 url로 데이터가 전송이 된다. 

여기서도 마찬가지로 하드코딩을 피하기 위해서 앱이름과 url 이름을 이용해서 url을 할당한다. 

 

그럼이제 view의 vote() 함수로 이동 

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'])
    except (KeyError, Choice.DoesNotExist):
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
question = get_object_or_404(Question, pk=question_id)

받은 인자값 question_id 값을 통해서 question 데이터값을 불러온다. 

 

try:
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })

try문을 사용해서 question이 없는경우에는 예외처리를 해준다. 

 

 else:
        selected_choice.votes += 1
        selected_choice.save()

        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

question가 존재한 경우에는 선택된 choice에 votes 값이 +1 이 되고 저장을한다. 

 

그다음 

return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

HttpResponseRedirect를 사용하는데 

 

post로 view가 호출된 경우에는HttpResponseRedirect 를 사용해줘야한다.  

 

post와 HttpResponseRedirect는 세트다.

 

polls앱의 results로 인자값 question.id를 넘겨준다.

 

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

results에서는 전해받은 인자값 question_id를 통해서 question을 찾고 restuls.html로 값을 보낸다.

 

polls/templates/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>

장고 홈페이지에서 보이는 모습

<h1>{{ question.question_text }}</h1>

우선 제목으로 question의 text내용이 나오고 

 

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

question을 외래키로하는 choice들을 모두 가져와서 for문을 돌리고 하나하나 리스트를 만든다. 

 

<li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>

리스트는 choice의 text 이름과 뒤에 텍스트 "--" 를 붙힌다. 

그다음,  choice.votes를 통해서 vote의 갯수를 불러오고 뒤에 문자열 "vote"를 붙히는데 여기서 pluralize를 사용했다. 

 

pluralize

vote{{ choice.votes|pluralize }}

pluralize는 vote의 값의 따라서 접미사를 변경해주기 위함이다.  값이 1인 경우에는 그대로 vote가 나오지만,

값이 0또는  1보다 큰 경우에는 복수 접미사 "s"가 붙어서 "votes"로 바뀌게 된다. 

 

vote{{ choice.vote|pluralize:"es" }}

초기값은 "s" 지만 위와 같은 방법으로 "es"등으로 접미사를 변경할 수 있다. 


제너릭 뷰 사용하기 Class화 하기 

쉽게 말해서 지금 함수로만 만들었던 코딩을 class화 시킴으로써 더 관리하기 쉽도록 만들어준다. 코드도 줄어듦

 

 

기존코딩

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)
    
def detail(request, question_id):
	question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})
    
def results(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/results.html', {'question': 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'

비교해보면 코드의 내용이 짧아진 것을 볼 수 있다. 


자동화 테스트

테스트를 만들어야 하는 이유?

1. 테스트를 통해 시간을 절약 할 수 있습니다

테스트를 작성하는 작업은 어플리케이션을 수동으로 테스트하거나 새로 발견된 문제의 원인을 확인하는 데 많은 시간을 투자하는 것보다 훨씬 더 효과적입니다.

 

2. 테스트는 문제를 그저 식별하는 것이 아니라 예방합니다.

테스트가 없으면 어플리케이션의 목적 또는 의도 된 동작이 다소 불투명 할 수 있습니다. 심지어 자신의 코드 일 때도, 정확히 무엇을하고 있는지 알아 내려고 노력하게 됩니다.

 

테스트는 이 불투명함을 바꿉니다. 그들은 내부에서 코드를 밝혀 내고, 어떤 것이 잘못 될 때, 그것이 잘못되었다는 것을 깨닫지 못했다고 할지라도, 잘못된 부분에 빛을 집중시킵니다.

 

-> 현재 앱의 기능을 좀 더 명확하게 할 수 있는 작업이다. 

 

3. 테스트가 코드를 더 매력적으로 만듭니다.

테스트 작성을 시작해야하는 또다른 이유는 다른 개발자들이 당신의 소프트웨어를 사용하는것을 진지하게 고려하기 전에 테스트 코드를 보기를 원하기 때문입니다.

4. 테스트는 팀이 함께 일하는것을 돕습니다.

이전의 내용은 어플리케이션을 유지 관리하는 단일 개발자의 관점에서 작성되었습니다. 복잡한 애플리케이션은 팀별로 유지 관리됩니다. 테스트는 동료가 실수로 코드를 손상시키지 않는다는 것을 보증합니다 (그리고 당신이 동료의 코드를 모르는새에 망가트리는것도). 장고 프로그래머로서 생계를 꾸려 나가려면 테스트를 잘해야합니다!

 

기본 cmd창을 이용해서 버그를 테스트해 볼 수 있다. 

python manage.py shell

 

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question

테스트에 필요한 라이브러리를 불러온다. 

future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))

질문을 하나 만드는데, 30일뒤인 미래의 질문을 하나 제작을한다. 

future_question.was_published_recently()
def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

만들어둔 was_published_recently() 함수를 사용해보면 True값이 나온다. 

미래의 질문은 등록되면 안되는 것인데 참 값이 나오므로 버그가 있는 것

 

자동으로 버그 테스트하기

위의 방법은 cmd창에서 직접 명령어를 하나하나 입력해가며 테스트하는 방법이고 , 이번에는 editer를 사용해서 자동으로 버그를 테스트하기 위한 코딩을한다. 

 

polls/tests.py

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question


class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is in the future.
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

django의 앱에는 tests.py라는 테스트를 할 수 있는 파일이 있어서 그 곳에 테스트하고 싶은 내용을 입력하면 된다.

테스트를 진행할 함수 이름 앞에는 test를 붙혀줘야만 한다. 

time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)

위의 코딩을 살펴보면 현재로부터 30일 뒤의 data로 question을 만든 다음에 

self.assertIs(future_question.was_published_recently(), False)

assertIs()를 이용해서 값이 False가 나오는지를 확인하는 작업이다. 

 

 

그러면 이젠 테스트를 진행하기 위해서 cmd 창을 키고 test를 실행시켜본다. 

python manage.py test polls

위의 명령어를 입력하게 되면 tests.py파일을 실행시킴으로써 자동으로 테스트가 진행이 된다.

 

실행 결과는 FALIED가 나오고 있다. 

Ran 1 test << 총 1개의 테스트가 진행이 되었고 FAILED (failures=1)  테스트 중에서 1개를 실패했다는 내용이다. 

 

그럼 결국은, 원래대로는 미래의 날짜일 경우에는 Question이 만들어지면 안되지만, 만들어지고 있다는 말이기 때문에 코딩을 수정해줘야한다. 

 

def was_published_recently(self):        
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now

만들어진 question의 date가 현재의 시간으로 부터 하루전 부터 , 지금 현재까지 안에 들어와지만 True가 되도록 했다. 

 

그리고 다시 테스트를 진행해 보면

 

Ran 1 test , 1개의 테스트가 진행 되었고

OK, 테스트가 성공했다. (버그가 없다.) 

 

 

테스트를 더욱 복잡하게 

테스트는 코드가 복잡해질 수록 앱의 기능이 좀 더 명확해지고 도움이 된다. 

테스트의 코드는 길면 길수록 좋다. 

class QuestionModelTests(TestCase):

    def test_was_published_recently_with_old_question(self):
        """
        was_published_recently() returns False for questions whose pub_date
        is older than 1 day.
        """
        time = timezone.now() - datetime.timedelta(days=1, seconds=1)
        old_question = Question(pub_date=time)
        self.assertIs(old_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() returns True for questions whose pub_date
        is within the last day.
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)
        

하루 하고도 1초가 지났을 경우의 Question과, 하루가 지나기 1초전의 Question을 서로 각각 만들어봐서 테스트를 진행해 본다. 

 

Ran 2 tests, 2개의 테스트가 진행이 되었고

OK, 테스트가 성공했다. 

 

 

 

 

 


뷰 테스트

 

Django는 뷰 레벨에서 코드와 상호 작용하는 사용자를 시뮬레이트하기위해 테스트 클라이언트 클래스 Client를 제공합니다. 이 테스트 클라이언트를 tests.py또는 shell에서 사용할 수 있습니다.

 

python manage.py shell

 

>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()

response.context와 같은 response의 추가적인 속성을 사용할수 있게 하기위해서 setup_test_environment()를 사용하여 템플릿 렌더러를 설치합니다. 이 메소드는 테스트 데이터베이스를 셋업하지 않습니다. 그렇기 때문에 테스트는 현재 사용중인 데이터베이스 위에서 돌게되며 결과는 데이터베이스에 이미 만들어져있는 질문들에 따라서 조금씩 달라질 수 있습니다.

 

>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
>>> response = client.get('/')

http://127.0.0.1:8000/ << 서버에 접근을 한다. 

 

response.status_code

위의 사진대로 url "http://127.0.0.1:8000/"을 가게되면 404번 에러가 나오기 때문에 코드의 실행결과도 "404"가 출력 

 

 

>>> from django.urls import reverse
>>> response = client.get(reverse('polls:index'))
>>> response.status_code

위의 코딩을 입력하게되면 해당 "http://127.0.0.1:8000/polls/" 주소창으로 가게되고, status_code는 "200"이 나온다. 

 

response.content

위의 코딩은 body안에 있는 content 내용이 나온다 

b'\n <ul>\n \n <li><a href="/polls/1/">What&#x27;s up?</a></li>\n \n </ul>\n\n'

 

 

 

위의 내용들을 이용해서 editer에서 코딩을 작성하여 테스트해보기 

polls/views.py

def get_queryset(self):
    """
    Return the last five published questions (not including those set to be
    published in the future).
    """
    return Question.objects.filter(
        pub_date__lte=timezone.now()
    ).order_by('-pub_date')[:5]

filter 를 위해 조건을 맞는 것을 필터링 해줌 

 

pub_date__lte=timezone.now(

__lte는  "<"의 의미로써, 현재와 같거나 적은 date를 의미함 

 

polls/tests.py

def create_question(question_text, days):
    """
    Create a question with the given `question_text` and published the
    given number of `days` offset to now (negative for questions published
    in the past, positive for questions that have yet to be published).
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)


class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        """
        If no questions exist, an appropriate message is displayed.
        """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_past_question(self):
        """
        Questions with a pub_date in the past are displayed on the
        index page.
        """
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_future_question(self):
        """
        Questions with a pub_date in the future aren't displayed on
        the index page.
        """
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

    def test_future_question_and_past_question(self):
        """
        Even if both past and future questions exist, only past questions
        are displayed.
        """
        create_question(question_text="Past question.", days=-30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

    def test_two_past_questions(self):
        """
        The questions index page may display multiple questions.
        """
        create_question(question_text="Past question 1.", days=-30)
        create_question(question_text="Past question 2.", days=-5)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question 2.>', '<Question: Past question 1.>']
        )


class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        The detail view of a question with a pub_date in the future
        returns a 404 not found.
        """
        future_question = create_question(
            question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        The detail view of a question with a pub_date in the past
        displays the question's text.
        """
        past_question = create_question(
            question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question.id,))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text)

위의 코딩은 총 5가지의 경우의 수를 만들어서 자동으로 테스트를 하기위한 코드이다 .

 

def create_question(question_text, days):
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

각 조건마다, 새로운 question을 생성하기 위해서 만든 함수이다. 

 

맨 위에서 부터 , question이 없을 때, 30일전 날짜의 question이 하나 존재할 때, 30일후 날짜의 question이 하나 존재할 경우, 30일전/후의 날짜의 question이 하나씩 총2개 존재할 경우, 30일전, 5일전 날짜의 question이 총 2개 존재할 경우

 

이렇게 5개의 경우의 수에 대한 테스트이다. 

 

 

question이 없을 때

class QuestionIndexViewTests(TestCase):
    def test_no_questions(self):
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.")
        self.assertQuerysetEqual(response.context['latest_question_list'], [])

response를 해보면 사이트의 상태는 "200"이 호출되고 있지만, 안의 내용은 존재하지 않는다. 

따라서 response.context['latest_question_list']로 최근 question을 검색해도  [] 빈 값만 나오게 된다. 

 

과거의 question이 하나 존재할 때

def test_past_question(self):
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        )

과거의 question은 filter에 걸리지 않고 잘 동작이 되므로 

response.context['latest_question_list']을 입력했을 경우에는 ['<Question: Past question.>']의 값이 제대로 출력

 

 

위와 같은 방법으로 테스트를 진행이 되고, 실제로 테스트를 돌려보면 

Ran 5 tests, 5개의 테스트가 진행

OK, 테스트 성공 


스타일 시트, 이미지 추가하기 

polls/static/polls/style.css

li a {
    color: green;
}

body {
    background: white url("images/background.gif") no-repeat;
}

style.css 파일은  새로 static 라는 폴더 경로를 설정한뒤에 템플릿을 만들 때와 똑같이 앱이름으로 된 폴더명을 하나 더 만들고 그안에 style.css 파일을 넣는다 

 

이미지 파일은 "polls/static/polls/images/이미지파일"의 경로로 만들면 된다. 

 

polls/templates/polls/index.html

{% load static %}

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">

위의 코딩은 그리고 static 디렉토리와 템플릿을 연동시켜주기 위한 코드이다. 

 

runserver를 하고 홈페이지를 확인해보면 css가 적용되어 있는 것을 볼 수 있다. 


admin Customizing

 

Question 모델 admin 변경

polls / admin.py 

from django.contrib import admin

from .models import Question


class QuestionAdmin(admin.ModelAdmin):
    fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)

 

 

왼쪽은 변경전, 오른쪽이 변경 후

 

fields = ['pub_date', 'question_text']

필드의 순서 대로 'pub_date'가 먼저나오고 'question_text'가 나중에 나오므로 순서가 바뀜

 

 

fieldset으로 분할

단지 2개의 필드만으로는 인상적이지는 않지만, 수십 개의 필드가 있는 관리 폼의 경우에는 직관적인 순서을 선택하는 것이 사용 편리성의 중요한 부분입니다.

수십 개의 필드가 있는 폼에 관해서는 폼을 fieldset으로 분할하는 것이 좋습니다

polls / admin.py 

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

admin.site.register(Question, QuestionAdmin)

 

 

왼쪽과 다르게 오른쪽에는 pub_date부분에 data information이라는 list가 하나 생긴 것을 볼 수 있다. 

 

fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date']}),
    ]

question_text에는 설정을 None으로 했기 때문에 이름이 없는 필드가 생겼다.  

pub_date는 'Date information' 이라는 이름을 설정 했기 때문에 이름을 가진 필드가 생겼다. 

 

 

 

원래 POLLS이라는 앱안에는 두가지의 모델이 있었고 admin에서도 따로따로 list가 되어있었다. 

하지만 Choices라는 모델은 Questions의 외래키를 받아 만들어지는 것이므로, Questions 안에서도 choice를 같이 설정 할 수 있도록 한가지로 묶을 것이다. 

 

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3


class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        (None,               {'fields': ['question_text']}),
        ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
    ]
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)

 

Question을 들어가보면 그 안에 choice를 설정할 수 있는 list가 생긴 것을 볼 수 있다. 

 

class ChoiceInline(admin.StackedInline):
    model = Choice
    extra = 3

 

 

extra가 의미하는 것은 choice 한번에 총 몇개까지 보여줄 것인가를 의미한다. 

 

inlines = [ChoiceInline]

기존에 있던 QuestionAdmin class에 위처럼 inlines로 만들어놓은 choiceinline을 추가해주면 된다. 

 

 

StackedInline -> TabularInline

class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 3

StackedInline 대신에 TabularInline을 사용하면, 관련된 객체는 좀 더 조밀하고 테이블 기반 형식으로 표시됩니다:

 

관리자 변경 목록(change list) 커스터마이징

 

위의 사진은 기본 관리자 변경 목록의 화면

 

class QuestionAdmin(admin.ModelAdmin):
    # ...
    list_display = ('question_text', 'pub_date')

위의 사진은 list의 필드를 추가해서 Question_text와 Date published가 보이도록 변경한 것이다. 

was_published_recently도 추가해서 검사여부를 확인한다. 

 

polls / models.py

class Question(models.Model):
    # ...
    def was_published_recently(self):
        now = timezone.now()
        return now - datetime.timedelta(days=1) <= self.pub_date <= now
    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?

위와과 같이 해당 메소드 (polls/models.py)에 몇 가지 속성을 부여하여 향상시킬 수 있습니다:

 

 

was_published_recently.admin_order_field = 'pub_date'

field를 pub_date 순서대로 정렬한다. 

 

was_published_recently.boolean = True

was_published_recently의 값이 True인지 False인지에 따라서 이미지로 o , x를 띄운다. 

 

was_published_recently.short_description = 'Published recently?'

was_published_recently였던 text를 'Published recently?'으로 변경

 

list_filter 추가하기

list_filter = ['pub_date']

 

화면 오른쪽에 pub_date를 기준으로한 filter가 생긴 것을 볼 수 있다. 

 

 

search_fields 추가하기

search_fields = ['question_text']

 

list위에 question_text를 기준으로하는 검색 filter가 생긴 것을 볼 수 있다.

 

 

프로젝트의 템플릿 커스터마이징

Django의 템플릿 시스템을 사용하여 변경할 수 있습니다. Django 관리자는 Django 자체에 의해 구동되며 해당 인터페이스는 Django의 자체 템플릿 시스템을 사용합니다.

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [BASE_DIR / 'templates'],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]
'DIRS': [BASE_DIR / 'templates'],

우선, 프로젝트 settings.py에서 templates 부분의 "DIRS"설정을 변경해준다.

 

python -c "import django; print(django.__path__)"

위의 명령어를 입력하게 되면 django의 파일 위치를 알 수 있다. 

 

그렇게 "django/contrib/admin/templates/admin/" 위치에 있는 "base_site.html" 파일을 복사해서 

 

현제 진행중인 프로젝트 최상위 위치에서 "Templates/admin"디렉토리를 만들고 그안에 복사한 파일을 넣는다. 

 

base_site.html

{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}

기존에는 Django adminstaraing이었던 최상위 타이틀 문구를 >Polls Administration로 변경하면 변경된다. 

 

사진처럼 변경된 모습을 볼 수 있다. 


 

 

 

 

<참고자료>

docs.djangoproject.com/ko/3.1/intro/tutorial01/

 

첫 번째 장고 앱 작성하기, part 1 | Django 문서 | Django

Django The web framework for perfectionists with deadlines. Overview Download Documentation News Community Code Issues About ♥ Donate

docs.djangoproject.com

www.youtube.com/watch?v=9WctwW_Pe1o&list=PLi4xPOplIq7d1vDdLBAvS5PmQR-p6KwUz&index=2&ab_channel=%EB%AA%85%EC%A4%80MJ