[Django3.0]独自のユーザ情報編集ページの作成

まず編集用のformから作成します。モデルはauth.modelsのUserを使い、UserChangeFormを作ります。

from django.contrib.auth.models import User
class UserChangeForm(forms.ModelForm):
	class Meta:
		model = User
		fields = ['username', 'last_name', 'first_name','email']

続いてviews.pyとurls.py

urlpatterns = [
    // 省略
    path('edit/<int:id>', views.edit, name='edit'),
    path('edit/<int:id>/complete', views.edit_complete, name='edit_complete'),
]

Userモデルの読み込みと、先ほど作成したUserChangeFormを使います。

from django.contrib.auth.models import User
from .forms import UserChangeForm

def edit(request, id):
	data = User.objects.get(id=id)
	params = {
		'data': data,
	}
	return render(request, 'myapp/edit.html', params)

def edit_complete(request, id):
	data = User.objects.get(id=id)
	if(request.method == 'POST'):
		user_data = UserChangeForm(request.POST, instance=data)
		if(user_data.is_valid()):
			user_data.save()
			return render(request, 'myapp/edit_complete.html')
		else:
			params = {
				'data': request.POST,
			}
			return render(request, 'myapp/edit.html', params)
	return render(request, 'myapp/edit_complete.html')

template

変更が反映されています。
mysql> select * from auth_user;
| 15 | hoge | NULL | 0 | user8 | 後藤 | 三郎 | goto@gmail.com | 0 | 1 | 2020-10-11 14:48:35.000000

続いてパスワード変更!

[Django3.0]ユーザの名前変更時にIncorrect string value

mysqlとの接続の問題だと思って、settings.pyのDATABASEで’charset’: ‘utf8mb4’,を追加するも治らず。

### 試した事
views.py でユーザ作成時(User.objects.create_user)に以下の様にエンコードして呼び出すときにdecodeしようとした。

views.py

first_name = request.POST['first_name'].encode('unicode_escape')
last_name = request.POST['last_name'].encode('unicode_escape')

### 問題点
– admin管理ツールからユーザの姓名を日本語で設定しようとすると、Incorrect string valueのエラーになるので、views.pyで.encode(‘unicode_escape’)としても根本的な解決にならない
– 更に、views.pyで以下の様にbyte型に変えてデコードしようとしたが、template側で日本語で表示できず、なんどやってもエンコードされた b’\\u592a\\u90ce’、b’\\u5c71\\u7530′ で表示される

data = User.objects.all()
	i = 0
	for item in data:
		data[i].first_name = item.first_name.encode().decode('unicode-escape')
		data[i].last_name = item.last_name.encode().decode('unicode-escape')
		i += 1

かなり色々試したが、、、

### 解決策
mysql側

mysql> ALTER TABLE auth_user CONVERT TO CHARACTER SET utf8mb4;

これだけ。これで、admin管理ツールからユーザの姓名も日本語に変更できる様になった。
なんですと!!!

[Django3.0]ログインユーザのユーザ作成画面を作っていく

adminページからユーザを作成するのはややアプリの趣向に合わないため、独自のユーザ作成画面を作ります。
まず作り始める前に、auth_userテーブルで項目を確認します。

### auth_userテーブル
入力必須項目は、username, password, first_name, last_name, emailでしょうか。
is_superuser(ユーザの追加削除), is_staff(管理サイトへのアクセス), is_active(論理削除), date_joinedは裏側で設定できそうです。

mysql> describe auth_user;
+————–+————–+——+—–+———+—————-+
| Field | Type | Null | Key | Default | Extra |
+————–+————–+——+—–+———+—————-+
| id | int(11) | NO | PRI | NULL | auto_increment |
| password | varchar(128) | NO | | NULL | |
| last_login | datetime(6) | YES | | NULL | |
| is_superuser | tinyint(1) | NO | | NULL | |
| username | varchar(150) | NO | UNI | NULL | |
| first_name | varchar(30) | NO | | NULL | |
| last_name | varchar(150) | NO | | NULL | |
| email | varchar(254) | NO | | NULL | |
| is_staff | tinyint(1) | NO | | NULL | |
| is_active | tinyint(1) | NO | | NULL | |
| date_joined | datetime(6) | NO | | NULL | |
+————–+————–+——+—–+———+—————-+
11 rows in set (0.00 sec)

### template
auth_userテーブルのカラムを参考に、templateを作成していきます。
username, password, last_name, first_name, emailぐらいでしょうか? 管理ツールからユーザ作成をしたところ、usernameとpasswordだけでユーザ作成が出来てしまったので、一旦、usernameとpasswordだけで作ってみたいと思います。

templates/myapp/register.html

<h1>ユーザ登録</h1>
	<form action="/register/complete" method="POST">
		{% csrf_token %}
		- ユーザネーム<br>
		<input type="text" name="username" maxlength="150" placeholder="ユーザネームを入力してください" required><br><br>
		- パスワード<br>
		<input type="password" name="password" maxlength="128" placeholder="パスワードを入力してください" required><br><br><br>
		<!-- - 苗字<br>
		<input type="text" name="last_name" maxlength="30" placeholder="苗字を入力してください" required><br><br>
		- 名前<br>
		<input type="text" name="first_name" maxlength="30" placeholder="名前を入力してください" required><br><br>
		- メールアドレス<br>
		<input type="email" name="email" maxlength="254" placeholder="メールアドレスを入力してください" required><br><br> -->
		
		<button type="submit" class="btn btn-success">登録</button>
	</form>
	<br>
	{{message}}

### views.py
from django.contrib.auth.models import User
passwordは、make_passwordでハッシュ化する。
取り敢えずis_superuser=0, is_staff=0, is_active=1として設定する。この値は、0 or 1で変更できる。

from django.contrib.auth.hashers import make_password

def register_complete(request):
	if(request.method == 'POST'):
		username = request.POST['username']
		password = make_password(request.POST['password'])	
		user = User(username=username, password=password, is_superuser=0, is_staff=0, is_active=1)
		try:
			user.save()
		except:
			params = {
				'message': 'ユーザ登録できませんでした。ユーザ名を変えて再度登録してください。'
			}
			return render(request, 'myapp/register.html', params)
	return render(request, 'myapp/register_complete.html')

user, passwordのみ作成したユーザでログイン認証を試したが、問題なくログインできた。

ただ、この方法だと管理画面上に表示されている以下のバリデーションが効かない。
Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.
Your password can’t be too similar to your other personal information.
Your password must contain at least 8 characters.
Your password can’t be a commonly used password.
Your password can’t be entirely numeric.

公式ドキュメントのUserCreationFormを見ます。
class UserCreationForm

<h1>ユーザ登録</h1>
	<form action="/register/complete" method="POST">
		{% csrf_token %}
		- ユーザネーム<br>
		<input type="text" name="username" maxlength="150" placeholder="ユーザネームを入力してください" required><br>
		<span style="color:gray;">150文字以下の英数字で入力してください。登録済のユーザ名と重複したユーザ名では登録できません。</span>
		<br>
		- パスワード<br>
		<input type="password" name="password1" maxlength="128" placeholder="パスワードを入力してください" required><br>
		- パスワード再入力<br>
		<input type="password" name="password2" maxlength="128" placeholder="パスワードを入力してください" required><br><br>
		
		<span style="color:gray;">
		8文字以上の英数字で入力してください。パスワードは良く使用されるパスワード、ユーザ名等と似た内容、数字のみでは登録できません。</span>
		<br><br><br>
		- 苗字<br>
		<input type="text" name="last_name" maxlength="30" placeholder="苗字を入力してください" required><br><br>
		- 名前<br>
		<input type="text" name="first_name" maxlength="30" placeholder="名前を入力してください" required><br><br>
		- メールアドレス<br>
		<input type="email" name="email" maxlength="254" placeholder="メールアドレスを入力してください" required><br><br>
		
		<button type="submit" class="btn btn-success">登録</button>
	</form>

views.py

def register_complete(request):
	if(request.method == 'POST'):
		form = UserCreationForm(request.POST)
		if form.is_valid():
			username = request.POST['username']
			password = make_password(request.POST['password1'])	
			first_name = request.POST['first_name'].encode('unicode_escape')
			last_name = request.POST['last_name'].encode('unicode_escape')
			email = request.POST['email']
			user = User.objects.create_user(username=username, password=password, first_name=first_name, last_name=last_name, email=email, is_superuser=0, is_staff=0, is_active=1)
			user.save()
		else:
			params = {
				'message': 'ユーザ登録できませんでした。入力内容を確認の上、再度登録してください。'
			}
			return render(request, 'myapp/register.html', params)
	return render(request, 'myapp/register_complete.html')

decode,encodeの箇所は後で修正する必要があるが、まずは先に進みます。

[Django3.0]ログアウト処理の作成

Django3.0公式ドキュメントを読みながらログアウト処理を作成していきます。

### template
まず、templateでログアウトのリンクを作ります。

<a href="/logout">ログアウト</a>

### urls.py
defのnameをlogoutにすると、ログアウト処理のためにimportするlogout関数と名前が被るので、適当にlogout_authにしておきます。

urlpatterns = [
    // 省略
    path('logout/', views.logout_auth, name='logout_auth'),
    // 省略
]

### views.py

from django.contrib.auth import authenticate, login, logout
def logout_auth(request):
	logout(request)
	return redirect('/login')

ログアウト後
-> ログイン画面にリダイレクト

これで、ログイン -> トップリダイレクト -> ログアウト の一連の流れと書き方を理解しました。
続いて、ユーザ作成処理を書いていきたいと思います。

[Django3.0]ログイン画面の作成およびログイン処理

途中までパラパラ記事を見ながら進めていたが、Django2系とかと同じようにやっても上手く行かなかったので、公式のドキュメントを読みながら進めたいと思います。

Djangoの認証システムを使用する

### forms.py
まず、forms.pyから最初に作ります。AuthenticationFormをimportします。

forms.py

from django.contrib.auth.forms import AuthenticationForm

class LoginForm(AuthenticationForm):
	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		for field in self.fields.values():
			field.widget.attrs['class'] = 'form-control'
			field.widget.attrs['placeholder'] = field.label

### urls.py
ログイン画面用のパス(login/)とログイン認証処理用(login_auth/)、ログイン後にリダイレクトするパス(/)を用意します。

urls.py

urlpatterns = [
    // 省略
    path('login/', views.login_form, name='login_form'),
    path('login_auth/', views.login_auth, name='login_auth'),
    path('', views.top, name='top'),
    // 省略
]

### views.py
– login画面は、forms.pyで作成したLoginFormを読み込みます。
– ログイン認証処理では、postされたusernameとpasswordからauthenticateして、userが存在すればトップページにリダイレクトさせます。

views.py

from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate, login
from .forms import LoginForm

def login_form(request):
	params = {
		'form': LoginForm(),
	}
	return render(request, 'myapp/login.html', params)
	
def login_auth(request):
	username = request.POST['username']
	password = request.POST['password']
	user = authenticate(request, username=username, password=password)
	if user is not None:
		login(request, user)
		return redirect('/')
	else:
		params = {
			'form': LoginForm(request.POST),
		}
		return render(request, 'myapp/login.html', params)

@login_required(login_url='/login/')
def top(request):
	return render(request, 'myapp/top.html')

### template
テストの為、テンプレートは適当に作ってます。※エラーメッセージのところが上手くいってないので、views.py, forms.pyと合わせて修正する必要がある。

<form action="/login_auth/" method="POST">
	{% csrf_token %}
	{{ form.non_field_errors }}
	{% for field in form %}
		{{ field}}
		{{ field.errors }}
	{% endfor %}
	<button type="submit" class="btn btn-success btn-lg btn-block">ログイン</button>
	</form>

ログイン前

ログイン後
-> topページにリダイレクトしました。

続いて、ログアウト処理を作っていきます。

[Django3.0]Auth機能のはじめの一歩 : ログイン後のみに表示

前準備として、migration後、管理者ツールでユーザを作ります。

### 管理者作成
python manage.py createsuperuser
http://192.168.33.10:8000/admin/

– adminとnormalユーザを作成します。

– adminユーザにはsurperユーザ権限を付与しておきます。

### @login_requiredによる制御

ページを二つ作ります。
urls.py

from django.contrib import admin
import myapp.views as views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('apple/', views.apple, name='apple'),
    path('banana/', views.banana, name='banana'),
]

ログインユーザのみ表示したいページには@login_requiredを付けます。
views.py

from django.shortcuts import render
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

@login_required(login_url='/admin/login/')
def apple(request):
	return render(request, 'myapp/apple.html')

def banana(request):
	return render(request, 'myapp/banana.html')

ログインした状態では、両方のページが見れます。

ログアウトした場合、@login_requiredを付けたページの方はログインページにリダイレクトされます。

username, passwordを入力すると、@login_requiredのページも表示される。

@login_requiredだと、ロールでの制御はどうするのかわからんが、取り敢えず表示・非表示は物凄く簡単だということはわかった。
次は、ユーザのget, create, update, deleteあたりだな。

[Django3.0]CSVダウンロード機能を実装する

顧客一覧をメニューからダウンロードできるようにする。
ローカルで実装したように、csvをimportする。
HttpResponseは’minetype’ではなく’content_type’にする
csvの日本語化はcontent_type=’text/csv;charset=utf_8_sig’とする。
あとは、forループで回すだけ。

### views.py

import csv

def csv_export(request):
	filename='clients.csv'
	response = HttpResponse(content_type='text/csv;charset=utf_8_sig')
	response['Content-Disposition'] = "attachment;  filename='{}'; filename*=UTF-8''{}".format(filename, filename)

	w = csv.writer(response)
	w.writerow(['会社名','会社名カナ','事業所名','部署名','役職','担当者名','メールアドレス','住所','電話','FAX','役職','代表者','備考'])
	
	data = Clients.objects.all()
	for item in data:
		w.writerow([item.name, item.name_kana, item.office, item.department, item.position, item.charge, item.charge_mail, item.zipcode + ' ' + item.prefecture + item.address, item.tel, item.fax, item.position_top, item.name_top, item.remark])
	return response

OK、あとはいよいよAuth機能の実装。
メール機能も実装したいが、まずはAuthか。

PythonでCSVを出力したい

DjangoでCSVを出力したいのですが、まずはlocalでpythonで試します。

– encodingを付けないと文字化けするので注意が必要

import csv

file = open('new.csv', 'w', newline='', encoding='utf_8_sig')
w = csv.writer(file)
w.writerow([2191,'テラ',851])
w.writerows([[4563,'アンジェス',1435],[3911,'Aiming',693],[3776,'ブロードバンドタワー',384]])

file.close()

$ python test.py

writerowをforループで良さそうです。これをDjangoで実装します。

Pythonで今月、前月、来月、昨年、毎月あたりを取得する

### 今日
まず本日から

from datetime import datetime, date, timedelta

today = datetime.today()
print(datetime.strftime(today, '%Y-%m-%d'))

$ python test.py
2020-10-10

#### 今年、今月、来月、前月

from datetime import datetime, date, timedelta
from dateutil.relativedelta import relativedelta

today = datetime.today()
print(datetime.strftime(today, '%Y-%m-%d')) # 今日

print(today.year) # 今年
print(today.month) # 今月

next_month = today + relativedelta(months=1)

print(next_month.year) # 来月
print(next_month.month) # 来月

one_month_before = today - relativedelta(months=1)

print(one_month_before.year) # 前月
print(one_month_before.month) # 前月

$ python test.py
2020-10-10
2020
10
2020
11
2020
9

前月や翌月など月ごとの計算はtimedeltaではできないらいしいので、relativedeltaを使うと良いらしい。
月単位で計算できるrelativedeltaはかなり使えますね。

### Python学習でオススメの本
掌田 津耶乃の本は勉強になります。

[Django3.0]商品登録時にレコードidを付番したqrコードを生成したい

### template側
widget_tweaksでforms.pyからformを作っていきます。

### views.py
mysqlに生成した直後に、生成したばかりのレコードidを*.objects.order_by(“id”).last()で取得して、qrコードのurlに付番する。

def stock_complete(request):
	if(request.method == 'POST'):
		data = StocksForm(request.POST)
		if data.is_valid():
			// データ作成
			category_id = request.POST['category']
			name = request.POST['name']
			unit = request.POST['unit']
			price = request.POST['price']
			total = request.POST['total']
			remark = request.POST['remark']
			stock = Stocks(category_id=category_id, name=name, unit=unit, price=price, total=total, remark=remark)
			stock.save()

			// qrコード作成
			new_data = Stocks.objects.order_by("id").last()
			qr = qrcode.QRCode()
			qr.add_data('http://192.168.33.10:8000/qrcode/' + str(new_data.id))
			qr.make()
			img = qr.make_image()
			img.save('./sales/media/img/qrcode/'+ str(new_data.id) +'.png')

			// 完了画面に遷移
			return render(request, 'sales/stock_complete.html')
		else:
			params = {
				'form': StocksForm(request.POST),
			}
			return render(request, 'sales/stock_input.html', params)
	return redirect('/stock/input')

### mysql側

mysql> select * from sales_stocks;
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
| id | name | unit | price | total | remark | created_at | updated_at | category_id |
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
| 4 | Foot Massage Machine Shiatsu | 5 | 50880 | 254400 | US MakertPlace社 | 2020-10-09 18:44:48.012892 | 2020-10-09 18:44:48.012925 | 5 |
+—-+——————————+——+——-+——–+——————-+—————————-+—————————-+————-+
1 row in set (0.00 sec)

### qrコード
id4でqrコードが生成されています。

urlもきちんと反映されています。

商品登録時にレコードidを取得するアイディアが思いつかず、商品詳細画面表示に初めてqrコードを生成するべきかずっと悩んでいましたが、order_by(“id”).last()を思いついたら割と簡単にできた。

続けて登録完了画面にqrコードを表示させて、ダウンロードできるように少し改良しました。