PUROGU LADESU

ポエムがメインのブログです。

【Python】メソッド内でのクラス変数の注意点

インスタンスメソッドからクラス変数を利用する

クラス変数はインスタンスメソッドから利用する場合混乱しやすいため注意が必要。

selfでアクセスする場合、インスタンス変数 -> クラス変数の順で探しに行くため、
クラス変数しかない状態でselfで代入すると、インスタンス変数が作成されてしまい、
以降selfではインスタンス変数にアクセスすることになってしまう。

class Person(object):
    # クラス変数
    count = 1

    def __init__(self, age=8):
        # インスタンス変数
        self.__age = age
    
    def add_count(self):
        # 参照は構わないが、ややこしい可能性あり
        print(self.count)
        # これをするとインスタンス変数になってしまう
        self.count += 1
        # 以下のどれかにする
        Person.count += 1
        # __class__.count += 1
        # type(self).count += 1
        print(self.count)
        print(Person.count)

    # クラスメソッド(clsでクラス変数にアクセス)
    @classmethod
    def get_cls_count(cls):
        return cls.count


これを避けるには以下のどれかの方法でクラス変数にアクセス(更新)する。
参照だけならselfでも問題ないが、可読性や取り違え防止を考えると参照時も統一したほうが良いかもしれない。
見た目のわかりやすさから言うと__class__がいいように思う。

Person.count += 1
__class__.count += 1
type(self).count += 1


[Python入門]クラス変数/クラスメソッド/スタティックメソッド:Python入門(1/2 ページ) - @IT

クラスメソッド内からクラス変数にアクセスする

クラスメソッドからはclsでアクセスする。
ちなみにclsは慣例であって別の名前でも問題ない。

そもそもの話として

そもそもインスタンスメソッドからクラス変数をアレコレ更新する、というのがそもそも設計として良くないとは思われるが、
使いたくなる場面は出てくるだろう。

【Django】ログインを強制する

LoginRequiredMixin

ログインしていることを前提とした機能の場合、
強制的にログイン画面に飛ばしたい。

クラスベースではLoginRequiredMixinを実装することで可能となる。
ログイン後は、指定した画面に遷移する。
デフォルトではアクセスしようとした画面に戻してくれる。

from django.contrib.auth.mixins import LoginRequiredMixin

class HomeView(LoginRequiredMixin, ListView):

このMixinは一番左に記述する必要がある。
遷移先はsettings.pyの下記で設定が可能。
ない場合はデフォルトが適用される。

LOGIN_URL = "accounts:login"

UserPassesTestMixin

チェック関数を自作して条件を通過した場合にアクセスさせる

PermissionRequiredMixin

機能ごとに指定した権限を持つユーザのみアクセスさせる


Djangoの認証システムを使用する | Django ドキュメント | Django

【Django】モデルインスタンスのsaveとバリデーション

Formクラスにもバリデーションを定義することができるが、
Modelクラスにも定義することができる。

full_clean()

バリデーションを実施するにはfull_clean()を呼べばよい。
full_clean()では下記が呼ばれる。
clean_field() -> 各フィールドのvalidatorsに定義したものが呼ばれる
clean() -> フィールドに関連しない全体のチェック。cleanを定義していなければ何もしない。
validate_unique() -> フィールドに定義したunique制約をチェックする
validate_constraints() -> Meta.constraintsに定義したデータベース制約をチェックする

save()では呼ばれないのでその前で呼ぶ。
問題があった場合はValidationErrorをキャッチする。

"""インスタンスの保存処理"""
try:
    sample = Sample()
    sample.full_clean()
    sample.save()

except ValidationError as e:
    print(e)
except DatabaseError as e:
    print(e)
"""モデルクラス"""
def clean(self) -> None:
        """フィールドに関連づかないバリデーションエラー"""
        something_happen = True
        if something_happen:
            raise ValidationError("何らかのエラー!!")

save()で呼ぶにはオーバーライドする。

"""モデルクラス"""
    def save(self, force_insert, force_update, using, update_fields):
        """saveの前になにかさせる場合"""
        self.full_clean()
        return super().save(force_insert, force_update, using, update_fields)
    

is_valid()

そもそもFormクラスのis_valid()を呼ぶことでこいつが呼ばれる思想なのかもしれない。
Formのis_valid() -> full_clean() -> _post_clean() -> Modelのfull_clean()
この順番で呼ばれてくるようだ。

モデルインスタンスリファレンス | Django ドキュメント | Django

【Django】JSONを返すだけのクラスベースView

画面がいらない場合、テンプレートを使わない場合はクラスベースではViewを使用します。
HttpResponseを作ってcontentにセットして返せば良し。
他のメソッドが必要なら、def post, def headなどを定義すれば良し。

from django.views.generic import View

class JsonTestView(View):
    """テンプレート使わない"""
    def get(self, request, *args, **kwargs) -> HttpResponse:
        response = HttpResponse()
        response.content = self.create_json()
        response.headers["Content-Type"] = "text/json"
        return response
    
    def create_json(self):
        data = {
            "id": 1,
            "name": "ジャック",
            "age": 25,
        }
        return json.dumps(data)

【Django】素のSQL文の実行

パターンは2つある

1.モデルオブジェクトに紐づける(SELECTの場合)
2.モデルに関係ないSQLを実行する(INSERT, UPDATE, DELETE, モデル生成しないSELECT)

1.モデルオブジェクトに紐づける

detail = DailyReportDetail.objects.raw("SELECT * FROM reportapp_dailyreportdetail WHERE id = %s", [6])[0]
detail.save()

結果はモデルオブジェクトになるので、モデルに合ったテーブルを指定している必要がある。

モデルを使うなら、主キーは必ずSELECTに含める(ないとエラー)
SELECTにないフィールドを参照したら、その時点で追加のクエリが発行される

2.モデルに関係ないSQLを実行する

INSERT, UPDATE, DELETEはこちら。
SELECTでモデルじゃなく単なる値を取りたい場合もこちら。

from django.db import connection
with connection.cursor() as cursor:
    cursor.execute("UPDATE myapp_person SET flag = 1 WHERE last_name = %s", [name])

with connection.cursor() as cursor:
    cursor.execute("SELECT flag FROM myapp_person WHERE last_name = %s", [name])
    row = cursor.fetchone() タプルで返される
    row = cursor.fetchall() 複数結果、タプルのリスト
    cursor.description タプルの1個目がフィールド名みたい -> [x[0] for x in cursor.description]

fetchone(), fetchall() はレコードをリストで返してくる。名前はつかない。
名前をつけたい場合はnamedtupleを使う。
descriptionを渡してフィールド名を定義し、レコード配列を渡す。

nt_res = namedtuple("Result", [x[0] for x in cursor.description])
res = nt_res(*row)

素の SQL 文の実行 | Django ドキュメント | Django

【Django】標準ログイン画面を使う方法

Djangoにはログイン画面を実装する方法が提供されていますので、
それを使用します。

Djangoの認証システムを使用する | Django ドキュメント | Django

ルーティングの追加

プロジェクトルートのurls.py

urlpatterns = [
    path("accounts/", include("django.contrib.auth.urls")),
    

accounts/login でアクセスできるようになります。
この一行で他にもlogout, password_changeなどが利用可能になります。
カスタマイズする場合はaccounts/loginなどそれぞれ定義します。

htmlテンプレート

loginはテンプレートを自作する必要があります。
テンプレートはregistrationフォルダに入れる必要があります。

他の機能は下記のフォルダに入っている管理画面のテンプレートが使われるようです。
login.htmlはadminの方に入ってるので自作しないといけないです。
.venv/lib/python3.10/site-packages/django/contrib/admin/templates/registration

マニュアルにサンプルが載ってます。
コンテキストのリストにはないが画面内でuserが使える。

<h2>ログイン</h2>

{% if form.errors %}
<p>正しいユーザとパスワードを入れてください</p>
{% endif %}

{% if next %}
  {% if user.is_authenticated %}
  <p>このユーザー({{ user }})はページを見る権限がありません。別のユーザでログインしてください。</p>
  {% else %}
  <p>ページを見るにはログインしてください {{ user }}</p>
  {% endif %}
{% endif %}

<form action="{% url 'login' %}" method="post">
  {% csrf_token %}
  {{ form.as_p }}

  <input type="submit" value="login">
  <input type="hidden" name="next" value="{{ next }}">
</form>

<p><a href="{% url 'password_reset' %}">パスワード忘れた人</a></p>

ログイン画面の設定

settings.py

ログイン画面の場所、ログイン後に遷移する場所の設定。

LOGIN_URL = "login"
LOGIN_REDIRECT_URL = "/admin/"

【Django】カスタムユーザーモデルを使う方法

User

名前はCustomUserとし、単純のためBaseUserManagerを継承することとします。
だだ含まれるフィールドが受け継がれてしまうので、すべてカスタムしたい場合はAbstractBaseUserを継承しましょう。

settings.pyにUserモデルの指定を追加する

AUTH_USER_MODEL = 'accounts.CustomUser'

Manager

これはデータベースにアクセスするためのものです。Userのobjectsにオーバーライドします。
ManagerもデフォルトのUserManagerでなくCustomUserManagerに変更します。
CustomUserManagerはBaseUserManagerを継承します。
ベースはUserManagerからcreate_superuser, create_user, _create_userをコピペして修正しましょう。

_create_userの修正点として、外部参照であるDepartment, UserPowerにデフォルト値を設定します。
上記にはあらかじめデータを作成しておくこと。
必須項目であるemployee_nameも入力チェックをしたいのでextra_fieldsの中をチェックしてエラーを出します。

またpasswordの入力がない場合も、エラーを出すようにします。
なぜかデフォルトのmake_password関数ではパスワード無しでもエラーにならず、
Noneが渡されることでログインできないパスワードが作成されてしまう可能性があります。
なぜこの仕様になっているのかよくわかりません。

Noneが渡されると!SamgQKLYDaqs87093・・のようなパスワードが生成され、
デフォルトハッシュアルゴリズムのpbkdf2_sha256のハッシュ、ストレッチ回数、ソルトを含むパスワードにならないです。

class Department(models.Model):
    code = models.IntegerField("コード", unique=True)
    name = models.CharField("名称", max_length=50)

    def __str__(self) -> str:
        return f"{self.code} - {self.name}"


class UserPower(models.Model):
    code = models.IntegerField("コード", unique=True)
    name = models.CharField("名称", max_length=50)

    def __str__(self) -> str:
        return f"{self.code} - {self.name}"


class CustomUserManager(BaseUserManager):
    def _create_user(self, username, email, password, **extra_fields):
        """
        Create and save a user with the given username, email, and password.
        """
        # 必須フィールドが未入力の場合エラーにする
        if extra_fields.get('employee_name') is None:
            raise ValueError("employee_name must be set")
        if not username:
            raise ValueError("The given username must be set")
        
        # エラーにすべき
        if not password:
            raise ValueError("The given password must be set")
        
        # デフォルト値の設定
        extra_fields.setdefault("department", Department.objects.get(code=0))
        extra_fields.setdefault("power", UserPower.objects.get(code=0))

        email = self.normalize_email(email)
        GlobalUserModel = apps.get_model(
            self.model._meta.app_label, self.model._meta.object_name
        )
        username = GlobalUserModel.normalize_username(username)
        user = self.model(username=username, email=email, **extra_fields)
        user.password = make_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", False)
        extra_fields.setdefault("is_superuser", False)
        return self._create_user(username, email, password, **extra_fields)

    def create_superuser(self, username, email=None, password=None, **extra_fields):
        extra_fields.setdefault("is_staff", True)
        extra_fields.setdefault("is_superuser", True)

        if extra_fields.get("is_staff") is not True:
            raise ValueError("Superuser must have is_staff=True.")
        if extra_fields.get("is_superuser") is not True:
            raise ValueError("Superuser must have is_superuser=True.")

        return self._create_user(username, email, password, **extra_fields)


class CustomUser(AbstractUser):
    employee_name = models.CharField("社員名", max_length=50)
    department = models.ForeignKey(
        "Department", verbose_name="部署", on_delete=models.PROTECT
    )
    power = models.ForeignKey(
        "UserPower", verbose_name="権限", on_delete=models.PROTECT
    )
    entry_date = models.DateField("入社日", null=True)
   
    REQUIRED_FIELDS = ["employee_name"]

    objects = CustomUserManager()

管理画面の編集

管理画面では必須項目や外部参照キーが入力できないので登録するとエラーになります。
ユーザー追加画面項目を定義しているadd_fieldsetsに追加します。

class CustomUserAdmin(UserAdmin):
    list_display = ["id", "username", "employee_name", "department", "power", "is_superuser", "is_staff"]
    add_fieldsets = (
        (
            None,
            {
                "classes": ("wide",),
                "fields": (
                    "username",
                    "password1",
                    "password2",
                    # 追加必須フィールド
                    "employee_name",
                    "department",
                    "power",
                ),
            },
        ),
    )


admin.site.register(CustomUser, CustomUserAdmin)


いろいろハマったけど、authenticateやパスワードハッシュの仕様について勉強になった。
複数バックエンドを用意して順番に認証していくこともできるし、パスワードハッシュの変更の仕組みもあるようだ。