AI developer 의 Backend django study

아무래도 초기 스타트업의 경우 자신이 어떤 능력을 가지고 있고 또 그 전에 어떤 직군이었던 간에 필요한 직군이 생긴다면 그 능력을 빠르게 학습하고 키워나가는 것이 중요하다고 생각합니다. 저는 AI 학습에 필요한 data 들을 효율적으로 구축/관리 하기 위해서 data-management server를 처음부터 끝까지 개발해야만 하는 상황이었고 그렇기에 backend study 는 필요했습니다.

본 포스트는 AI 밖에 개발할 줄 모르던 AI 개발자가 작성하는 django-project study 정리에 관한 글입니다. 미래에 비개발자나 django 에 익숙하지 않은 다른 개발자들에게 도움이 됐으면 하는 마음입니다!

django-tutorial 을 참고했습니다.



PART 1

가장 먼저 프로젝트를 만들어야겠죠? django 를 처음 사용한다면, 이러한 초기 설정에 유의해야 합니다.

$ django-admin startproject mysite

mysite 에는 다음과 같은 파일들이 생성됩니다. (상위 mysite 라는 이름은 추후 있을 path 에서 혼동이 있을 수 있기 떄문에 변경합니다.)

일단 Django 가 제대로 동작하는지 확인해봅시다

$ python manage.py runserver (기본은 http://127.0.0.1:8000/ 입니다) 포트 변경을 위해서는 runserver 뒤에 포트번호를 붙이면 됩니다.

설문조사 앱을 만들기 위해 manage.py가 존재하는 디레고리에서 다음의 명령을 입력합니다. Django는 app의 기본 디렉토리 구조를 자동으로 생성할 수 있는 도구를 제공하기 때문에, 코드에만 집중이 가능합니다.

$ python manage.py startapp polls

첫 번째 views.py와 urls.py를 작성해봅시다 (polls/views.py)

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

PART 2

$ python manage.py migrate : migrate 명령은 INSTALLED_APPS  설정을 탐색하여

mysite/settings.py  데이터베이스 설정과 app과 함께 제공되는 데이터베이스 migrations에 따라 필요한 데이터베이스 테이블을 생성합니다.
# 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)

일단 settings.py에서 polls이라는 app을 INSTALLED_APPS 설정에 추가합니다.

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

$ python manage.py makemigrations polls : 모델을 변경시킨 사실과  변경사항을 migration으로 저장시키고 싶다는 명령어 입니다.
Migrations for 'polls':
polls/migrations/0001_initial.py:
		- Create model Choice
		- Create model Question
		- Add field question to choice

추가적으로 migration이 내부적으로 어떤 SQL 문장을 실행하는지 보려면 아래의 코드를 입력하면 됩니다.

$ python manage.py sqlmigrate polls 0001

이제, migrate 를 실행시켜 데이터베이스에 모델과 관련된 테이블을 생성해줍니다. (항상 변경사항을 makemigrations로 알리고 migrate로 변경사항을 적용시키세요!)

$ python manage.py migrate
Operations to perform: Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations: Rendering model states... DONE
Applying polls.0001_initial... OK

API 가지고 놀기 : 대화식 Python shell 에서 API를 자유롭게 가지고 놉시다.

$ python manage.py shell

shell 진입 후 아래의 tutorial에 따라 데이터를 만들고 queryset을 확인해봅니다.

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

# No questions are in the system yet.

>>> Question.objects.all()<QuerySet []>

# Create a new Question.

# Support for time zones is enabled in the default settings file, so

# Django expects a datetime with tzinfo for pub_date. Use timezone.now()

# instead of datetime.datetime.now() and it will do the right thing.

>>> from django.utils import timezone

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

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

>>> q.save()

# Now it has an ID.

>>> q.id1

# Access model field values via Python attributes.

>>> q.question_text

"What's new?"

>>> q.pub_date

datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().

>>> q.question_text = "What's up?"

>>> q.save()

# objects.all() displays all the questions in the database.

>>> Question.objects.all()

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

여기서 <Question: Question object (1)>은 이 객체를 표현하는 데 좋지 않기 때문에 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

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)

>>> from polls.models import Choice, Question

# Make sure our __str__() addition worked.

>>> Question.objects.all()

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

# Django provides a rich database lookup API that's entirely driven by

# keyword arguments.

>>> Question.objects.filter(id=1)

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

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

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

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

<Question: What's up?>

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

<Question: What's up?>

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

<QuerySet []>

# Create three choices.

>>> q.choice_set.create(choice_text='Not much', votes=0)

<Choice: Not much>

>>> q.choice_set.create(choice_text='The sky', votes=0)

<Choice: The sky>

>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice objects have API access to their related Question objects.

>>> c.question

<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.

>>> q.choice_set.all()

<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

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

<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.

>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')

>>> c.delete()

관리자 생성하기

$ python manage.py createsuperuser

Username: admin

Email address : admin@example.com

Password: mondeique

Password: mondeique

Superuser created successfully.

관리자 사이트에서 polls app 을 변경가능하도록 만들기 : admin.py 페이지에서 Question 객체를 등록해주면 됩니다

from django.contrib import admin

from .models import Question

admin.site.register(Question)

PART 3 : 공개 인터페이스 “뷰(View)” 추가하기

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)
from django.urls import path

from . import views

urlpatterns = [

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

]
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)
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})
from django.urls import path

from . import views

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

PART 4

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,)))
from django.urls import path

from . import views

app_name = 'polls'

urlpatterns = [

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

]
from django.http import HttpResponseRedirect

from django.shortcuts import get_object_or_404, render

from django.urls import reverse

from django.views import generic

from .models import Choice, Question

class IndexView(generic.ListView):

	template_name = 'polls/index.html'

	context_object_name = 'latest_question_list'

	def get_queryset(self):
	
	"""Return the last five published questions."""

		return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):

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

	model = Question
	
	template_name = 'polls/results.html'
	
	def vote(request, question_id):

... # same as above, no changes needed.

PART 5 : testing 하기

$ python manage.py 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
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)
$ python manage.py test polls
def was_published_recently(self):

	now = timezone.now()

	return now - datetime.timedelta(days=1) <= self.pub_date <= now

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)
$ python manage.py shell

>>> from django.test.utils import setup_test_environment

>>> setup_test_environment()

>>> from django.test import Client

>>> # create an instance of the client for our use

>>> client = Client()

>>> # get a response from '/'

>>> response = client.get('/')

Not Found: /

>>> # we should expect a 404 from that address; if you instead see an

>>> # "Invalid HTTP_HOST header" error and a 400 response, you probably

>>> # omitted the setup_test_environment() call described earlier.

>>> response.status_code

404

>>> # on the other hand we should expect to find something at '/polls/'

>>> # we'll use 'reverse()' rather than a hardcoded URL

>>> from django.urls import reverse

>>> response = client.get(reverse('polls:index'))

>>> response.status_code

200

>>> response.contentb'\n    <ul>\n    \n        <li><a href="/polls/1/">What&#39;s up?</a></li>\n    \n    </ul>\n\n'

>>> response.context['latest_question_list']

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

추가 view 수정과 test 는 django-practice repo에 있습니다.


PART 6 : 관리자 폼 커스터마이징

from django.contrib import admin

from .models import Question

class QuestionAdmin(admin.ModelAdmin):

fields = ['pub_date', 'question_text']

admin.site.register(Question, QuestionAdmin)
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)
from django.contrib import admin

from .models import Choice, Question

# ...

admin.site.register(Choice)
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)