Django2

안재영·2024년 4월 9일

DB정보 화면에 출력하기

views.py파일로 이동합니다

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

index를 해당 부분으로 수정하면
shell에서 db를 값을 가져올때와 같은방법을 사용하여 데이터를 가져올수 있습니다

이제 주소/polls로 이동하면 기존 hello world에서 Question에 넣어둿던 데이터들의 제목이 출력될것입니다

장고에서 html을 사용하기위한 템플릿에 대해 알아봅시다

polls 폴더 안에 templates/polls폴더를 생성해줍시다
생성한 폴더안에 html문서를 간단하게 작성해서넣어줍시다
다시 views 파일로 돌아와서

from django.shortcuts import render

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

render를 import해주고 생성한 html주소를 넣어주면 polls로 접근했을때 기존에 뜨던화면이 아닌
html파일이 나타나게됩니다 또한 해당 html에서 값을 사용하고싶을경우 render에서 주소뒤에 데이터를 넣어 전달해준뒤
html파일 내에서

<li>{{first_question}}</li>

{{변수명}}을 넣어 사용해줄수 있습니다

이번엔 리스트 전체를 출력해보겠습니다

from django.shortcuts import render

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

views 파일을 수정해서 html로 리스트를 전달하게 만든뒤

html파일로 이동해서

{% if questions %}
<ul>
    {% for question in questions %}
    <li>{{question}}</li>
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

for문을 돌려 출력해줍시다

장고 템플릿에서 for문을 사용할때는 {% for i in list%}로 시작하고 끝에 {% endfor %}로 마무리해줍니다

if문 또한 {% 조건 %} {% else %}로 조건을 구분하며 if 끝에 {% endif %}로 마무리해줍니다

이번에는 상세 페이지를 만들어봅시다

views 수정과 urls 수정이 필요합니다

urlpatterns = [
    path("", views.index, name='index'),
    path('<int:question_id>', views.detail, name="detail"),
    path("some_url", views.some_url),
]

urlpatterns 에 새로 들어갈 detail의 정보를 보내줍니다 주소로는 int:question_id가 들어갈예정입니다

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

view는 question id를 받아 해당question_id를 가진 question을 html로 보내줄예정입니다

템플릿 폴더 안에 polls로 이동해서 detail.html를 생성해줍시다

내용으로는 간단한 제목과 choice목록을 띄워주도록합니다

<h1>제목 : {{question.question_text}}</h1>
<ul>
{%for choice in question.choice_set.all%}
    <li>{{choice.choice_text}}</li>
{% endfor %}
</ul>

이제 페이지로 이동하기위한 question목록에 a태그를 삽입하여 처리해줍시다

{% if questions %}
<ul>
    {% for question in questions %}
    <li><a href="{% url 'detail' question.id %}">{{question}}</a></li>
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

a태그에 href 속성이 기존 html과 상당히 다른부분이 보입니다

url은 detail이란 name을 가진path를 찾으며 넘겨줄 값으로 question.id를 받는다고 되어있네요. 이제 새로고침을하면 question을 클릭하여 페이지를 이동하며 해당 question에 대한 정보 또한 잘 받아오는 것 도 확인할수 있습니다

하지만 이것은 템플릿이 복잡해지고 다양한 app을 사용할수록 url의 name이 겹치는경우가 생길수 있습니다 이를 해결하기위한 app_name을 알아봅시다

urls.py로 이동해서 app_name을 생성해줍니다

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

이제 questionList가 있는 html로 이동해서 url부분을 살짝 수정해줍니다

{% if questions %}
<ul>
    {% for question in questions %}
    <li><a href="{% url 'polls:detail' question.id %}">{{question}}</a></li>
    {% endfor %}
</ul>
{% else %}
<p>no questions</p>
{% endif %}

이와같이 app_name을 생성해서 url의 name이 곂치더라도 url를 구분할수 있게되었습니다

오류 처리

잘못된 페이지로 접근하려할때 404페이지가 나오게됩니다

서버는 각종 요청을 처리를 하면서 해당 결과를 status Code로 응답하게 되는데 해당 코드가 무슨 의미가있는지 찾아본다면 오류를 해결할수 있습니다

detail페이지로 없는 id로 접근시 해당쿼리를 작동하며 값을 찾을수없어 500에러를 발생하게 됩니다

하지만 없는 페이지로 접근할려는 시도 였기때문에 404에러로 처리해줍시다

이제 detail페이지가 없는 id로 접근시 생기는 오류를 처리해봅시다

from django.shortcuts import render, get_object_or_404

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

get_object_or_404를 임포트해서 사용하면 해당 get을 했을시 object를 찾을수 없으면 404코드를 response하게 됩니다

이번엔 detail을 수정해서 form을 보내봅시다

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

{% csrf_token %} 는 장고 서버에서 허가하는 form 인지를 확인할 토큰입니다

urls도 수정해줍시다

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

submit을 누르면 이동할주소를 처리해주고

views로 가서 기능도 처리해줍니다

from django.urls import reverse
from django.http import HttpResponse, HttpResponseRedirect

def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    selected_choice = question.choice_set.get(pk=request.POST['choice'])
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))

HttpResponseRedirect와 reverse를 사용해서 작업이 완료되면 polls의 index화면으로 보내줍시다

이제 브라우저로 돌아가서 값을 체크한뒤 vote버튼을 누르면

choice의 votes값이 1이 올라가는것을 볼수있습니다

하지만 웹에서 다양한 작업들을 진행하며 오류가 생길수있습니다. 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' : '선택이 없습니다'})
    else :
        selected_choice.votes += 1
        selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))

views로 이동해서 try문을 사용해서 오류발생시 detail페이지로 기존 question값과 함께 error_message를 가지고가게 만들었습니다

가져간 메세지를 표시할수있게 detail을 수정해줍니다

<form action="{% url 'polls:vote' question.id %}" method="post">
    {% csrf_token %}
    <h1>제목 : {{question.question_text}}</h1>
    {% if error_message %}
    <p><strong>{{error_message}}</strong></p>
    {% endif %}
    <ul>
    {%for choice in question.choice_set.all%}
        <input type="radio" name="choice" id="choice{{forloop.counter}}" value="{{choice.id}}">
        <label for="{{forloop.counter}}">{{choice.choice_text}} : {{choice.votes}}</label>
        <br/>
    {% endfor %}
    </ul>
    <input type="submit" value="Vote">
</form>

이제 잘못된선택을 하고나면 제목 하단에 에러메세지가 출력되게 수정되었습니다

다른 오류상황을 상상해봅시다

A가 A서버에서 A를 선택하고

B가 B서버에서 A를 선택하고

두 동작이 동시에 작동 할 경우를 생각해본다면 vote는 두 명이 했지만 결과는 둘이 동시에 db에서 votes의 값을 읽어와 계산을 한뒤 메모리에 저장 해당 내용을 save하는 과정을 지나며 결국 하나의 값만을 반영이 되게 됩니다 이러한 오류를 수정해봅시다

from django.db.models import F

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' : f"선택이 없습니다 : id = {request.POST['choice']}"})
    else :
        selected_choice.votes = F('votes') + 1
        selected_choice.save()
    return HttpResponseRedirect(reverse('polls:index'))

F(’votes’)는 db에서 바로 값을 읽어와 사용하는 기능으로 해당 기능을 사용하면 동시에 작업이 진행되며 하나의 값이 씹히는 상황을 피할수 있습니다

기능을 제작 할 때는 발생 할 수 있는 오류를 최대한 상상해서 해당 오류가 발생하지않도록 하는 노력이 필요로합니다

이번에는 투표후 index가 아닌 결과를 보기위한 페이지로 이동시켜보겠습니다

urlpatterns = [
    path("", views.index, name='index'),
    path('<int:question_id>', views.detail, name="detail"),
    path('<int:question_id>/result', views.result, name="result"),
    path("<int:question_id>/vote/", views.vote, name="vote"),
]
<h1>제목 : {{question.question_text}}</h1>
{%for choice in question.choice_set.all%}
    <label for="{{forloop.counter}}">{{choice.choice_text}} : {{choice.votes}}</label>
    <br/>
{% endfor %}
def result(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/result.html', {'question': question})

path를 추가하고, result.html을 생성한뒤 views에서 result작업을 진행해줍시다

그리고 result 페이지로 이동할수있게 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' : f"선택이 없습니다 : id = {request.POST['choice']}"})
    else :
        selected_choice.votes = F('votes') + 1
        selected_choice.save()
    return HttpResponseRedirect(reverse('polls:result', args=(question_id,)))

reverse('polls:result', args=(question_id,))를 보면 알수있듯 reverse안에 arg=(question_id,)를 넣어 question_id를 같이 보내줍니다.

이제 vote를 누르면 index가 아닌 result페이지로 가서 투표결과를 확인할수 있게되었습니다

장고 어드민 커스터마이징

from django.contrib import admin
from .models import Question, Choice

# Register your models here.
admin.site.register(Choice)

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

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문섹션', {'fields': ['question_text']}),
        ('생성일', {'fields' : ['pub_date'],'classes' : ['collapse']}),
    ]
    readonly_fields = ['pub_date']
    inlines = [ChoiceInline]

admin.site.register(Question, QuestionAdmin)

admin.py로 들어가서 코드를 수정해줍니다

QuestionAdmin의 fieldsets 은 각 필드에 효과나 설명을 넣을수 있습니다 classes로 collapse를 가져간 생성일을 admin페이지에서 확인해보면 기본으로 접혀있는상태로 있는것을 확인할수 있습니다

readonly_fields는 해당 필드를 readOnly상태로 만듭니다

ChoiceInline은 model Choice를 선택하여 1개의 엑스트라 만큼을 생성 한 뒤 QuestionAdmin의 inlines로 들어가 admin페이지에서 확인해보면 question에서 바로 choice를 수정할수있는것을 볼수있습니다

ChoiceInline의 .TabularInline을 StackedInline으로 바꾸면 choice의 입력창의 구조가 바뀌는볼수있습니다

이번에는 목록을 커스터마이징을 해봅시다

from django.contrib import admin
from .models import Question, Choice

# Register your models here.
class ChoiceInline(admin.TabularInline):
    model = Choice
    extra = 1

class QuestionAdmin(admin.ModelAdmin):
    fieldsets = [
        ('질문섹션', {'fields': ['question_text']}),
        ('생성일', {'fields' : ['pub_date'],'classes' : ['collapse']}),
    ]
    list_display = ('question_text', 'pub_date', 'was_published_recently')
    readonly_fields = ['pub_date']
    inlines = [ChoiceInline]
    list_filter = ['pub_date']
    search_fields = ['question_text', 'choice__choice_text']
admin.site.register(Question, QuestionAdmin)
class Question(models.Model) :
    question_text = models.CharField(max_length=200, verbose_name='질문')
    pub_date = models.DateTimeField(auto_now_add=True, verbose_name='생성일')

    @admin.display(boolean=True, description='최근생성')
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

    def __str__(self):
        if self.was_published_recently :
            return f'[NEW!!]{self.question_text}'
        return self.question_text

list_diplay는 필드를 col의 name에 따라 정리할수 있습니다. 또 모델의 verbose_name를 수정해주면 해당 필드의 name을 원하는대로 적을수 있습니다.

하지만 db에 존재하지않는 was_published_recently또한 col로 표현해줄수 있으며 col의 name은@admin.display(boolean=True, description='최근생성')을 통해 꾸며줄수 있습니다

list_filter 는 해당col에 대한 filter자료구조에 따라 사용할 수 있는 list형태로 만듭니다

search_fields 는 해당 내용을 검색으로 찾을수 있습니다 choice__choice_text를 사용해서 question_text뿐만 아니라 question을 타고 들어가서 choice의 choice_text까지 검색하는것이 가능해집니다

0개의 댓글