ラベル django の投稿を表示しています。 すべての投稿を表示
ラベル django の投稿を表示しています。 すべての投稿を表示

2024年1月24日水曜日

django-rest-framework を試す

django-rest-framework を試す

概要

django で REST API を構築することができるプラグインがあったので試してみました

環境

  • macOS 11.7.10
  • Python 3.11.6
  • django 5.0.1
  • django-rest-framework 3.14.0

インストール

  • pipenv run djangorestframework

プロジェクト作成

  • pipen run django-admin startproject tutorial .
  • cd tutorial
  • django-admin startapp quickstart
  • cd …
tutorial
├── __init__.py
├── asgi.py
├── quickstart
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── serializers.py
│   ├── tests.py
│   └── views.py
├── settings.py
├── urls.py
└── wsgi.py

マイグレーション

今回はマイグレーションで自動的に作成されるユーザテーブルの情報を使うようです

  • pipenv run python manage.py migrate

ユーザ作成

  • python manage.py createsuperuser --username admin --email admin@example.com

パスワードだけ入力します

シリアライザの作成

データベースのモデルをレスポンスモデルに変換するクラスです

  • vim tutorial/quickstart/serializers.py
from django.contrib.auth.models import Group, User
from rest_framework import serializers


class UserSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = User
        fields = ["url", "username", "email", "groups"]


class GroupSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Group
        fields = ["url", "name"]

ビューの作成

先程のシリアライザを指定します
データベースから取得したデータは指定されたシリアライザを使って自動的にレスポンスデータに変換されます

  • vim tutorial/quickstart/views.py
from django.contrib.auth.models import Group, User
from rest_framework import permissions, viewsets

from tutorial.quickstart.serializers import GroupSerializer, UserSerializer


class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all().order_by("-date_joined")
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]


class GroupViewSet(viewsets.ModelViewSet):
    queryset = Group.objects.all()
    serializer_class = GroupSerializer
    permission_classes = [permissions.IsAuthenticated]

URL マッピングの追加

admin 用の画面と実際に REST API をコールするためのマッピングを追加します
REST は DefaultRouter を使って定義します

  • vim tutorial/urls.py
from django.urls import include, path
from rest_framework import routers

from tutorial.quickstart import views

router = routers.DefaultRouter()
router.register(r"users", views.UserViewSet)
router.register(r"groups", views.GroupViewSet)

urlpatterns = [
    path("", include(router.urls)),
    path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]

urlpatterns += router.urls

自動ページネイトの追加

SQL から取得したデータが長い場合などに自動でページネイト処理をしてくれるようです

  • vim tutorial/settings.py
REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
}

REST アプリの追加

最後に REST アプリのの設定を追加します

  • vim tutorial/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "rest_framework",
]

動作確認

  • pipenv run python manage.py runserver

認証あり API なので先程作成したユーザとパスワードを入力しましょう

  • curl -s -u admin -H 'Accept: application/json' http://localhost:8000/users/ | jq .
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "url": "http://127.0.0.1:8000/users/1/",
      "username": "admin",
      "email": "admin@example.com",
      "groups": []
    }
  ]
}

ブラウザで http://localhost:8000/users/ いアクセスすると管理画面を使えます
右上のログインから admin ユーザでログインすると REST API をコールするクライアントとして使えます

最後に

django-rest-framework を試してみました

触りだけなので何ともですがリクエストモデルやレスポンスモデルはシリアライザを使えばうまく表現できるかもしれません
ビューの queryset などは独特の記法なのでこのあたりは django に慣れている場合は嬉しいかもしれません
URL マッピングなども django の機能をそのまま使っているようです

admin が付属しているのでテストなどはしやすいかもしれません

参考サイト

2024年1月19日金曜日

Django と celery の連携方法

Django と celery の連携方法

概要

Django と Celery を連携してみました
公式のドキュメントだと django 側には celery 連携のドキュメントなく celery 側にあるようです

環境

  • macOS 11.7.10
  • Python 3.11.6
  • Django 5.0.1
  • celery 5.3.6
  • Redis 7.2.1

mysite/settings.py

一部抜粋

# for celery
CELERY_CACHE_BACKEND = "default"
CELERY_BROKER_URL = "redis://localhost:6379"

polls/celery.py

import os

from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")

app = Celery("polls")
app.config_from_object("django.conf:settings", namespace="CELERY")
app.autodiscover_tasks()


@app.task(bind=True, ignore_result=True)
def debug_task(self):
    print(f"Request: {self.request}")

polls/__init__.py

from .celery import app as celery_app

__all__ = ("celery_app",)

ワーカー起動

  • pipenv run celery -A polls worker -l INFO

動作確認

  • pipenv run python manage.py shell
from polls.celery import debug_task
debug_task.apply_async()

最後に

連携自体はそれほど難しくないようです
設定やタスクは celery の公式にあるように記載しましたが別ファイルで管理してもいいかなと重ます

参考サイト

2024年1月18日木曜日

Django で MySQL を使う

Django で MySQL を使う

概要

前回作ったチュートリアルアプリは SQLite で動作していました
今回は MySQL に切り替えてみたいと思います

環境

  • macOS 11.7.10
  • Python 3.11.6
  • Django 5.0.1
  • MySQL 8.1.0

データベース作成

  • create database django_test;

settings.py の編集

ドライバは PyMySQL を使います
公式おすすは mysqlclient のようです
DATABASES の部分のみ紹介します
ユーザ名やホストは適宜変更してください

  • vim mysite/settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.mysql",
        "NAME": "django_test",
        "HOST": "localhost",
        "PORT": 3306,
        "USER": "root",
        "PASSWORD": "",
        # "ENGINE": "django.db.backends.sqlite3",
        # "NAME": BASE_DIR / "db.sqlite3",
    }
}

manage.py の編集

main の先頭で install_as_MySQLdb をコールします

  • vim manage.py
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys

import pymysql


def main():
    pymysql.install_as_MySQLdb()
    """Run administrative tasks."""
    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
    try:
        from django.core.management import execute_from_command_line
    except ImportError as exc:
        raise ImportError(
            "Couldn't import Django. Are you sure it's installed and "
            "available on your PYTHONPATH environment variable? Did you "
            "forget to activate a virtual environment?"
        ) from exc
    execute_from_command_line(sys.argv)


if __name__ == "__main__":
    main()

マイグレーション

  • pipenv run python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying polls.0001_initial... OK
  Applying sessions.0001_initial... OK

動作確認

mysql -u root -p django_test -e "show create table polls_question \G"
Enter password: 
*************************** 1. row ***************************
       Table: polls_question
Create Table: CREATE TABLE `polls_question` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `question_text` varchar(200) NOT NULL,
  `pub_date` datetime(6) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci

最後に

切り替え自体は簡単に行なえます
プロダクションでは mysqlclient を使ったほうがいいかなと思います
DATABASES で複数のデータベースを管理するのはどうやればいいのだろうか

参考サイト

2024年1月17日水曜日

Django のチュートリアルをやってみた

Django のチュートリアルをやってみた

概要

実は触っことがなかったのでチュートリアルを消化してみました

簡単な Hello world からモデルの定義、テンプレートの利用など基本的な使い方を紹介しているようです

環境

  • macOS 11.7.10
  • Python 3.11.6
  • Django 5.0.1

インストール

  • pipenv install django

プロジェクト作成

  • pipenv run django-admin startproject mysite
  • cd mysite

以下作業は pipenv + mysite 配下のディレクトリで行っていきます

Hello world

  • pipenv run python manage.py startapp polls

チュートリアルでは投票アプリを作るようなのでそのまま指示に従ってアプリを作成します

Hello world を返却するビューを作成します

  • vim polls/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse(b"Hello world.")

ビューをルーティングに追加します

  • vim polls/urls.py
from django.urls import path

from . import views

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

大本のサイトから投票アプリを参照します

  • vim mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("polls/", include("polls.urls")),
    path("admin/", admin.site.urls),
]

あとはアプリを起動して

  • pipenv run python manage.py runserver
  • curl localhost:8000/polls/

にアクセスして Hello world のテキスト返ってくることを確認します

データベースマイグレート

  • pipenv run python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

デフォルトは SQLite のようです
mysite/settings.py に記載してある INSTALLED_APPS にあるアプリをマイグレートするようです

実際にテーブルの一覧はこんな感じでした

  • sqlite3 db.sqlite3
SQLite version 3.32.3 2020-06-18 14:16:19
Enter ".help" for usage hints.
sqlite> select name from sqlite_master where type = 'table';
django_migrations
sqlite_sequence
auth_group_permissions
auth_user_groups
auth_user_user_permissions
django_admin_log
django_content_type
auth_permission
auth_group
auth_user
django_session

モデルの作成

データベースのモデルを定義します
このあとこれを使って再度マイグレーションしテーブルを作成します

  • vim polls/models.py
import datetime

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


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

    def __str__(self):
        return self.question_text

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


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

    def __str__(self):
        return self.choice_text

__str__ などのメソッドはあとで追加することになるので先に実装しています

polls の設定を追加します
mysite/settings.py は長いので変更部分のみ記載しています
INSTALLED_APPS に PollsConfig を追加します

  • vim 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",
]

アプリ用のテーブルの作成

  • pipenv run python manage.py makemigrations polls
Migrations for 'polls':
  polls/migrations/0001_initial.py
    - Create model Question
    - Create model Choice

マイグレーションファイルが作成されることを確認します
SQLAlchemy のようにマイグレーションファイルが微妙な場合は自分で修正しましょう

マイグレーションファイルが作成できたら実際にマイグレーションしてみます
sqlmigrate コマンドで事前に実行される SQL を確認することもできます

  • pipenv run python manage.py sqlmigrate polls 0001
  • pipenv run python manage.py migrate

マイグレートコマンドは先程と同じです

Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Applying polls.0001_initial... OK

実際にテーブルを確認すると polls_question と polls_choice テーブルが追加されています

sqlite> .schema polls_question
CREATE TABLE IF NOT EXISTS "polls_question" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "question_text" varchar(200) NOT NULL, "pub_date" datetime NOT NULL);
sqlite> .schema polls_choice
CREATE TABLE IF NOT EXISTS "polls_choice" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "choice_text" varchar(200) NOT NULL, "votes" integer NOT NULL, "question_id" bigint NOT NULL REFERENCES "polls_question" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "polls_choice_question_id_c5b4b260" ON "polls_choice" ("question_id");

おまけ: 作成したモデルをコンソールから操作する

rails console のような機能があり作成したモデルを操作することができるようなので試してみます

  • pipenv run python manage.py shell

これで作成した Question や Choice モデルが扱えます
操作方法は公式にあるので割愛しますが簡単なデバッグが挙動の確認、テーブル情報の確認をしたいときには便利かなと思います

テスト用のデータなども登録しているので一通り流しておきましょう

from polls.models import Question, Choice
Question.objects.all()
from django.utils import timezone
q = Question(question_text="What's new?", pub_date=timezone.now())
q.save()
q.id
q.question_text
q.pub_date
q.question_text = "What's up?"
q.save()
Question.objects.all()

q = Question.objects.get(pk=1)
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)
c = q.choice_set.filter(choice_text__startswith="Just hacking")
c.delete()

admin ツールを使う

django には標準で admin 用のツールがありデータの確認や登録がアプリとは別で行うことができるようになっています

  • pipenv run python manage.py createsuperuser

ユーザ名、パスワード、メールアドレスの入力を求められるので入力しましょう
サーバを起動し localhost:8000/admin/ にアクセスすると管理画面に遷移します

  • pipenv run python manage.py runserver

  • vim polls/admin.py
from django.contrib import admin

from .models import Question

admin.site.register(Question)

これで再度リロードすると polls アプリのオブジェクト (データベース) も扱うことができます

適当に登録、更新、削除など試してみると良いかなと思います
デフォルトで更新履歴も管理してくているようです

ビューを追加する

アプリの機能を追加していきます

  • polls/views.py
from django.http import HttpResponse


def index(request):
    return HttpResponse(b"Hello world.")


def detail(request, question_id):
    return HttpResponse(f"You're looking at question {question_id}.")


def results(request, question_id):
    return HttpResponse(f"You're looking at the results of question {question_id}.")


def vote(request, question_id):
    return HttpResponse(f"You're voting on question {question_id}.")
  • polls/urls.py
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"),
]

テンプレートの追加

少し独特なパスですが以下のようにすることで自動で読み込んでくれます
html ファイルで構文は j2 です (たぶん)

  • mkdir -p polls/templates/polls/
  • vim polls/templates/polls/index.html

url という特殊なタグが使えるようです

urls.py
の name とリンクしているようです
{% if latest_question_list%}
<ul>
{% for question in latest_question_list %}
  <li><a href='{% url "detail" question.id %}'>{{ question.question_text }}</a>
{% endfor %}
</ul>
{% else %}
<p>No polls are available.</p>
{% endif %}
  • vim polls/templates/polls/detail.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
  <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

ビュー側です
django.shortcuts モジュールを使うとテンプレートの呼び出しやエラーハンドリングを簡単に書けるようです

  • vim polls/views.py
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, 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)


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


def results(request, question_id):
    return HttpResponse(f"You're looking at the results of question {question_id}.")


def vote(request, question_id):
    return HttpResponse(f"You're voting on question {question_id}.")

フォームの作成

先程作成したテンプレートを改良して form post できるようにします

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

結果を表示するテンプレートも作成します

  • vim polls/templates/polls/results.html
<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all%}
  <li>{{ choice.choice_text }} -> {{ choice.votes }} vote{{ choice.votes | pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

POST が受け取れるように vote 関数を変更します
フォームデータには request.POST でアクセスするようです

  • vim polls/views.py
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse

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)


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


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


def vote(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    try:
        selected_choice = question.choice_set.get(pk=request.POST["choice"])
    except (KeyError, Choice.DoesNotExist):
        return render(
            request,
            "polls/detail.html",
            {"question": question, "error_message": "You didn't select a choice"},
        )
    else:
        selected_choice.votes += 1
        selected_choice.save()
        return HttpResponseRedirect(reverse("polls:results", args=(question.id,)))

汎用ビュー化

ビューを少しリファクリングします
今まで関数で定義していたビューをクラス化します
汎用ビューはビューで使用するモデルとテンプレートが決まっている場合に簡潔にビューを定義できる機能です

  • vim 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 Question


class IndexView(generic.ListView):
    template_name = "polls/index.html"
    context_object_name = "latest_question_list"

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

URL マッピング側も関数ではなくクラスを参照するように書きえます
汎用ビューの場合には pk というパスパラメータが自動的に使用されるので question_id -> pk に変更します

  • vim polls/urls.py
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"),
]

テストを書く

django.test.TestCase のサブクラスとして書くようです
テストファイルのアプリ配下に書きます

  • vim polls/tests.py
import datetime

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

from .models import Question


class QuestionModelTests(TestCase):
    def test_was_published_recently_with_future_question(self):
        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):
        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):
        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)
  • pipenv run python manage.py test polls

テストは失敗します
バグがあるので Question モデルの was_published_recently を修正します

  • vim polls/models.py
import datetime

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


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

    def __str__(self):
        return self.question_text

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


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

    def __str__(self):
        return self.choice_text

これで再度テストを実行すると OK になります

以下続きますが省略します
やってることはそれほど難しくないので興味があれば続けてやってみるよ良いかなと思います

ビューのテスト

テストクライアントがあるのでそれを使います

デザインの追加

css や画像を追加します

Admin のカスタマイズ

リレーション先の参照やフォームのカスタマイズをします

デバッグツール

django-debug-toolbar を使います

自分のアプリのパッケージ化

Pypi で公開できるようにパッケージ化します

感想

  • とにかくドキュメントが充実している
  • Ruby で言うところの rails
  • django が提供しているモデルやツールを使う感じなので DSL という感じが強かった
  • ディレクトリ構成が強制されるのは良いことだと思うが少し変な構造な感じもした (tests や admin など)
  • チュートリアルのコードはコピペで横着せず全部自分でタイプしながらやると力になる
  • admin ツールが標準装備なので便利 (テストデータの登録や確認など)
  • チュートリアルだと pyright のタイプエラーが結構出る
  • pydantic との連携が微妙そう (アダプタ的なのはあるがあまりメンテされてなさそう)

最後に

途中までですが django のチュートリアルを試してみました
まさにフレームワークという感じで書き方や作り方はルールに則って進める必要があります
簡単に構築したい場合にはおすすめなのかもしれませんが如何せんブラックボックスが多いのと独特な機能や記法を使う部分も多いので Python 全般というよりかは django という機能や言語を学んでいるという感覚が強いかなと思います

参考サイト