From 579fd598228853820b07469841d5ce909958dc55 Mon Sep 17 00:00:00 2001 From: gywls517 Date: Sun, 22 Sep 2019 14:26:45 +0900 Subject: [PATCH 1/6] DB changed from SQLite to MySQL --- mysite/__pycache__/__init__.cpython-37.pyc | Bin 156 -> 156 bytes mysite/__pycache__/settings.cpython-37.pyc | Bin 2286 -> 2353 bytes mysite/__pycache__/urls.cpython-37.pyc | Bin 982 -> 982 bytes mysite/__pycache__/wsgi.cpython-37.pyc | Bin 557 -> 557 bytes mysite/settings.py | 11 +++++++---- polls/__pycache__/__init__.cpython-37.pyc | Bin 155 -> 155 bytes polls/__pycache__/admin.cpython-37.pyc | Bin 270 -> 301 bytes polls/__pycache__/apps.cpython-37.pyc | Bin 369 -> 369 bytes polls/__pycache__/models.cpython-37.pyc | Bin 1267 -> 1267 bytes polls/__pycache__/urls.cpython-37.pyc | Bin 490 -> 490 bytes polls/__pycache__/views.cpython-37.pyc | Bin 1773 -> 1773 bytes polls/admin.py | 3 ++- .../__pycache__/0001_initial.cpython-37.pyc | Bin 1005 -> 1005 bytes .../__pycache__/__init__.cpython-37.pyc | Bin 166 -> 166 bytes 14 files changed, 9 insertions(+), 5 deletions(-) diff --git a/mysite/__pycache__/__init__.cpython-37.pyc b/mysite/__pycache__/__init__.cpython-37.pyc index d7e22112172a2991ce15033ce9f1d6f6264c8fdd..430f0278edea814330fe866f7816afb7878ce681 100644 GIT binary patch delta 19 ZcmbQkIERtjiIrM?2{swBA=p=qL`wTqCAH!lQD`vMI}Ww zlMzU&fk|~RsgcQ;&Jrb%q8TNaq7@~S!V)D6#3Dc}nxY*gmckUwpsBO*#~GG-iIl9w zy!3p%lq9{R#N_PMyp&?S+{)s@oG9*$)SR4ri1aPaoc!d(oQ(Y9k|-8qV*|5WEJgYG zB~ct%nR%H8riSL4Y`554{oFnMTyL@XIr;)g5C7nhTPy+oK|nGz*fr=DM}T8+aJYYv zOA#l~UsWDplQ*-o@iIzOaXA)eCh7;L=9lJFap)J9B$i|*>uZWnR%Ks2`5b#Dn+i}z hk;-ISj!4Fs$sSGAnG8xlZq6AY^ql8k_qJ&deqC|jL6o|!A)T6{xn1UHJH8y@b!(u0u zl9ia3p0AgZq?eSKoSmANQmj{8n3GwOYFxzy6oj%hnQpPU`nh}hx!z*&bM$pBVh6ge z%470GCgI8cZ2twLm_tLHt2p$FOA<>mll3(vCa1A4W@X9B&&-?5!I8x#4^&V< Yk}+iRB@Sm15iT}n5ai-uVS++70HDr7fdBvi diff --git a/mysite/__pycache__/urls.cpython-37.pyc b/mysite/__pycache__/urls.cpython-37.pyc index aac175d5cb16187453df60342e444b0738b26880..0adb3a405432ea272b6eee2abfd9b5277c94d842 100644 GIT binary patch delta 20 acmcb{evO^miIvX+J0iIvX+J0iIt^fc4 delta 117 zcmZ3>)W^i@#LLUY00eDYnq!kE@=B^`068fPDU3M`xr|Yaj0`DEDa<)cxy(__j0~xa zSu6`!Q<*15$?-AX;s`8FEiTE-&-2q{o7kr$d5fhuvn2HvM^S2eW^qYs(MpCQ4xnL0 Q923ula Date: Sun, 22 Sep 2019 14:30:05 +0900 Subject: [PATCH 2/6] Update README.md I update the way to change admin.py b.c. it is needed to change DB from SQLite to MySQL --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 91b89eb..531e6b0 100644 --- a/README.md +++ b/README.md @@ -137,4 +137,12 @@ ``` + `...\> py manage.py migrate` #DB 설정과 app과 함께 제공되는 데이터베이스 migration에 따라 필요한 DB 테이블 생성 + 확인하고 싶다면 설정한 schema 들어가서 `show tables;` + + `mysite/admin.py` 다음과 같이 수정 + ''' + from django.contrib import admin + + from .models import Question, Choice + admin.site.register(Question) + admin.site.register(Choice) + ''' From 9c5e2d53e3b08bc473fc9fbdd1a8b92e5afde200 Mon Sep 17 00:00:00 2001 From: gywls517 <47051596+gywls517@users.noreply.github.com> Date: Fri, 27 Sep 2019 19:23:10 +0900 Subject: [PATCH 3/6] Update README.md part1 - part4 completed --- README.md | 526 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 503 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 531e6b0..c649074 100644 --- a/README.md +++ b/README.md @@ -39,19 +39,20 @@ - mysite 디렉토리로 이동 - `...\> py manage.py startapp polls` #polls 디렉토리 생성 - polls에서 생성되는 것들 - - mysite/ - manage.py - mysite/ - __init__.py - settings.py - urls.py - wsgi.py - - 4. 첫 번째 뷰 작성하기 + + ``` + polls/ + __init__.py + admin.py + apps.py + migrations/ + __init__.py + models.py + tests.py + views.py + ``` + 5. 첫 번째 뷰 작성하기 - `polls/view.py` 열어 다음 코드 입력 - - polls/view.py ``` from django.http import HttpResponse @@ -61,8 +62,6 @@ - 뷰를 호출하기 위해 연결된 url 필요. 이를 위해 URLconf 사용 - polls 폴더에 urls.py 만들기 - `polls/urls.py`에 다음 코드 포함되어 있음 - - polls/urls.py ``` from django.urls import path @@ -115,8 +114,8 @@ - `error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools` - 해결 방법 : Visual C++ Build tools 2015 download - 참고 링크 : https://nologout.blog.me/221440309296 - * 발생했던 에러 1 - - `_mysql.c(42) : fatal error C1083: Cannot open include file: 'config-win.h': No such file or directory`<> + * 발생했던 에러 2 + - `_mysql.c(42) : fatal error C1083: Cannot open include file: 'config-win.h': No such file or directory` - 해결 방법 : `pip install wheel` `pip install mysqlclient-1.4.4-cp37-cp37m-win32.whl` #깔려있는 Python이 3.7에 32bit라서 cp37, win32로 다운로드 - 참고 링크 : https://stackoverflow.com/questions/26866147/mysql-python-install-error-cannot-open-include-file-config-win-h + 데이터베이스 연결 설정과 맞게 DATABASES 'default' 값 변경 @@ -135,14 +134,495 @@ TIME_ZONE = 'Asia/Seoul' ``` - + `...\> py manage.py migrate` #DB 설정과 app과 함께 제공되는 데이터베이스 migration에 따라 필요한 DB 테이블 생성 + + `...\> py manage.py migrate` #mysite/settings.py의 DB 설정과 app과 함께 제공되는 데이터베이스 migrations에 따라 필요한 DB 테이블 생성 + 확인하고 싶다면 설정한 schema 들어가서 `show tables;` + `mysite/admin.py` 다음과 같이 수정 - ''' - from django.contrib import admin + ``` + from django.contrib import admin + + from .models import Question, Choice + + admin.site.register(Question) + admin.site.register(Choice) + ``` + 2. 모델 만들기 + - 모델 : 부가적인 메타데이터를 가진 데이터베이스의 구조(layout) + - migration들은 모두 모델 파일로부터 유도됨 + + - Question, Choice 두 개의 모델 생성 + - Question의 필드 두 개 : question, question date + - Choice의 필드 두 개 : choice, vote + - `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) + ``` + - 각 Field 인스턴스 이름(question_text ...) - 데이터베이스 필드 이름. 데이터베이스에서 컬럼명으로 사용. + - CharField : 문자 필드 표현 - 필수 인수 : (max_length) / DateTimeField : 날짜, 시간 필드 표현 + - IntegerField : 32비트 정수형 필드 - 선택 인수 : 기본값 설정(default=0) + - ForeignKey : Choice가 하나의 Question에 관계된다는 것을 Django에게 알려줌. 관계(다대일, 다대다, 일대일) + + 3. 모델의 활성화 + - 앱을 현재 프로젝트에 포함시키기 위해, 앱의 구성 클래스에 대한 참조를 INSTALLED_APPS 설정에 추가해야 함. + - PollsConfig 클래스 polls/apps.py 파일 내에 존재. 점으로 구분된 경로 -> `polls.apps.PollsConfig` + - `mysite/settings.py`를 다음과 같이 수정 + ``` + INSTALLED_APPS = [ + 'polls.apps.PollsConfig', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + ] + ``` + + - `...\> py manage.py makemigrations polls` #모델 변경사항 migration으로 저장하겠다고 Django에게 알림 + - 결과 + ``` + BEGIN; + -- + -- Create model Choice + -- + CREATE TABLE "polls_choice" ( + "id" serial NOT NULL PRIMARY KEY, + "choice_text" varchar(200) NOT NULL, + "votes" integer NOT NULL + ); + -- + -- Create model Question + -- + CREATE TABLE "polls_question" ( + "id" serial NOT NULL PRIMARY KEY, + "question_text" varchar(200) NOT NULL, + "pub_date" timestamp with time zone NOT NULL + ); + -- + -- Add field question to choice + -- + ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL; + ALTER TABLE "polls_choice" ALTER COLUMN "question_id" DROP DEFAULT; + CREATE INDEX "polls_choice_7aa0f6ee" ON "polls_choice" ("question_id"); + ALTER TABLE "polls_choice" + ADD CONSTRAINT "polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id" + FOREIGN KEY ("question_id") + REFERENCES "polls_question" ("id") + DEFERRABLE INITIALLY DEFERRED; + + COMMIT; + ``` + - 테이블 이름 : 앱의 이름, 모델의 이름(소문자)이 조합되어 자동 생성(ex. polls&Question -> polls_question), 재지정 가능 + - 기본 키(ID) 자동 추가, 재지정 가능 + - Django는 외래 키 필드명에 `_id` 이름을 자동으로 추가 + - 외래 키 관계는 FOREIGN KEY 제약이 명시적으로 생성됨. + - sqlmigrate 명령은 실제로 migration 실행하지 않고 단순히 결과만 출력. + + - `...\> py manage.py migrate #migration` #DB에 모델과 관련된 테이블 생성 + ``` + - ...\> py manage.py migrate #migration 실행시켜 DB에 모델과 관련된 테이블 생성 + Operations to perform: + Apply all migrations: admin, auth, contenttypes, polls, sessions + Running migrations: + Rendering model states... DONE + Applying polls.0001_initial... OK + ``` + - 결과 + ``` + Operations to perform: + Apply all migrations: admin, auth, contenttypes, polls, sessions + Running migrations: + Rendering model states... DONE + Applying polls.0001_initial... OK + ``` + - migrate 명령 : 적용되지 않은 migration 모두 수집해 이를 실행. 모델에서의 변경 사항, 데이터베이스 스키마의 동기화 + - migration : 동작 중인 DB 자료 손실 없이 업그레이드 하는 데에 최적화 + - 모델의 변경을 만드는 세 단계 : + + `models.py`에서 모델 변경 + + `py manage.py makemigrations`를 통해 변경사항에 대한 migration 생성. + + `py manage.py migration` 명령을 통해 변경사항 DB에 적용 + + 4. API 가지고 놀기 + - 대화식 Python 쉘에 뛰어들어 Django API 자유롭게 가지고 놀기 + + - `...\> py manage.py shell` #python shell 실행 + - python이라고 실행하는 대신 위의 명령 실행한 이유 : manage.py에 설정된 DJANGO_SETTINGS_MODULE 환경변수 때문. + - 이 환경변수는 mysite/settings.py 파일에 대한 Python 임포트 경로를 Django에게 제공. + - Django에서 동작하는 모든 명령을 대화식 Python Shell에서 시험해볼 수 있음. + + 5. Django 관리자 소개 + - Django는 모델 관리용 관리자 인터페이스를 자동으로 생성 + - `...\> py manage.py createsuperuser` + - username, email, password 입력 + + 6. 개발 서버 시작 + - `...\> py manage.py runserver` + - localhost:8000/admin/ 또는 http://127.0.0.1:8000/admin/ 으로 접근했을 때 로그인 화면 보임 + - 편집 가능한 그룹, 사용자 ... -> django.contrib.auth 모듈에서 제공 + + 7. 자유로운 관리 기능 탐색 + - 수정 가능 + - 서식은 모델에서 자동 생성 + - 모델의 각 필드 유형들(DateTimeField, CharField)은 적절한 HTML 입력 위젯으로 표현됨. + +
+ + +## part3 + 1. 뷰 추가 + - `polls/view.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) + ``` + - path() 호출 추가해서 새로운 뷰를 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('/', views.detail, name='detail'), + # ex: /polls/5/results/ + path('/results/', views.results, name='results'), + # ex: /polls/5/vote/ + path('/vote/', views.vote, name='vote'), + ] + ``` + - 브라우저에 "/polls/34/" 입력하면 datail() 함수를 호출해서 url에 입력한 id 출력 + - "/polls/34/results/", "/polls/34/vote/" -> 페이지의 뼈대 출력 + - "/polls/34/" 요청하면 Django는 mysite.urls 파이썬 모듈 불러옴. mysite.urls에서 urlpatterns라는 변수 찾고, 순서대로 패턴 따라감. 'polls/' 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 남은 텍스트인 "34/"를 'polls.urls'의 URLconf로 전달하여 남은 처리 진행. 거기에 '/'와 일치하여 결과적으로 detail() 뷰 함수가 호출됨. + ``` + detail(request=, question_id=34) + ``` + - question_id=34 부분은 에서 왔음. 괄호를 사용해서 URL의 일부를 "캡처"하고, 해당 내용을 keyword 인수로서 뷰 함수로 전달. )) 일치되는 패턴을 구별하기 위해 정의한 이름 + + 2. 뷰가 실제로 뭔가를 하도록 만들기 + - 각 뷰는 두 가지 중 하나를 함 : 1. 요청된 페이지의 내용이 담김 HttpResponse 객체를 반환 2. Http404 같은 예외를 발생 + - Django에 필요한 것은 HttpResponse 객체 혹은 예외. + - 뷰는 DB의 레코드를 읽을 수 있음. 템플릿 시스템도 사용 가능. + + - 뷰에서 사용할 수 있는 템플릿 작성하여 Python 코드로부터 디자인 분리하도록 Django의 템플릿 시스템 사용하기 + + polls 디렉토리에 templates 디렉토리 만들기 : Django가 여기에서 템플릿을 찾게될 것. + + templates 디렉토리 내에 polls 디렉토리 생성, 그 안에 index.html 생성. 템플릿 : polls/templates/polls/index.html. 템플릿을 단순히 polls/index.html로 참조 가능 + * 템플릿 네임스페이싱 : polls/templates/polls라고 만들 필요 없이 polls/templates에 넣어도 되지 않을까? 좋은 생각 x. Django는 이름이 일치하는 첫번째 템플릿을 선택. 만약 동일한 템플릿 이름이 다른 어플리케이션에 있을 경우, Django는 이 둘 간의 차이를 구분하지 못함. Django에게 정확한 템플릿을 지정하기 가장 편리한 방법 : 이름공간으로 구분짓기, 어플리케이션의 이름으로 된 디렉토리에 이러한 템플릿들 넣기 + + `polls/templates/polls/index.html`에 다음 코드 입력 + ``` + {% if latest_question_list %} + + {% else %} +

No polls are available.

+ {% endif %} + ``` + + `polls/views.py`에 index 뷰 업데이트 + ``` + from django.http import HttpResponse + from django.template import loader + + from .models import Question + + + 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)) + ``` + * polls/index.html 템플릿 불러온 후 context 전달. context는 템플릿에서 쓰이는 변수명과 Python 객체를 연결하는 사전형 값 + ``` + + 브라우저에서 "/polls/" 페이지 불러오면 질문이 포함된 리스트가 표시됨. + + 3. 지름길 : render() + - 템플릿에 context 채워 넣어 표현한 결과를 HttpResponse 객체와 함께 돌려주는 구문은 자주 쓰는 용법. + - Django 이를 위해 단축 기능 제공 + - `polls/views.py` index() 뷰 단축 기능으로 작성 + ``` + 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) + ``` + - 더 이상 loader와 HttpResponse를 임포트하지 않아도 됨. (단 detail, results, vote에서 stub 메소드를 가지고 있다면 유지해야 함.) + - render() 함수 _ 첫 번째 인수 : request / 두 번째 인수 : 템플릿 이름 / 세 번째 선택적 인수 : context 사전형 객체. 인수로 지정된 context로 표현된 템플릿의 HttpResponse 객체가 반환됨. + + 4. 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}) + ``` + - 뷰는 요청된 질문의 ID가 없을 경우 Http404 예외를 발생시킴. + + + 5. 지름길 : get_object_or_404() + - 객체가 존재하지 않을 때 get() 사용해 Http404 예외 발생시키는 것은 자주 쓰이는 용법. + - Django 이를 위해 단축 기능 제공 + - `polls/views.py` detail() 뷰 단축 기능으로 작성 + ``` + 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() : Django 모델 첫 번째 인자로 받고, 몇 개의 키워드 인수를 모델 관리자의 get() 함수에 넘김. 만약 객체가 존재하지 않을 경우, Http404 예외가 발생. + + 6. 템플릿 시스템 사용하기 + - detail() 뷰. context 변수 question이 주어졌을 때, polls/detail.html이라는 템플릿이 어떻게 보이는지 + - `polls/templates/polls/detail.html` + ``` +

{{ question.question_text }}

+
    + {% for choice in question.choice_set.all %} +
  • {{ choice.choice_text }}
  • + {% endfor %} +
+ ``` + - 템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법 사용. + - {{ question.question_text }} : Django는 먼저 question 객체에 대해 사전형으로 탐색. 실패하면 속성값으로 탐색. 실패하면 인덱스 탐색. + - {% for %} 반복 구문에서 메소드 호출 일어남. + - question.choice_set.all은 Python에서 question.choice_set.all() 코드로 해석됨. 이때 반환된 Choice 객체의 반복자는 {% for %}에서 사용하기 적당. + + 7. 템플릿에서 하드코딩된 URL 제거 + - `polls/index.html` 템플릿에 링크를 적으면, 다음과 같이 부분적으로 하드코딩됨. + ``` +
  • {{ question.question_text }}
  • + ``` + - 이렇게 강력하게 결합되고 하드코딩된 접근 방식의 문제는 수 많은 템플릿을 가진 프로젝트들의 URL을 바꾸기 어렵다는 것. + - 그러나 polls.urls 모듈의 path() 함수에서 인수의 이름을 정의했으므로, {% url %} template 태그를 사용하여 url 설정에 정의된 특정한 URL 경로들의 의존성 제거 가능. + ``` +
  • {{ question.question_text }}
  • + ``` + - detail이라는 이름의 url이 어떻게 정의되어있는지 확인 가능 + + 6. 템플릿 시스템 사용하기 + - detail() 뷰. context 변수 question이 주어졌을 때, polls/detail.html이라는 템플릿이 어떻게 보이는지 + - `polls/templates/polls/detail.html` + ``` +

    {{ question.question_text }}

    +
      + {% for choice in question.choice_set.all %} +
    • {{ choice.choice_text }}
    • + {% endfor %} +
    + ``` + - 템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법 사용. + - {{ question.question_text }} : Django는 먼저 question 객체에 대해 사전형으로 탐색. 실패하면 속성값으로 탐색. 실패하면 인덱스 탐색. + - {% for %} 반복 구문에서 메소드 호출 일어남. + - question.choice_set.all은 Python에서 question.choice_set.all() 코드로 해석됨. 이때 반환된 Choice 객체의 반복자는 {% for %}에서 사용하기 적당. + + 8. URL의 이름공간 정하기 + - Django가 {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 아는 방법 : URLconf에 이름 공간(namespace) 추가 + - polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 이름 공간을 설정 + - `polls/urls.py¶` + ``` + from django.urls import path + + from . import views + + app_name = 'polls' + urlpatterns = [ + path('', views.index, name='index'), + path('/', views.detail, name='detail'), + path('/results/', views.results, name='results'), + path('/vote/', views.vote, name='vote'), + ] + ``` + - `polls/index.html` 템플릿의 기존 내용을 이름공간으로 나눠진 상세 뷰를 가리키도록 변경. + ``` +
  • {{ question.question_text }}
  • + ``` + +
    + + +## part4 + 1. 간단한 폼 만들기 + - `polls/templates/polls/detail.html` 투표 상세 템플릿을 수정하여, 템플릿에 HTML
    요소를 포함시키기 + + ``` +

    {{ question.question_text }}

    + + {% if error_message %}

    {{ error_message }}

    {% endif %} + + + {% csrf_token %} + {% for choice in question.choice_set.all %} + +
    + {% endfor %} + +
    + ``` + - 위의 템플릿은 각 질문 선택 항목에 대한 라디오 버튼 표시. value는 연관된 질문 선택 항목의 ID + - method="post" (method="get" 와 반대로) 를 사용하는 것은 매우 중요 + - forloop.counter 는 for 태그가 반복을 한 횟수 + - 내부 URL들을 향하는 모든 POST 폼에 템플릿 태그 {% csrf_token %}를 사용하면 됨. + + - 제출된 데이터를 처리하고 그 데이터로 무언가를 수행하는 Django 뷰 + - `polls/urls.py`에 다음 추가. + ``` + path('/vote/', views.vote, name='vote'), + ``` + + - vote() 함수 구현 + - `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']) + 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.", + }) + 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,))) + ``` + - request.POST는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체. request.POST['choice'] 는 선택된 설문의 ID를 문자열로 반환. request.POST 의 값은 항상 문자열. Django는 같은 방법으로 GET 자료에 접근하기 위해 request.GET 를 제공 + - 만약 POST 자료에 choice 가 없으면, request.POST['choice'] 는 KeyError. choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시 보여줌. + - 설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환하고, HttpResponseRedirect 는 하나의 인수를 받음. 그 인수는 사용자가 재전송될 URL + - POST 데이터를 성공적으로 처리 한 후에는 항상 HttpResponseRedirect 를 반환해야 함. + - HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용. 이 함수는 뷰 함수에서 URL을 하드코딩하지 않도록 도와줌. 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킴. reverse() 호출은 다음 문자열 반환 `/polls/3/results/` redirect된 URL은 최종 페이지 표시 위해 result 뷰 호출. + + - 설문조사 하고 난 뒤, vote() 뷰는 설문조사 페이지로 redirect함. + - `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}) + ``` + - `polls/templates/polls/results.html` 그 뷰 작성, /polls/1/로 가면 투표 가능. + ``` +

    {{ question.question_text }}

    + +
      + {% for choice in question.choice_set.all %} +
    • {{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}
    • + {% endfor %} +
    + + Vote again? + ``` + + 2. 제너릭 뷰 사용하기 : 적은 코드가 더 좋습니다. + - 뷰는 URL에서 전달된 매개변수에 따라 DB에서 데이터를 가져오는 것과 템플릿을 로드하고 렌더링된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타냄. -> Django는 이를 위해 '제너릭 뷰' 시스템이라는 지름길을 제공 + + - 설문조사 어플리케이션을 제너릭 뷰 시스템을 사용하도록 변환하는 단계 + + URLconf 변환 + + 불필요한 오래된보기 중 일부 삭제 + + Django의 제너릭 뷰를 기반으로 새로운 뷰 도입 + + 3. URLconf 수정 + - `polls/urls.py` URLconf를 다음과 같이 변경 + ``` + from django.urls import path + + from . import views + + app_name = 'polls' + urlpatterns = [ + path('', views.IndexView.as_view(), name='index'), + path('/', views.DetailView.as_view(), name='detail'), + path('/results/', views.ResultsView.as_view(), name='results'), + path('/vote/', views.vote, name='vote'), + ] + ``` + + 4. views 수정 + - 이전의 index, detail, results 뷰 제거, 장고의 일반적인 뷰 사용 + - `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' + - from .models import Question, Choice + def vote(request, question_id): + ... # same as above, no changes needed. + ``` + - 두 제너릭 뷰 : ListView, DetailView - admin.site.register(Question) - admin.site.register(Choice) - ''' + ``` From 61cf85d837edbadd19eadd6408dc3fa539e09bf4 Mon Sep 17 00:00:00 2001 From: gywls517 Date: Fri, 27 Sep 2019 20:43:20 +0900 Subject: [PATCH 4/6] codes for part5 - part7 --- mysite/__pycache__/settings.cpython-37.pyc | Bin 2353 -> 2382 bytes mysite/settings.py | 2 +- polls/__pycache__/admin.cpython-37.pyc | Bin 301 -> 892 bytes polls/__pycache__/models.cpython-37.pyc | Bin 1267 -> 1392 bytes polls/__pycache__/tests.cpython-37.pyc | Bin 0 -> 946 bytes polls/__pycache__/views.cpython-37.pyc | Bin 1773 -> 2186 bytes polls/admin.py | 19 ++- polls/models.py | 6 +- polls/static/polls/images/background.gif | Bin 0 -> 41742 bytes polls/static/polls/style.css | 7 ++ polls/templates/admin/base_site.html | 10 ++ polls/templates/polls/index.html | 14 ++- polls/tests.py | 127 ++++++++++++++++++++- polls/views.py | 20 +++- 14 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 polls/__pycache__/tests.cpython-37.pyc create mode 100644 polls/static/polls/images/background.gif create mode 100644 polls/static/polls/style.css create mode 100644 polls/templates/admin/base_site.html diff --git a/mysite/__pycache__/settings.cpython-37.pyc b/mysite/__pycache__/settings.cpython-37.pyc index 8948a22dcd7d17b57aaf5ddc18754010f3bfce10..56047e32701b4712aa3eb8a4d8db97cced8052fa 100644 GIT binary patch delta 216 zcmdlebWVuZiIsXK9wmK3oR@f3*^ z$rPz{mQ2PJ={YRvj8S|kGEw{~vMF*Y@+k@_iYZDd$|)*y*fJTT1X5H})PU+#Q`Etv z2AI^$WK3s?5=_yG5=zmI5>8=>5&>dSAQnr}i4sp?3TDvMP1I&&VA%M33p4XA&XUc` zS^62-K<=yZm^_h5c(Xs77$c*^$)e!z>!4`c@$7o+#{48T#*zg%Qz~iqJ}zbG|)ur zVR$4HaM0GBgU;XGzk41jE|ciX6b8})muXE#lf_Wx5L&SQ$Q=@6vDw<)sI9$U9S%X7 kp5frlFGOZAH^xMH{}?&@X7p@0`s~i}@6juT{JJ^$0aS1=zW@LL diff --git a/mysite/settings.py b/mysite/settings.py index 3fd5490..f97ef69 100644 --- a/mysite/settings.py +++ b/mysite/settings.py @@ -55,7 +55,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [os.path.join(BASE_DIR, 'templates')], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ diff --git a/polls/__pycache__/admin.cpython-37.pyc b/polls/__pycache__/admin.cpython-37.pyc index c3f5239ecccbb9450b4a687baa260eee2a48d438..4f8c7da7e8800a81dcdd787f0eca39c45421ad55 100644 GIT binary patch literal 892 zcmZuvy>1jS5VpO)cezbM6j4*M1$h93Q2r!DK_mh+)*;JT&+R#QcQ5vyLTI7o4Je@G zmE2NYOG(9emlHz5(v0nyZ$>lU%xO_f37&^PKfk?Xg!~Af?TPVmjN&d)DWa$$4ds*) z#Z+Wi6Y;2tc}&SOqGFYNA}RrToqUXVik4JmXvxr$p(V$A@^uOkR z&(FJR-9X#Bd98bK+u3(+B`cUO!IN zVEL!TP{eV7oTA#csAyolrkXBTV7sO&MajOf1*;j4Ag%)eIZuO+X93G|MFY>i*iw2D zh`#XYLW5Dx7dL;X7CkomVgFqA@IbeV&NgzGK)zaE358alSJ;PKIVhRk$GUu08R;Cj zGP3vZZx4d;{;hNZV~lo7P{Klm^q9MHV)KBR1Qiy}PsR$McSHX^)5EZwpJ8Y(ly)m4 z@BGg2TWHgR^@RgztECu2|BhvR?S|ZYsCH1?3{}o%^pK^r8u)B^%h4_YIl?;OM_B+2CX-WA3j+5usiYPb+FCdHD zK$^VRCAOJ-e4SAo)=@{OXa^CKM|dK4FoT%hXdF15LygSHjjj`o3?bZ_!4SHH5+Jxl z>wE*cY)EzPP9u9kF}hJr*?7`(xEl+bN``$|R@-zppIsurltVWg(!%`wwt710`Cc>- zyeUOr1hGFo=-4H7Pfyy}ge+!QLR?rbA9;ZjN-m^x?FpZ!`Cb_M!VOX@8iX=-xQP1F z8^>N4sCU|^mC?t*1cNe-X*tM&%zo%6*0O49FR-RQwM$r6ZGFE{$Hpou${uD=L7Pcs SR<)U2RCB!rb@i(6LFFG3rBV?9 delta 256 zcmeys^_i2;iIf})e>G9O@+m>kCv%_u#27mJac1W=VQLb(7?0}oRX VNK})fNMJG-t1(cy9jhTD4*)&uH3$Fz diff --git a/polls/__pycache__/tests.cpython-37.pyc b/polls/__pycache__/tests.cpython-37.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cb8f254efc739f4eb65a24195d60a1d0e8b4094 GIT binary patch literal 946 zcmb7C!EO^V5VgIV%{EPiC>7kQJ(2?v;)qZM1Ql^05&|x(AZv-;W{Y4{$R z56O{Vvcw5-$S-hWCYw|h^ukD<9eZ}(yyx-DgM$_U`|)e>>6j7n6E`~xA$ShUq7y_= zMN<4SMGKZPigBQVB1}VwL(x!;B1$7lUK0_C<`of5$-V_?Q_%OM6R!b<^gGnYAEb4? z%u0-1UdWj)<(fhk58hRf&2>4%-**PoYAQz%NRm=PQYNTiS0oKY0Pvwd+*Q8SLMrTG zhX{%mz(P4aSoR4xN0xL&9J>e*WkDa1OX|Wo6~TgDvN>5c=4=_QK-LO{HlRZ^)2(D{ zlg#pQ^;zY1Bn3BeC`+fN4?BsGt}>-fPBUd?a;8o4b*slFlaYoqDB~h?@)|3*NnR#y z1ix2MEW6j4^)WDoI|)TLwRktX+S7igX;707NT9BCZU7M=C}op1MlG5hdN zV$RE~kevG#=YhGltBIZ91GX`^n6+t}wSzwc7YBL( literal 0 HcmV?d00001 diff --git a/polls/__pycache__/views.cpython-37.pyc b/polls/__pycache__/views.cpython-37.pyc index 7bd391b76d33d8ff8852dfb6393ef6ad7c024698..ee6280b66cc2fb5b547947aa9e349d15d36beadd 100644 GIT binary patch delta 1041 zcmZ`%OK;Oa5Z?7GP8>UrrY~rVDO9bI8c~U3#Q~_0pvr}Zge8cQ?zVO0*mS+7Bt7&% zIe^qdwSohu_Q)ULNAMGJ;)XzQ<;<*G(iXv5`>prenc10d=coO);@q1q^qK5}254yA;M^x1Chb0%%;HZN;3Re%cBSi2% zKGcbFgo#SD*UBUUSG~Vea8HK#j2Cv@g(&835UqtN38D>`Z^j9A6Utp4yKOo&exA0& zAlak@Y{7`S>nTqe_3A?lNXQ1OCTVfnY=)fn^NWA>jc+<+=NZg`Wsr;89lFMofw4Xp zfFWb1q}3?i9+)vBlr`Hu2&W}9T>pqzOh@sYPGoAVl3Ss2(uCSe_C-?tW;qNW}*Nftv`N;@>UQ&4(N7$gYqV%!g}ze4KK&W!GcXln3Ryc zhu)0|4cP^Rt|RteN#zaF@vNj%;-Y#({7|p1{39-#mN!7uvk7OZXIam{!4(-#1CUR| zX2c`y7tV`SeHL3H*5`0RywmUD%i@bZI(ok7f%YQibU_r1>0BKGG`yOq8&7tMq|=Hv z;(E%1FgflkBP%aqvLB8MHdlj$OL?-MV6rkM(_u2w8Fh!r4$Nf;l7{rA?2D`)%D4<& O2%l0`t4`Ib>c0Vio!7?z delta 668 zcmZ{gO=}cE5Qe*YK6XBLXU8a;U@n5j5#3xocnBh*2qNntB08W;cDqAnnPo!vB*DEJ zJ><6Z&66Je1O5R03!WDA?!l`$YSkzhAr4G+&r>z;TUAHh?{WKf90v@a^`EbweRbyT z9vscrHZ~pMmcca4!)cU9!0s{865b&bp7ba2ODk_9@kM|nK+-|dK@y4xN%V&#A{OmK zmUl&GlV!>L<8yNr-ke`Tx0+4hEf??(1Q*sTzRa+~o@M43F5l?S{A;^=qoLTU<=~rx z<(Z|Z$wjLgP2{-N3$Ic_s^REG1`Auua{E0~iTT7Ig%Q?ij)8G=$rAk(Zz}mWbm=q1 z;Nh!Xx_}KfSE0G(TrYw)a#-l{hfMR4l-ArApx30ou$z9=1eOg zOLdOGX?{ISkg>Kgke}+I+mpMeXsKlq`LEInW-0ksX~leTet>5_yB8oZ$L>Y&&1&oZ z!&R)xJGdWQniRwFPF0NMiSDSMg}qn@ek^P?Bdr;__w$iHCt&cy$uxEE9FhVGEF>z1dSt}@YGM8)cyr$ Cwt+eT diff --git a/polls/admin.py b/polls/admin.py index ffd3760..35a8e56 100644 --- a/polls/admin.py +++ b/polls/admin.py @@ -2,5 +2,20 @@ from .models import Question, Choice -admin.site.register(Question) -admin.site.register(Choice) + +class ChoiceInline(admin.TabularInline): + model = Choice + extra = 3 + + +class QuestionAdmin(admin.ModelAdmin): + fieldsets = [ + (None, {'fields': ['question_text']}), + ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), + ] + inlines = [ChoiceInline] + list_display = ('question_text', 'pub_date', 'was_published_recently') + list_filter = ['pub_date'] + search_fields = ['question_text'] + +admin.site.register(Question, QuestionAdmin) diff --git a/polls/models.py b/polls/models.py index 8753cae..9b5ab17 100644 --- a/polls/models.py +++ b/polls/models.py @@ -12,7 +12,11 @@ def __str__(self): return self.question_text def was_published_recently(self): - return self.pub_date >= timezone.now() - datetime.timedelta(days=1) + 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?' diff --git a/polls/static/polls/images/background.gif b/polls/static/polls/images/background.gif new file mode 100644 index 0000000000000000000000000000000000000000..a33bab5104c4802c9a5a0cdbc66ee776c1ad742a GIT binary patch literal 41742 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 literal 0 HcmV?d00001 diff --git a/polls/static/polls/style.css b/polls/static/polls/style.css new file mode 100644 index 0000000..ad5ed3a --- /dev/null +++ b/polls/static/polls/style.css @@ -0,0 +1,7 @@ +li a { + color: green; +} + +body { + background: white url("images/background.gif") no-repeat; +} diff --git a/polls/templates/admin/base_site.html b/polls/templates/admin/base_site.html new file mode 100644 index 0000000..0e61d89 --- /dev/null +++ b/polls/templates/admin/base_site.html @@ -0,0 +1,10 @@ +{% extends "admin/base.html" %} + + +{% block header %}{{ header }} | {{ site_header|default:_('Django site admin') }}{% endblock %} + +{% block branding %} +

    {{ site_header|default:_('Django administration') }}

    +{% endblock %} + +{% block nav-global %}{% endblock %} diff --git a/polls/templates/polls/index.html b/polls/templates/polls/index.html index e3acbf3..366983e 100644 --- a/polls/templates/polls/index.html +++ b/polls/templates/polls/index.html @@ -1 +1,13 @@ -
  • {{ question.question_text }}
  • +{% load static %} + + + +{% if latest_question_list %} + +{% else %} +

    No polls are available.

    +{% endif %} diff --git a/polls/tests.py b/polls/tests.py index 7ce503c..6f3ce16 100644 --- a/polls/tests.py +++ b/polls/tests.py @@ -1,3 +1,128 @@ +import datetime + from django.test import TestCase +from django.utils import timezone +from django.urls import reverse + +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) + + 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) + self.assertIs(recent_question.was_published_recently(), True) + + +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'], + [''] + ) + + 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'], + [''] + ) + + 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'], + ['', ''] + ) + + +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) -# Create your tests here. + 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) diff --git a/polls/views.py b/polls/views.py index e54665d..8898033 100644 --- a/polls/views.py +++ b/polls/views.py @@ -2,26 +2,40 @@ from django.shortcuts import get_object_or_404, render from django.urls import reverse from django.views import generic +from django.utils import timezone 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] + """ + 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] class DetailView(generic.DetailView): model = Question template_name = 'polls/detail.html' - + + def get_queryset(self): + """ + Excludes any questions that aren't published yet. + """ + return Question.objects.filter(pub_date__lte=timezone.now()) + class ResultsView(generic.DetailView): model = Question template_name = 'polls/results.html' + def vote(request, question_id): question = get_object_or_404(Question, pk=question_id) From b62933244e8d2e436234a4d7763a69a5f6cebd75 Mon Sep 17 00:00:00 2001 From: gywls517 Date: Fri, 27 Sep 2019 23:48:15 +0900 Subject: [PATCH 5/6] README.md -ing --- README.md | 432 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 355 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index c649074..b03bace 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ urls.py wsgi.py - + `mysite/` 디렉토리 바깥의 디렉토리는 단순히 프로젝트를 담는 공간. 이 이름은 Django 와 아무 상관 없으므로 원하는 이름으로 변경 가능. + + `mysite/` 디렉토리 바깥의 디렉토리는 단순히 프로젝트를 담는 공간. 원하는 이름으로 변경 가능. + `manage.py`: Django 프로젝트와 다양한 방법으로 상호작용하는 커맨드라인의 유틸리티. + `mysite/` 디렉토리 내부에는 프로젝트를 위한 실제 Python 패키지들이 저장됨. 이 디렉토리 내의 이름을 이용하여, (mysite.urls 와 같은 식으로) 프로젝트의 어디서나 Python 패키지들 import 가능. + `mysite/__init__.py`: Python으로 하여금 이 디렉토리를 패키지처럼 다루라고 알려주는 용도의 단순한 빈 파일. @@ -36,7 +36,7 @@ - runserver의 자동 변경 기능 : 개발 서버는 요청 들어올 때마다 다시 코드 불러옴. 그러나 파일 추가 등 몇 동작은 서버 재기동 해야 적용. 4. 설문조사 앱 만들기 - - mysite 디렉토리로 이동 + - mysite 디렉토리(manage.py 있는 directory))로 이동 - `...\> py manage.py startapp polls` #polls 디렉토리 생성 - polls에서 생성되는 것들 @@ -51,6 +51,7 @@ tests.py views.py ``` + 5. 첫 번째 뷰 작성하기 - `polls/view.py` 열어 다음 코드 입력 ``` @@ -59,8 +60,9 @@ def index(request): return HttpResponse("Hello, world. You're at the polls index.") ``` + - 뷰를 호출하기 위해 연결된 url 필요. 이를 위해 URLconf 사용 - - polls 폴더에 urls.py 만들기 + - polls 폴더에 urls.py 만들기 - polls 디렉토리에서 URLconf 생성하기 위해 - `polls/urls.py`에 다음 코드 포함되어 있음 ``` from django.urls import path @@ -68,16 +70,16 @@ from . import views urlpatterns = [ - path('', views.index, name='index'), + path('', views.index, name='index'), #views.py 안에 def index ] ``` - - `mysite/urls.py` 열어 다음 코드 입력 + - `mysite/urls.py` 열어 다음 코드 입력 - 최상위 URLconf에서 polls.urls 모듈 바라보게 설정 ``` from django.contrib import admin - from django.urls import include, path + from django.urls import include, path #django.urls.include import urlpatterns = [ - path('polls/', include('polls.urls')), + path('polls/', include('polls.urls')), #include() 함수 추가 path('admin/', admin.site.urls), ] ``` @@ -90,13 +92,10 @@ - path() : 필수 인수 == route, view / 선택 인수 == kwargs, name + path() 인수 : route * route는 URL 패턴을 가진 문자열. - * Django는 urlpatterns의 첫 번째 패턴부터 시자가여, 일치하는 패턴을 찾을 때까지 요청된 url을 각 패턴과 리스트 순서대로 비교. - * 패턴들은 GET,POST의 매개변수들 혹은 도메인 이름 검색 x. - * https://www.example.com/myapp/ 이 요청된 경우, URLconf 는 오직 myapp/ 부분만 봄. https://www.example.com/myapp/?page=3, 같은 요청에도, URLconf 는 myapp/ 부분만 신경씀 + * Django는 urlpatterns의 첫 번째 패턴부터 패턴을 찾을 때까지 요청된 URL을 각 패턴과 리스트의 순서대로 비교. + * https://www.example.com/myapp/ 이 요청된 경우, URLconf 는 오직 myapp/ 부분만 봄. https://www.example.com/myapp/?page=3, 같은 요청에도, URLconf 는 myapp/ 부분만 신경씀. + path() 인수 : view * Django 에서 일치하는 패턴을 찾으면, HttpRequest 객체를 첫번째 인수로 하고, 경로로 부터 '캡처된' 값을 키워드 인수로하여 특정한 view 함수를 호출 - + path() 인수 : kwargs - * 임의의 키워드 인수들은 목표한 view에 사전형으로 전달 + path() 인수 : name * URL에 이름을 지으면, 템플릿을 포함한 Django 어디에서나 명확하게 참조 가능 * 이 기능 이용해 하나의 파일만 수정해도 프로젝트 내의 모든 URL 패턴을 바꿀 수 있도록 도와줌. @@ -145,11 +144,13 @@ admin.site.register(Question) admin.site.register(Choice) ``` + 2. 모델 만들기 - 모델 : 부가적인 메타데이터를 가진 데이터베이스의 구조(layout) + - 데이터 모델을 한 곳에서 정의하고, 이것으로부터 자동으로 뭔가를 유도 - migration들은 모두 모델 파일로부터 유도됨 - - Question, Choice 두 개의 모델 생성 + - Question, Choice 두 개의 모델 생성, 두 개의 모델 연관됨 - Question의 필드 두 개 : question, question date - Choice의 필드 두 개 : choice, vote - `polls/models.py`를 다음과 같이 수정 @@ -165,20 +166,24 @@ class Choice(models.Model): question = models.ForeignKey(Question, on_delete=models.CASCADE) choice_text = models.CharField(max_length=200) - votes = models.IntegerField(default=0) + votes = models.IntegerField(default=0) #기본값 설정 선택 인수 ``` - 각 Field 인스턴스 이름(question_text ...) - 데이터베이스 필드 이름. 데이터베이스에서 컬럼명으로 사용. - - CharField : 문자 필드 표현 - 필수 인수 : (max_length) / DateTimeField : 날짜, 시간 필드 표현 - - IntegerField : 32비트 정수형 필드 - 선택 인수 : 기본값 설정(default=0) + - CharField : 문자 필드 표현 - 필수 인수 : max_length / DateTimeField : 날짜, 시간 필드 표현 + - IntegerField : 32비트 정수형 필드 - ForeignKey : Choice가 하나의 Question에 관계된다는 것을 Django에게 알려줌. 관계(다대일, 다대다, 일대일) 3. 모델의 활성화 - - 앱을 현재 프로젝트에 포함시키기 위해, 앱의 구성 클래스에 대한 참조를 INSTALLED_APPS 설정에 추가해야 함. + - Django는 모델에 대한 정보로 + + 앱을 위한 DB schema 생성(CREATE TABLE문) + + Question과 Choice 객체에 접근하기 위한 Python 데이터베이스 접근 API 생성 + + - polls 앱을 현재 프로젝트에 포함시키기 위해, 앱의 구성 클래스에 대한 참조를 INSTALLED_APPS 설정에 추가해야 함. - PollsConfig 클래스 polls/apps.py 파일 내에 존재. 점으로 구분된 경로 -> `polls.apps.PollsConfig` - `mysite/settings.py`를 다음과 같이 수정 ``` INSTALLED_APPS = [ - 'polls.apps.PollsConfig', + 'polls.apps.PollsConfig', #추가 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -189,6 +194,16 @@ ``` - `...\> py manage.py makemigrations polls` #모델 변경사항 migration으로 저장하겠다고 Django에게 알림 + - 결과 + ``` + Migrations for 'polls': + polls/migrations/0001_initial.py: + - Create model Choice + - Create model Question + - Add field question to choice + ``` + + - `...\> py manage.py makemigrations polls 0001` #모델 변경사항 migration으로 저장하겠다고 Django에게 알림 - 결과 ``` BEGIN; @@ -228,15 +243,7 @@ - 외래 키 관계는 FOREIGN KEY 제약이 명시적으로 생성됨. - sqlmigrate 명령은 실제로 migration 실행하지 않고 단순히 결과만 출력. - - `...\> py manage.py migrate #migration` #DB에 모델과 관련된 테이블 생성 - ``` - - ...\> py manage.py migrate #migration 실행시켜 DB에 모델과 관련된 테이블 생성 - Operations to perform: - Apply all migrations: admin, auth, contenttypes, polls, sessions - Running migrations: - Rendering model states... DONE - Applying polls.0001_initial... OK - ``` + - `...\> py manage.py migrate` #DB에 모델과 관련된 테이블 생성 - 결과 ``` Operations to perform: @@ -245,7 +252,8 @@ Rendering model states... DONE Applying polls.0001_initial... OK ``` - - migrate 명령 : 적용되지 않은 migration 모두 수집해 이를 실행. 모델에서의 변경 사항, 데이터베이스 스키마의 동기화 + - migrate 명령 : 적용되지 않은 migration 모두 수집해 이를 실행. 모델에서의 변경 사항, 데이터베이스 스키마의 동기화 이루어짐. + - migration : 동작 중인 DB 자료 손실 없이 업그레이드 하는 데에 최적화 - 모델의 변경을 만드는 세 단계 : + `models.py`에서 모델 변경 @@ -259,26 +267,185 @@ - python이라고 실행하는 대신 위의 명령 실행한 이유 : manage.py에 설정된 DJANGO_SETTINGS_MODULE 환경변수 때문. - 이 환경변수는 mysite/settings.py 파일에 대한 Python 임포트 경로를 Django에게 제공. - Django에서 동작하는 모든 명령을 대화식 Python Shell에서 시험해볼 수 있음. + ``` + >>> from polls.models import Choice, Question # Import the model classes we just wrote. + + # No questions are in the system yet. + >>> Question.objects.all() + + + # 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=) + + # 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() + ]> //이렇게 나오면 Question 내용 볼 수 없어서 도움 안 됨. + ``` + + - `polls/models.py` Question 모델 수정. __str__() 메소드 추가 + ``` + 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__() method is called whenever you call str() on an object. + + - `polls/models.py` Question 모델에 custom 메소드 추가 + ``` + 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) + ``` + + - `...\> py manage.py shell` #python shell 재실행 + ``` + >>> from polls.models import Choice, Question + + # Make sure our __str__() addition worked. + >>> Question.objects.all() + ]> //이제 문제가 보임 + + # Django provides a rich database lookup API that's entirely driven by + # keyword arguments. + >>> Question.objects.filter(id=1) + ]> + >>> Question.objects.filter(question_text__startswith='What') + ]> + + # Get the question that was published this year. + >>> from django.utils import timezone + >>> current_year = timezone.now().year + >>> Question.objects.get(pub_date__year=current_year) + + + # Request an ID that doesn't exist, this will raise an exception. + >>> Question.objects.get(id=2) + Traceback (most recent call last): + ... + DoesNotExist: Question matching query does not exist. + + # Lookup by a primary key is the most common case, so Django provides a + # shortcut for primary-key exact lookups. + # The following is identical to Question.objects.get(id=1). + >>> Question.objects.get(pk=1) + + + # Make sure our custom method worked. + >>> q = Question.objects.get(pk=1) + >>> q.was_published_recently() + True + + # Give the Question a couple of Choices. The create call constructs a new + # Choice object, does the INSERT statement, adds the choice to the set + # of available choices and returns the new Choice object. Django creates + # a set to hold the "other side" of a ForeignKey relation + # (e.g. a question's choice) which can be accessed via the API. + >>> q = Question.objects.get(pk=1) + + # Display any choices from the related object set -- none so far. + >>> q.choice_set.all() + + + # Create three choices. + >>> q.choice_set.create(choice_text='Not much', votes=0) + + >>> q.choice_set.create(choice_text='The sky', votes=0) + + >>> c = q.choice_set.create(choice_text='Just hacking again', votes=0) + + # Choice objects have API access to their related Question objects. + >>> c.question + + + # And vice versa: Question objects get access to Choice objects. + >>> q.choice_set.all() + , , ]> + >>> q.choice_set.count() + 3 + + # The API automatically follows relationships as far as you need. + # Use double underscores to separate relationships. + # This works as many levels deep as you want; there's no limit. + # Find all Choices for any question whose pub_date is in this year + # (reusing the 'current_year' variable we created above). + >>> Choice.objects.filter(question__pub_date__year=current_year) + , , ]> + + # Let's delete one of the choices. Use delete() for that. + >>> c = q.choice_set.filter(choice_text__startswith='Just hacking') + >>> c.delete() + ``` 5. Django 관리자 소개 - Django는 모델 관리용 관리자 인터페이스를 자동으로 생성 - - `...\> py manage.py createsuperuser` + - `...\> py manage.py createsuperuser` #관리자 생성 - username, email, password 입력 6. 개발 서버 시작 - `...\> py manage.py runserver` - localhost:8000/admin/ 또는 http://127.0.0.1:8000/admin/ 으로 접근했을 때 로그인 화면 보임 - - 편집 가능한 그룹, 사용자 ... -> django.contrib.auth 모듈에서 제공 - 7. 자유로운 관리 기능 탐색 + 7. 관리 사이트에서 poll app을 변경 가능하도록 만들기 + - 관리 사이트에 Question 객체가 관리 인터페이스를 가지고 있다고 알려주기 + - `polls/admin.py` 다음과 같이 편집 + ``` + from django.contrib import admin + + from .models import Question + + admin.site.register(Question) + ``` + + 8. 자유로운 관리 기능 탐색 - 수정 가능 - - 서식은 모델에서 자동 생성 + - 서식은 모델(이 경우엔 Question))에서 자동 생성 - 모델의 각 필드 유형들(DateTimeField, CharField)은 적절한 HTML 입력 위젯으로 표현됨. + - History : Django 관리사이트를 통해 누가(username), 언제(timestamp), 무엇을 바꾸었는지 확인 가능
    ## part3 + - Django에서, 웹페이지와 기타 내용들이 view에 의해 제공됨. + - Django는 요청된 URL(도메인 네임에 따라오는 URL 부분)을 조사하여 view를 선택. + - URL로부터 뷰를 얻기 위해, Django는 'URLconfs'라는 것을 사용. URLconf는 URL 패턴을 뷰에 연결. + 1. 뷰 추가 - `polls/view.py`에 뷰 추가. ``` @@ -292,6 +459,7 @@ def vote(request, question_id): return HttpResponse("You're voting on question %s." % question_id) ``` + - path() 호출 추가해서 새로운 뷰를 polls.urls 모듈로 연결 - `polls/urls.py` 수정 ``` @@ -312,21 +480,26 @@ ``` - 브라우저에 "/polls/34/" 입력하면 datail() 함수를 호출해서 url에 입력한 id 출력 - "/polls/34/results/", "/polls/34/vote/" -> 페이지의 뼈대 출력 - - "/polls/34/" 요청하면 Django는 mysite.urls 파이썬 모듈 불러옴. mysite.urls에서 urlpatterns라는 변수 찾고, 순서대로 패턴 따라감. 'polls/' 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 남은 텍스트인 "34/"를 'polls.urls'의 URLconf로 전달하여 남은 처리 진행. 거기에 '/'와 일치하여 결과적으로 detail() 뷰 함수가 호출됨. + - "/polls/34/" 요청하면 Django는 mysite.urls 파이썬 모듈 불러옴. mysite.urls에서 urlpatterns라는 변수 찾고, 순서대로 패턴 따라감. + - 'polls/' 찾은 후엔, 일치하는 텍스트("polls/")를 버리고, 남은 텍스트인 "34/"를 'polls.urls'의 URLconf로 전달하여 남은 처리 진행. 거기에 '/'와 일치하여 결과적으로 detail() 뷰 함수가 호출됨. ``` detail(request=, question_id=34) ``` - - question_id=34 부분은 에서 왔음. 괄호를 사용해서 URL의 일부를 "캡처"하고, 해당 내용을 keyword 인수로서 뷰 함수로 전달. )) 일치되는 패턴을 구별하기 위해 정의한 이름 + - question_id=34 부분은 에서 왔음. 괄호를 사용해서 URL의 일부를 "캡처"하고, 해당 내용을 keyword 인수로서 뷰 함수로 전달. = 일치되는 패턴을 구별하기 위해 정의한 이름 2. 뷰가 실제로 뭔가를 하도록 만들기 - - 각 뷰는 두 가지 중 하나를 함 : 1. 요청된 페이지의 내용이 담김 HttpResponse 객체를 반환 2. Http404 같은 예외를 발생 + - 각 뷰는 두 가지 중 하나를 함 : + + 요청된 페이지의 내용이 담긴 HttpResponse 객체를 반환 + + Http404 같은 예외를 발생 - Django에 필요한 것은 HttpResponse 객체 혹은 예외. - 뷰는 DB의 레코드를 읽을 수 있음. 템플릿 시스템도 사용 가능. - - - 뷰에서 사용할 수 있는 템플릿 작성하여 Python 코드로부터 디자인 분리하도록 Django의 템플릿 시스템 사용하기 - + polls 디렉토리에 templates 디렉토리 만들기 : Django가 여기에서 템플릿을 찾게될 것. - + templates 디렉토리 내에 polls 디렉토리 생성, 그 안에 index.html 생성. 템플릿 : polls/templates/polls/index.html. 템플릿을 단순히 polls/index.html로 참조 가능 - * 템플릿 네임스페이싱 : polls/templates/polls라고 만들 필요 없이 polls/templates에 넣어도 되지 않을까? 좋은 생각 x. Django는 이름이 일치하는 첫번째 템플릿을 선택. 만약 동일한 템플릿 이름이 다른 어플리케이션에 있을 경우, Django는 이 둘 간의 차이를 구분하지 못함. Django에게 정확한 템플릿을 지정하기 가장 편리한 방법 : 이름공간으로 구분짓기, 어플리케이션의 이름으로 된 디렉토리에 이러한 템플릿들 넣기 + + - Python 코드로부터 디자인 분리 위해 Django의 템플릿 시스템 사용하기 + + polls 디렉토리에 templates 디렉토리 생성 : Django가 여기에서 템플릿을 찾게될 것. + + templates 디렉토리에 polls 디렉토리 생성, 그 안에 index.html 생성. 템플릿을 단순히 polls/index.html로 참조 가능 + * 템플릿 네임스페이싱 : polls/templates/polls라고 만들 필요 없이 polls/templates에 넣어도 되지 않을까? + * 좋은 생각 x. Django는 이름이 일치하는 첫번째 템플릿을 선택. 만약 동일한 템플릿 이름이 다른 어플리케이션에 있을 경우, Django는 이 둘 간의 차이를 구분하지 못함. Django에게 정확한 템플릿을 지정하기 가장 편리한 방법 : 이름공간(namespace))으로 구분짓기 == 어플리케이션의 이름으로 된 디렉토리에 이러한 템플릿들 넣기 + + `polls/templates/polls/index.html`에 다음 코드 입력 ``` {% if latest_question_list %} @@ -339,7 +512,7 @@

    No polls are available.

    {% endif %} ``` - + `polls/views.py`에 index 뷰 업데이트 + + `polls/views.py`에 템플릿 이용해서 index 뷰 업데이트 ``` from django.http import HttpResponse from django.template import loader @@ -353,7 +526,7 @@ context = { 'latest_question_list': latest_question_list, } - return HttpResponse(template.render(context, request)) + return HttpResponse(template.render(context, request)) ``` * polls/index.html 템플릿 불러온 후 context 전달. context는 템플릿에서 쓰이는 변수명과 Python 객체를 연결하는 사전형 값 ``` @@ -374,7 +547,7 @@ context = {'latest_question_list': latest_question_list} return render(request, 'polls/index.html', context) ``` - - 더 이상 loader와 HttpResponse를 임포트하지 않아도 됨. (단 detail, results, vote에서 stub 메소드를 가지고 있다면 유지해야 함.) + - 모든 뷰에 적용한다면, 더 이상 loader와 HttpResponse를 임포트하지 않아도 됨. (단 detail, results, vote에서 stub 메소드를 가지고 있다면 유지해야 함.) - render() 함수 _ 첫 번째 인수 : request / 두 번째 인수 : 템플릿 이름 / 세 번째 선택적 인수 : context 사전형 객체. 인수로 지정된 context로 표현된 템플릿의 HttpResponse 객체가 반환됨. 4. 404 에러 일으키기 @@ -428,6 +601,8 @@ - question.choice_set.all은 Python에서 question.choice_set.all() 코드로 해석됨. 이때 반환된 Choice 객체의 반복자는 {% for %}에서 사용하기 적당. 7. 템플릿에서 하드코딩된 URL 제거 + - 하드코딩이란? 데이터를 코드 내부에 직접 입력하는 것. (ex. 상수 변수의 초기값) 주로 파일 경로, URL 또는 IP 주소, 비밀번호, 화면에 출력될 문자열 등이 대상이 됨. + - `polls/index.html` 템플릿에 링크를 적으면, 다음과 같이 부분적으로 하드코딩됨. ```
  • {{ question.question_text }}
  • @@ -437,24 +612,19 @@ ```
  • {{ question.question_text }}
  • ``` + - detail이라는 이름의 url이 어떻게 정의되어있는지 확인 가능 - - 6. 템플릿 시스템 사용하기 - - detail() 뷰. context 변수 question이 주어졌을 때, polls/detail.html이라는 템플릿이 어떻게 보이는지 - - `polls/templates/polls/detail.html` - ``` -

    {{ question.question_text }}

    -
      - {% for choice in question.choice_set.all %} -
    • {{ choice.choice_text }}
    • - {% endfor %} -
    - ``` - - 템플릿 시스템은 변수의 속성에 접근하기 위해 점-탐색(dot-lookup) 문법 사용. - - {{ question.question_text }} : Django는 먼저 question 객체에 대해 사전형으로 탐색. 실패하면 속성값으로 탐색. 실패하면 인덱스 탐색. - - {% for %} 반복 구문에서 메소드 호출 일어남. - - question.choice_set.all은 Python에서 question.choice_set.all() 코드로 해석됨. 이때 반환된 Choice 객체의 반복자는 {% for %}에서 사용하기 적당. - + ``` + # the 'name' value as called by the {% url %} template tag + path('/', views.detail, name='detail'), + ``` + + - `polls/urls.py`상세 뷰의 URL을 polls/specifics/12로 바꾸고 싶다면 + ``` + # the 'name' value as called by the {% url %} template tag + path('/', views.detail, name='detail'), + ``` + 8. URL의 이름공간 정하기 - Django가 {% url %} 템플릿태그를 사용할 때, 어떤 앱의 뷰에서 URL을 생성할지 아는 방법 : URLconf에 이름 공간(namespace) 추가 - polls/urls.py 파일에 app_name을 추가하여 어플리케이션의 이름 공간을 설정 @@ -464,7 +634,7 @@ from . import views - app_name = 'polls' + app_name = 'polls' #이름공간 설정 urlpatterns = [ path('', views.index, name='index'), path('/', views.detail, name='detail'), @@ -472,7 +642,7 @@ path('/vote/', views.vote, name='vote'), ] ``` - - `polls/index.html` 템플릿의 기존 내용을 이름공간으로 나눠진 상세 뷰를 가리키도록 변경. + - `polls/index.html` 템플릿의 기존 내용을 이름공간으로 나눠진 상세 뷰를 가리키도록 변경. 'detail' -> 'polls:detail' ```
  • {{ question.question_text }}
  • ``` @@ -482,8 +652,7 @@ ## part4 1. 간단한 폼 만들기 - - `polls/templates/polls/detail.html` 투표 상세 템플릿을 수정하여, 템플릿에 HTML
    요소를 포함시키기 - + - `polls/templates/polls/detail.html` 템플릿에 HTML 요소를 포함시키기 ```

    {{ question.question_text }}

    @@ -498,18 +667,14 @@
    ``` - - 위의 템플릿은 각 질문 선택 항목에 대한 라디오 버튼 표시. value는 연관된 질문 선택 항목의 ID - - method="post" (method="get" 와 반대로) 를 사용하는 것은 매우 중요 - - forloop.counter 는 for 태그가 반복을 한 횟수 - - 내부 URL들을 향하는 모든 POST 폼에 템플릿 태그 {% csrf_token %}를 사용하면 됨. + - 위의 템플릿은 각 질문 선택 항목에 대한 라디오 버튼 표시. name은 choice, value는 연관된 질문 선택 항목의 ID + - 하나 선택해서 폼 제출하면 choice=# 전송 + - method="post" (method="get" 와 반대로) 꼭 사용하기 + - 내부 URL들을 향하는 모든 POST 폼에 템플릿 태그 {% csrf_token %}를 사용하면 됨. : 사이트 간 요청 위조(CSRF)에 대항 - 제출된 데이터를 처리하고 그 데이터로 무언가를 수행하는 Django 뷰 - - `polls/urls.py`에 다음 추가. - ``` - path('/vote/', views.vote, name='vote'), - ``` - - vote() 함수 구현 + - 가상으로 만들었던 vote() 함수 구현 - `polls/views.py`에 다음 추가. ``` from django.http import HttpResponse, HttpResponseRedirect @@ -538,12 +703,12 @@ ``` - request.POST는 키로 전송된 자료에 접근할 수 있도록 해주는 사전과 같은 객체. request.POST['choice'] 는 선택된 설문의 ID를 문자열로 반환. request.POST 의 값은 항상 문자열. Django는 같은 방법으로 GET 자료에 접근하기 위해 request.GET 를 제공 - 만약 POST 자료에 choice 가 없으면, request.POST['choice'] 는 KeyError. choice가 주어지지 않은 경우에는 에러 메시지와 함께 설문조사 폼을 다시 보여줌. - - 설문지의 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환하고, HttpResponseRedirect 는 하나의 인수를 받음. 그 인수는 사용자가 재전송될 URL + - 응답 수가 증가한 이후에, 코드는 일반 HttpResponse 가 아닌 HttpResponseRedirect 를 반환하고, HttpResponseRedirect 는 하나의 인수를 받음. 그 인수는 사용자가 redirect될 URL - POST 데이터를 성공적으로 처리 한 후에는 항상 HttpResponseRedirect 를 반환해야 함. - HttpResponseRedirect 생성자 안에서 reverse() 함수를 사용. 이 함수는 뷰 함수에서 URL을 하드코딩하지 않도록 도와줌. 제어를 전달하기 원하는 뷰의 이름을, URL패턴의 변수부분을 조합해서 해당 뷰를 가리킴. reverse() 호출은 다음 문자열 반환 `/polls/3/results/` redirect된 URL은 최종 페이지 표시 위해 result 뷰 호출. - 설문조사 하고 난 뒤, vote() 뷰는 설문조사 페이지로 redirect함. - - `polls/views.py` 그 뷰 작성 + - `polls/views.py` 그 뷰 작성. detail() 뷰와 거의 동일 ``` from django.shortcuts import get_object_or_404, render @@ -552,7 +717,7 @@ question = get_object_or_404(Question, pk=question_id) return render(request, 'polls/results.html', {'question': question}) ``` - - `polls/templates/polls/results.html` 그 뷰 작성, /polls/1/로 가면 투표 가능. + - `polls/templates/polls/results.html` /polls/1/로 가면 투표 가능. ```

    {{ question.question_text }}

    @@ -566,7 +731,8 @@ ``` 2. 제너릭 뷰 사용하기 : 적은 코드가 더 좋습니다. - - 뷰는 URL에서 전달된 매개변수에 따라 DB에서 데이터를 가져오는 것과 템플릿을 로드하고 렌더링된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타냄. -> Django는 이를 위해 '제너릭 뷰' 시스템이라는 지름길을 제공 + - 뷰 : URL에서 전달된 매개변수에 따라 DB에서 데이터를 가져오는 것과, 템플릿을 로드하고 렌더링된 템플릿을 리턴하는 기본 웹 개발의 일반적인 경우를 나타냄. + - Django는 이를 위해 '제너릭 뷰' 시스템이라는 지름길을 제공 - 설문조사 어플리케이션을 제너릭 뷰 시스템을 사용하도록 변환하는 단계 + URLconf 변환 @@ -588,6 +754,7 @@ path('/vote/', views.vote, name='vote'), ] ``` + - question_id -> pk 4. views 수정 - 이전의 index, detail, results 뷰 제거, 장고의 일반적인 뷰 사용 @@ -624,5 +791,116 @@ ... # same as above, no changes needed. ``` - 두 제너릭 뷰 : ListView, DetailView + - 제너릭 뷰는 어떤 모델이 적용될 것인지 알아야 함. model 속성 사용하여 제공. + - template_name 속성은 Django에게 자동 생성된 기본 템플릿 이름 대신에 특정 템플릿 이름을 사용하도록 알려주기 위해 사용됨. + - 결과 뷰와 상세 뷰가 렌더링될 때 둘 다 동일한 DetailView를 사용하고 있더라도 서로 다른 모습을 갖도록 함. + +
    + + +## part5 + 1. 첫 번째 테스트 작성하기 + - 버그 식별하기 + + 현재 Question의 pub_date 필드가 미래로 설정되어 있을 때에도 Question.was_published_recently() True 반환. + + `...\> py manage.py shell` shell 통해 버그 확인 + ``` + >>> import datetime + >>> from django.utils import timezone + >>> from polls.models import Question + >>> # create a Question instance with pub_date 30 days in the future + >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) + >>> # was it published recently? + >>> future_question.was_published_recently() + True + ``` + + - 버그 노출하는 테스트 만들기 + + 어플리케이션 테스트는 일반적으로 text.py 파일에 있음. + + `polls/tests.py¶` shell 통해 다음 입력 + ``` + 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) ``` + + - 테스트 실행 + + `...\> py manage.py test polls` # polls 어플리케이션에서 테스트 찾음 + + 결과 + ``` + Creating test database for alias 'default'... + System check identified no issues (0 silenced). + F + ====================================================================== + FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests) + ---------------------------------------------------------------------- + Traceback (most recent call last): + File "/path/to/mysite/polls/tests.py", line 16, in test_was_published_recently_with_future_question + self.assertIs(future_question.was_published_recently(), False) + AssertionError: True is not False + + ---------------------------------------------------------------------- + Ran 1 test in 0.001s + + FAILED (failures=1) + Destroying test database for alias 'default'... + ``` + + - 버그 수정 + + `polls/models.py`에서 날짜가 과거에 있을 때에만 True를 반환하도록 메소드 수정 + + 결과 + ``` + def was_published_recently(self): + now = timezone.now() + return now - datetime.timedelta(days=1) <= self.pub_date <= now + ``` + + `...\> py manage.py test polls` #테스트 재실행, 버그 해결 + + - 보다 포괄적인 테스트 + - 뷰 테스트 + + - 장고 테스트 클라이언트 + + test.py 또는 shell에서 사용 가능 + + shell : text.py에서 필요하지 않았던 두 가지 일 해야 함 + + 1. shell에서 테스트 환경 구성 + + `...\> py manage.py shell` + ``` + >>> from django.test.utils import setup_test_environment + >>> setup_test_environment() + ``` + + 2. 테스트 클라이언트 클래스 import + ``` + >>> from django.test import Client + >>> # create an instance of the client for our use + >>> client = Client() + ``` + + - 뷰를 개선시키기 + + `polls/views.py` + ``` + from django.utils import timezone #가져오기 추가 + + 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] + ``` + + Question.objects.filter (pub_date__lte = timezone.now ())는 timezone.now보다 pub_date가 작거나 같은 Question을 포함하는 queryset을 반환 \ No newline at end of file From a6e87d00d1a6c5598aec234e340eca3f80dc2a7e Mon Sep 17 00:00:00 2001 From: gywls517 Date: Sat, 28 Sep 2019 01:10:31 +0900 Subject: [PATCH 6/6] README.md for part 1-7 --- README.md | 121 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 107 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index b03bace..bc1dfa2 100644 --- a/README.md +++ b/README.md @@ -803,16 +803,6 @@ - 버그 식별하기 + 현재 Question의 pub_date 필드가 미래로 설정되어 있을 때에도 Question.was_published_recently() True 반환. + `...\> py manage.py shell` shell 통해 버그 확인 - ``` - >>> import datetime - >>> from django.utils import timezone - >>> from polls.models import Question - >>> # create a Question instance with pub_date 30 days in the future - >>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30)) - >>> # was it published recently? - >>> future_question.was_published_recently() - True - ``` - 버그 노출하는 테스트 만들기 + 어플리케이션 테스트는 일반적으로 text.py 파일에 있음. @@ -870,9 +860,6 @@ ``` + `...\> py manage.py test polls` #테스트 재실행, 버그 해결 - - 보다 포괄적인 테스트 - - 뷰 테스트 - - 장고 테스트 클라이언트 + test.py 또는 shell에서 사용 가능 + shell : text.py에서 필요하지 않았던 두 가지 일 해야 함 @@ -882,6 +869,7 @@ >>> from django.test.utils import setup_test_environment >>> setup_test_environment() ``` + + setup_test_environment() 사용한 렌더러 설치 - response.context와 같은 response의 추가적인 속성 사용 + 2. 테스트 클라이언트 클래스 import ``` >>> from django.test import Client @@ -903,4 +891,109 @@ pub_date__lte=timezone.now() ).order_by('-pub_date')[:5] ``` - + Question.objects.filter (pub_date__lte = timezone.now ())는 timezone.now보다 pub_date가 작거나 같은 Question을 포함하는 queryset을 반환 \ No newline at end of file + + Question.objects.filter (pub_date__lte = timezone.now ())는 timezone.now보다 pub_date가 작거나 같은 Question을 포함하는 queryset을 반환 + + ## part6 + - 정적 파일(이미지, Javascript, CSS...) + + 1. 앱의 모양과 느낌을 원하는 대로 바꿔보세요. + - polls/static/polls 생성 + - 그 안에 style.css 생성 + - style.css 선언 + ``` + {% load static %} + + + ``` + - {% static %} 템플릿 태그는 정적 파일의 절대 URL을 생성. + + 2. 배경 이미지 추가하기 + - polls/static/polls 안에 images 디렉토리 생성 + - 이미지 넣기 + - 스타일시트에 추가 + ``` + body { + background: white url("images/background.gif") no-repeat; + } + ``` + + ## part7 + 1. 관리자 폼 커스터마이징 + - `polls/admin.py` admin.site.register(Question) 줄 다음과 같이 수정 + ``` + from django.contrib import admin + + from .models import Question + + + class QuestionAdmin(admin.ModelAdmin): + fields = ['pub_date', 'question_text'] + + admin.site.register(Question, QuestionAdmin) + ``` + - 모델 관리자 옵션을 변경해야 할 때마다 모델 어드민 클래스를 만든 후 admin.site.register()에 두 번째 인수로 전달 + - 발행일이 설문 필드 앞쪽으로 옴. + + - 수십 개의 필드가 있는 폼에 관해서는 폼을 fieldset으로 분할하는 게 좋음. + - `polls/admin.py` + ``` + from django.contrib import admin + + from .models import Question + + + class QuestionAdmin(admin.ModelAdmin): + fieldsets = [ + (None, {'fields': ['question_text']}), + ('Date information', {'fields': ['pub_date']}), + ] + + admin.site.register(Question, QuestionAdmin) + ``` + - 튜플의 첫 번째 요소 : fieldset의 제목 + + 2. 관련된 객체 추가 + - Question이 여러 개의 Choice 가지고 있었는데도 표시 안 했었음 + - 해결 방법 1 : 관리자에 Choice 등록 + - `polls/admin.py` + ``` + from django.contrib import admin + + from .models import Choice, Question + # ... + admin.site.register(Choice) + ``` + - Question 필드가 select box로 되어있음 : Django는 ForeignKey가 admin에서 select로 표현되어야 함 앎. + + - Question 객체 생성할 때 여러 개의 Choices 직접 추가하는 방법 + - choice 모델에 대한 register() 제거, Question 코드 편집 + - `polls/admin.py` + ``` + from django.contrib import admin + + from .models import Choice, Question + + + 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) + ``` + - choice 객체는 Question 관리자 페이지에서 편집됨, 기본적으로 3가지 선택항목 제공함. + + - inline 관련 객체를 표시하는 표 형식의 방법 : StackedInLine 대신에 TabularInline 사용 + - `polls/admin.py` + ``` + class ChoiceInline(admin.TabularInline): + #... + ``` + \ No newline at end of file