Hướng dẫn tạo app django - P4
Bài đăng này đã không được cập nhật trong 5 năm
Phần 4 - Quản lý static files. Customizing Django’s admin site
- Note lại từ tài liệu học django 3.0 https://docs.djangoproject.com/en/3.0
Introducing automated testing
Automated testing là gì ?
- Kiểm tra code
- Kiểm tra chi tiết nhỏ
- Kiểm tra hoạt động tổng thể của phần mềm
- Thực hiện việc kiểm tra một cách tự động
Tại sao cần tests ? Nếu việc tạo ứng dụng thăm dò ý kiến(app polls đã viết ở phần trước) là phần cuối cùng của lập trình Django sẽ làm thì không cần viết tests. Nhưng tính năng của ứng dụng không bao giờ dừng thay đổi.
Nghịch lý được chứng minh là đúng: viết test sẽ mất thêm thời gian, nhưng tests sẽ giúp tiết kiệm thời gian.
Đến 1 thời điểm nào đó, ứng dụng sẽ phức tạp tới mức mà chỉ một thay đổi nhỏ có thể gây hậu quả không ngờ tới. Việc kiểm tra thực tế sẽ gây lãng phí thời gian không kiểm soát.
Nên tests tự động sẽ đặc biệt đúng vì nó thực hiện thay developer và giúp xác định code gây ra hành vi không mong muốn.
Thật khó khăn khi cứ phải tách mình ra khỏi quá trình lập trình năng suất và sáng tạo. Và phải dành thời gian cho công việc viết test 1 cách cực kỳ vô duyên và nhàm chán, nhất là khi đã biết rằng code đang hoạt động rất tốt. Cực hình hơn nữa là trên thực tế spec của các chức năng thay đổi chóng mặt dẫn đến tests cũng phải viết lại (trên thế giới chỉ 5% cty có đội ngũ đủ khả năng viết tests trước khi code tính năng, nguyên nhân bởi vì design của ứng dụng phải cực kỳ hoàn thiện và linh hoạt, design phải cực kỳ hợp lý, chính xác, đầy đủ, chi tiết không bị thay đổi vô tội vạ).
Tuy nhiên, về tổng thể thì viết tests sẽ hoàn thiện hơn là test thủ công hoặc có gắng để tìm ra nguyên nhân của một vấn đề phát sinh. Giữa những điều tốt hãy chọn điều tố nhất - Giữa những điều tối tệ hãy chọn điều ít tồi tệ nhất :v.
Các test không chỉ xác định vấn đề, mà còn ngăn chặn chúng
Nếu không có test, hành vi dự định của ứng dụng trơ nên lờ mờ, ngay cả khi code do chính bạn viết ra, không tin hãy đọc lại code của mình một vài năm trước, tin tôi đi đó là một điều kinh khủng, bạn sẽ cố chọc sâu vào code của chính mình chỉ để tìm hiểu xem nó đang làm gì. Tests thay đổi điều này, nó làm "trong sáng " những đoạn code ngay từ bên trong bản chất, và khi phát hiện ra lỗi có thể tập trung vào đó, ngay cả khi bạn không thấy có lỗi.
Test làm code hấn dẫn hơn
Bạn phải refactor code cho đến khi pass được test, đem so sánh với đoạn code khi chưa kết hợp với test là một cách giúp bạn code rõ ràng hơn.
Code mà không có test sẽ "đồng nghĩa" với việc phá vỡ thiết kế. Dù code bạn có code hay đến mấy thì các developer khác sẽ chỉ nghiêm túc tiếp cận khi họ thấy được tests của ứng dụng.
Tests giúp làm việc teamwork
ứng dụng phức tạp sẽ được duy trì bởi team các developer, vì vậy phải viết test tốt mới lập trình django được.
Chiến lược testing cơ bản
Một số lập trình viên tuân theo quy trình Test-driven development, viết test trước cả khi viết code, tuy có vẻ trái ngược với trực giác, nhưng thực tế nó tương tự những gì hầu hết mọi người thường làm đó laf: mô tả vấn đề, và viết code để giải quyết vấn đề. của phần mềm. Phổ biến hơn là người mới tiếp cận với testing sẽ viết 1 đống code trước khi quyết định rằng có nên có một số tests, Có lẽ viết test càng sớm cang tốt. Cho dù đã code cả nghìn dòng, hiệu quả nhất là hãy ngay lập tức bắt đầu viết test đầu tiên khi thực hiện 1 thay đổi mới, hoặc một tính năng mới, hoặc sửa lỗi.
Đoạn trên này đọc thấy hay vãi nên đọc cho ngấm 
Writing our first test
Xác định 1 bug: method Question.was_published_recently() đúng nếu xác định Question’s pub_date là ở quá khứ, tuy nhiên nếu là ở tương lai (case này chắc chắn không thể xảy ra).
Hệ thống test tự động kiểm tra các file có tên bắt đầu = test:
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)
Chạy test: python manage.py test polls
Điều gì đã xảy ra ?
- Thằng manage.py test polls đã xem xét các tests trong app
polls - Nó tìm thấy subclass của
django.test.TestCase - Nó đã tạo 1 csdl đặc biệt cho mục đích test
- Một Question instance đã được tạo với trường pub_date ở tương lai.
- Sử dụng assertIs() để phát hiện
was_published_recently()trả vềTrue, mặc dù ta mong muốn làFalse
Fix Bug sửa lại polls/models.py:
def was_published_recently(self):
now = timezone.now()
return now - datetime.timedelta(days=1) <= self.pub_date <= now
=> Chạy test: python manage.py test polls
Full test case
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)
Tiếp tục test cả view
Một thay đổi: View đang publish toàn bộ quention nghĩa là bao gồm cả những bản ghi ở tương lai. Và khác với ví dụ bên trên ta sẽ thay đổi view trước khi thay đổi test.
The Django test client django cung cấp test Client giúp mô phỏng hành vi người dùng với code ở múc view level. có thể sử dụng tests.py hoặc:
shell: python manage.py shell:
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
Phương pháp này sẽ chạy với csdl hiện có (không tạo csdl test). Kết quả phụ thuộc vào giờ hệ trống trên máy, nếu TIME_ZONE trong settings.py là đúng.
>>> from django.test import Client
>>> # create an instance of the client for our use
>>> client = Client()
Thay đổi quy vấn view 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]
Bây giờ viết test cho cái view này, thêm vào polls/tests.py:
from django.urls import reverse
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.>']
)
test DetailView
Người dùng vẫn có thể đoán url để vào trang detail của những question không hợp lệ, vậy nên cần hạn chế theo cách tương tự.
polls/views.py:
class DetailView(generic.DetailView):
...
def get_queryset(self):
"""
Excludes any questions that aren't published yet.
"""
return Question.objects.filter(pub_date__lte=timezone.now())
polls/tests.py
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)
Thêm ý về tests
- https://docs.djangoproject.com/en/3.0/intro/tutorial05/#ideas-for-more-tests
- https://docs.djangoproject.com/en/3.0/topics/testing/
Introducing
Phần này nói về cách django quản lý "static files" như js, css, image ...
Customize app's
Tương tự polls/templates/, tạo thư mục polls/static/ để lưu static files.
Các khai báo settings.py liên quan đến static files gọi chung là Django’s STATICFILES_FINDERS, một trong đó là AppDirectoriesFinder sẽ tìm trong thư mục con static của mỗi một INSTALLED_APPS được khai báo.
Nên trước hết tạo file polls/static/polls/style.css:
li a {
color: green;
}
Đem sử dụng tại polls/templates/polls/index.html: (đặt code ở trên cùng file)
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">
Adding background-image
Thêm file polls/static/polls/images/background.gif.
Thêm vào file polls/static/polls/style.css:
Introducing
customizing Django’s automatically-generated admin site
Customize the admin form
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)
Thêm related objects
Có thể thêm khai báo đối tượng Choice
from django.contrib import admin
from .models import Choice, Question
# ...
admin.site.register(Choice)
Kiểm tra lại sẽ thấy trang admin có thêm phần quản trị cho Choice, tuy nhiên không hiệu quả nếu phải tạo question trước rồi quay sang tạo choice, nên sẽ bỏ register Choice, sửa lại như sau: 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)
=> thêm classs ChoiceInline., extra là số object Choice được build sẵn trên màn hình quản trị question.
Vì vấn đề hiển thị, hãy sửa lại StackedInline thành TabularInline:
class ChoiceInline(admin.TabularInline):
Customize the admin change list
Khai báo option là list_display
class QuestionAdmin(admin.ModelAdmin):
# ...
list_display = ('question_text', 'pub_date', 'was_published_recently')
Sửa lại cột WAS PUBLISHED RECENTLY:
=> Tại: 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?'
Thêm filter, search_fields tại admin.py, class QuestionAdmin:
list_filter = ['pub_date']
search_fields = ['question_text']
Customize the admin look and feel
Customizing your project’s templates
Template có thể đặt ở bất cứ đâu, tuy nhiên tốt hơn là tạo thư mục templates đặt ở ngoài cùng project.
=> Thêm đường dẫn templates tại mysite/settings.py, DIRS :
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(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',
],
},
},
]
DIR là list of filesystem directories. => Templates có thể đặt gộp chung lại, tuy nhiên nhưng template chỉ liên quan đến mỗi app cụ thể nên để riêng trong thư mục con template của app đó.
Với template cho admin, django sẽ tìm tới package django/contrib/admin/templates/admin/base_site.html sẽ thấy:
{% block branding %}
<h1 id="site-name"><a href="{% url 'admin:index' %}">Polls Administration</a></h1>
{% endblock %}
=> đây là phần header trang admin
Customizing your application’s templates
Để customize trang admin index, copy template có sẵn từ package admin của django sang thư mục custom template, tương tự base_site.html, copy code sang admin/index.html , biến app_list chính là các apps được hiển thị của project
Tương tự app admin có sẵn trong package, ta cũng có thể biến app của mình thành 1 package của riêng mình
Customize static path
Để sử dụng folder static/ ngoài cùng:
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'static'),
]
All rights reserved