PUROGU LADESU

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

【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やパスワードハッシュの仕様について勉強になった。
複数バックエンドを用意して順番に認証していくこともできるし、パスワードハッシュの変更の仕組みもあるようだ。

ORマッパーなんか要らない

DjangoのORマッパーに四苦八苦していますが、そもそも必要ないんじゃないかという話。
もちろん単純なSELECTやfilter条件などを使うだけなら難しくもないけれど、
QとかFとかAnnotateとかどう考えたらいいのかわからない。

SQLを覚えたほうが早い

ORMのメリットとしてSQLを知らなくても良いみたいなのがあるようですが、
SQLを知らないならSQLを学習するほうが簡単だと思います。

汎用性がない

ORMはフレームワークが変わったらやり方が変わりますが、SQLはどのフレームワークでも使えます。
ORMの学習コストを考えればSQLを覚える方がコスパはいいかなあ。

結局SQLの知識が必要

複雑になればどういうSQLが発行されたのか確認する必要があると思います。
なので手順としては、
SQLを考える -> ORMでどう書くか調べる -> 結果としてのSQLを確認
みたいな感じになるため2度手間ですね。

ORMを知らなくてもフレームワークは使える。

結局データベースからデータをどう取るかという話です。
rawを使えばSQLを使えますし無理に学習する必要はなさそうです。
フレームワークを学習する場合にORマッパーは触りだけで良いと思います。

メリットがあるとすれば

データベースソフトに合わせて出力を変えてくれるというところでしょうか。(よく知らない)
ただDBが変わるなんて早々ないですし、変わったからORMに丸投げしてそのままリリースはできないでしょう。

確かに理解してしまえばドットだけでアクセスできるのは気持ちいいですし、最初はスゲエと思いました。
でもSQLをシコシコ書かなくていいのは楽といえば楽ですが、違うところでハマって時間食いそうな感じがしますね。

docs.djangoproject.com

【Django】VSCode拡張

なんか久しぶりにDjangoを開いたけどhtml補完がなんかいまいち。

もともとDjaneiroを入れてたけど更新されていないようだ。

というわけでコレ

marketplace.visualstudio.com

入れたらhtml補完ができなくなった。というか以前はできないと思っていたが、回避方法は公式に書いてあった。

Emmet: Include Languages
ここにItem:django-html, Value:htmlを追加する。

エメットとは?

emmetなんて知らなかったが、htmlタグとかコードを省略入力する機能でVSCodeには標準搭載されているという。

例えばformと打って確定したら、このようなものがすぐできる。

<form action=""><form>

もちろんDjangoのforとかifとかextendsとかも閉じタグまで用意してくれる。
htmlの補完機能もhtml snippetのextensionを使っていたがもう要らね。

divと打てばdivのhtmlタグが入るし、blockと打てばテンプレートのタグが入る。いい感じ!

Bongo Cat Buddy

ついでにこんなかわいい拡張機能を見つけました。
一緒にタイピングしてくれます。
エディタの一部を専有し、マジ邪魔ですがw
ステータスバーサイズで邪魔にならないのもあるみたいです。

marketplace.visualstudio.com

【C#】delegate、Action、Func

宣言

関数の入れ物。定義した型の関数を格納できる。
delegateキーワードを付けて引数、返り値の型を宣言する。

delegate void del_func(int val);

new

以前はnewでインスタンス化していたが、v2.0からインスタンス化は不要
del_func dft = new del_func(func1);
del_func dft = func1;

マルチキャスト

一つのdelegateに複数登録すると、全て呼び出される。
del_func dfm = func1;
dfm += func2;
dfm += func3;
dfm(6);

Action, Func

delegateをいちいち宣言せずに使える。
Actionは返り値なし、Funcは返り値あり。
引数と返り値の型は<>内に定義する。
Action act = func1
Func fnc = func1

ラムダ式

匿名メソッドを格納できる。delegateキーワードも不要。
Action act2 = delegate (int x) { Console.WriteLine(x); };
Action act3 = (x) => { Console.WriteLine(x); };

まとめ

というわけで、ラムダ式を使えばよし。

AutoHotKeyでキーマッピングを変更する

キーマッピングを変更するソフトです。
www.autohotkey.com

Macならkarabiner-elementsを使っていますが、
Windowsはこいつを使ってます。

スクリプトファイル(ahkファイル)にキーバインドを設定し、
ファイルを何処かに設置してダブクリで起動します。

するとタスクバーに常駐するようになります。

ChangeKey.ahk

;AutoHotkey
;Remapping the Keyboard
;http://ahkwiki.net/KeyList
;http://ahkwiki.net/Send

;無変換+JIKLで矢印キーの動作
vk1D & I::Send,{Blind}{Up}
vk1D & J::Send,{Blind}{Left}
vk1D & K::Send,{Blind}{Down}
vk1D & L::Send,{Blind}{Right}

;無変換+Aで前の単語、無変換+;で次の単語の動作(ctrlは^をつける)
vk1D & H::Send,{Blind}^{Left}
vk1D & vkBB::Send,{Blind}^{Right}

;無変換+GでHOME、無変換+:でENDの動作
vk1D & G::Send,{Blind}{Home}
vk1D & vkBA::Send,{Blind}{End}
vk1D & N::Send,{Blind}{Home}
vk1D & vkBF::Send,{Blind}{End}

;無変換+OでBackSpace、無変換+UでReturnの動作
vk1D & O::Send,{Blind}{BS}
vk1D & U::Send,{Blind}{Return}

;無変換を全角半角にする
vk1D::Send,{Blind}{vkF3}

;CapsLockで数字入力
;なぜか解除はCapsLock+Shift
;sc03A & sc079::.
;sc03A & Space::0
;sc03A & M::1
;sc03A & vkBC::2
;sc03A & vkBE::3
;sc03A & J::4
;sc03A & K::5
;sc03A & L::6
;sc03A & U::7
;sc03A & I::8
;sc03A & O::9

;Win+tでvk確認
;#t::
;key  := "/" ; Any key can be used here.
;name := GetKeyName(key)
;vk   := GetKeyVK(key)
;sc   := GetKeySC(key)
;MsgBox, % Format("Name:`t{}`nVK:`t{:X}`nSC:`t{:X}", name, vk, sc)

速習Python:パッケージ

パッケージを作成

my_package の中の utils.py
(v3.3以降は__init__.pyは必須でない)

def sample1_func():
    print("this is sample 1 func")

def sample2_func():
    print("this is sample 2 func")
パッケージのインポート

フルパスでパッケージ名を明示するか、
fromを使ってパッケージを省略する。
asで別名をつけるをわかりにくくなる恐れがある。

import my_package.utils
from my_package import utils

my_package.utils.sample1_func()
utils.sample1_func()

importの*はinit.pyのallで定義されたもののみ読み込む
__all__ = ['utils', 'tool']
*でのimportは推奨されない

ImportError

バージョンによって読み込むものを変えたい場合など

try:
   from xxx1 import utils
except ImportError:
   from xxx2 import utils
setup.pyでパッケージ化

setup.py を作成しsdistを実行すると、tar.gzファイルが出来上がり配布できる

from distutils.core import setup

  • setuptools

多機能
find_packageでpythonファイルを見つける
install_requiresで必要なライブラリをインストールできる

  • distutils

指定したものをパッケージ化
※これはもう非推奨となるのでsetuptoolsを使いましょう
python setup.py sdist
tar zxvf xxxx.tar.gz

  • インストール

tar zxvf xxxxxx
cd xxxxxx
sudo python setup.py install

速習Python:内包表記、例外処理

リスト内包表記

[]の中にfor文をかいて配列を返すような場合、簡潔に書ける。

t = (1,2,3,4,5)
r = []
for i in t:
    r.append(i)

r = [i for i in t]

ディクショナリ内包表記

zipを使う

w = ['mon','tue','wed']
f = ['coffee','milk','water']

d = {}
for x, y in zip(w, f):
    d[x] = y

d = {x: y for x, y in zip(w, f)}

集合内包表記

{}に値だけ入れれば集合になる。キーバリューを入れたらディクショナリ。

s = set()
for i in range(5):
    s.add(i)

s = {i for i in range(5)}

ジェネレータ内包表記、タプル内包表記

タプルの場合は tupule()
ふつうの()はジェネレータ

g = (i for i in range(5))
t = tuple(i for i in range(5))

スコープ

関数内からグローバル変数を書き換えるにはglobalをつける
locals()でローカル変数を列挙
globals()でグローバル変数を列挙

amount = 8
def foo():
    global amount
    print(amount)
    amount = 5
    print(amount)
    print(locals())

foo()
print(globals())

例外処理

Exceptionで予期しないエラーもキャッチするようなことはしてはだめ
ちゃんと想定するエラーのみをキャッチする

try:
    liex = []
    # liex[3]
    # lino[4]
except IndexError as ex:
    print(f"err: {ex}")
except Exception as ex:
    print(ex)
else:
    print("処理成功、エラーなし")
finally:
    print("常に実行")
独自エラーの作成、Exceptionを継承する
class MyException(Exception):
    pass

エラーを発生させる
raise IndexError('test')