Django自分用メモになります
今回はFormの操作についてまとめます
開発環境
OS:mac
エディタ:vscode
python:3.10.9
django:4.1.0
フォーム作成の基本的な流れ
#forms.py
class UserForm(forms.Form): #forms.Formを継承してフォームで入力したい項目を設定する
name = forms.CharField(label='your name' max_length=100)
age = forms.IntegerField()
mail = forms.EmailField()
#views.py
form = UserForm() #インスタンスを作成して初期化
return render(request,'template_path',context={'form':form} #contextとしてテンプレートに渡す
#テンプレートへの記述
<form action = '/your-name/' method ='post'> #formタグで囲んでpostで送信する
{% csrf_token %} #セキュリティ必要な記述(クロスサイトリクエストフォージェリ対策用の乱数?)
{{ form }} #formを表示
<input type = 'submit' value = 'Submit'> #送信ボタン忘れずに
</form>
POSTとGETの違いは?
こちらが参考になりました。
自分で噛み砕いたイメージは
GET:URLを詳細に指定してそのリソースを単純に取ってくるのがGET
POST:あるリソースに対して登録や保存といった処理をするために使うのがPOST
csrf:クロスサイトリクエストフォージュリとは
Amazonなどでログイン状態のときに偽のサイトをクリックしたときに、ユーザーが意図しない
操作で勝手に注文や掲示板への書き込みが行われないようcsrf_tokenという乱数文字列を
生成した時にしかフォームの処理がなされないという状態にすることで不正に利用されるのを防ぐことができる
forms.py views.py templatesをつかってフォームを送信した値を受け取るまで
forms.pyでフォームの入力項目を定義
from django import forms
class UserInfo(forms.Form):
name = forms.CharField()
age = forms.IntegerField()
mail = forms.EmailField()
forms.pyでフォームの入力項目を定義
from django.shortcuts import render
from . import forms
def index(request):
return render(request,'formapp/index.html')
def form_page(request):
form = forms.UserInfo() #インスタンス化して初期化
#2.Formで送られたデータを取り出す。デフォルトでは同じ関数上に送られる(ターミナル上に表示)
if request.method =='POST':
form = forms.UserInfo(request.POST) #requestのPOSTの結果をインスタンス化する
if form.is_valid(): #バリデーションをして(フィールドが正しいかをチェック)
print('バリデーション成功')
print(
#cleaned_dataで中身を取り出すことができる
'name:{},age:{},mail:{}'.format(form.cleaned_data['name'],form.cleaned_data['age'],form.cleaned_data['mail'])
)
#1.テンプレートにフォームを渡す
return render(
request,'formapp/form_page.html',context={ #contextで渡してあげる
'form':form
}
)
manage.pyと同じ階層にtemplates/formapp/フォルダを作成してその中にテンプレートを作る
setting.pyでのテンプレートの指定も忘れない!
import os
BASE_DIR = Path(__file__).resolve().parent.parent
TEMPLATE_DIR = os.path.join(BASE_DIR,'templates')
TEMPLATES = [
{
'DIRS': [TEMPLATE_DIR,] #カンマをよく忘れがち
#(略)
},
]
テンプレート中身
<! DOCTYPE html>
<html>
<head>
<meta charset = 'utf-8'>
<title>Form</title>
</head>
<body>
<form method ="POST">
{% csrf_token %} #セキュリティ上必要
<table>
{{form.as_table}}
</table>
<input type = 'submit' value ='送信'>
</form>
</body>
</html>
フォームを送信して、送信した値を受け取るまでが完了。
views.pyでformsのインスタンスを作成してcontextでテンプレートに渡してあげるだけ
フォームのカスタマイズ
より使いやすくするためにカスタマイズができる
#ラベルを変更、文字列長さの設定
field = forms.CharField(label='名前',max_length=10,min_length=5)
#入力必須ではないように変更
field = forms.URLField(required=False)
#テキストエリアに変更
field = forms.CharField(widget=forms.Textarea)
#初期値を設定する
field = forms.BooleanField(initial=True)
#プレースホルダーの設定
field = forms.EmailField(widget=forms.TextInput(attrs={'placeholder':'mail@mail.com'}))
その他、セレクトボックスを見やすくするwidgetもある。
フォームのカスタマイズ その2 クラスやidを与えてcssを当てたりする
やりかたは色々とある
#widget内でattrsからclassを付与する
mail = forms.EmailField(
label='メールアドレス',
widget=forms.TextInput(attrs={'class':'mail-class','placeholder':'sample@mail.com'})
)
#formのクラス内でコンストラクタを使って付与する
def __init__(self, *args, **kwargs):
super(UserInfo,self).__init__(*args,**kwargs) #コンストラクタの親クラス(Form)を呼び出す処理
self.fields['job'].widget.attrs['id']='id_job' #idを付与
self.fields['hobby'].widget.attrs['class']='hobby_class' #classを付与
バリデーションの設定
こちらもいろんなやり方がある
from django.core import validators #バリデーションに必要
#バリデーションの自作
def check_name(value):
if value =='あああああ':
raise validators.ValidationError('その名前は登録できません')
class UserInfo(forms.Form):
#1.自作したバリデーションを付与する(配列なのでカンマで区切って複数設定することもできる)
name = forms.CharField(label='名前',max_length=10,min_length=5,validators=[check_name])
#2.validatorを使ったバリデーションを設定(ドキュメントにこれ以外のvalidatorもあり)
age = forms.IntegerField(label='年齢',validators=[validators.MinValueValidator(20,message='20以上にしましょう')])
homepage = forms.URLField(required=False)
mail = forms.EmailField(
label='メールアドレス',
widget=forms.TextInput(attrs={'class':'mail-class','placeholder':'sample@mail.com'})
)
#メールアドレス再入力させるフィールド
verify_mail = forms.EmailField(
label='メールアドレス再入力',
widget=forms.TextInput(attrs={'class':'mail-class','placeholder':'sample@mail.com'})
)
#3.clean_フィールド名でバリデーションを設定。これ以外の名前ではバリデーションされない
def clean_homepage(self):
homepage= self.cleaned_data['homepage']
if not homepage.startswith('https'):
raise forms.ValidationError('ホームページのURLはhttpsのみ!')
#4.cleanメゾットで複数のフィールドの値をチェックする
def clean(self):
cleaned_data=super().clean() #Formからcleanメゾットを継承
mail = cleaned_data['mail']
verify_mail = cleaned_data['verify_mail']
if mail !=verify_mail:
raise forms.ValidationError('メールアドレスが一致しません')
バリデーション後にModelに格納する(ModelFormを使う)
まずmodelを作成
from django.db import models
class Post(models.Model):
name = models.CharField(max_length=50)
title = models.CharField(max_length=255)
memo = models.CharField(max_length=255)
forms.pyでforms.ModelFormを継承したクラスを作成
#モデルと紐づいたフォームの作成
from .models import Post
class PostModelForm(forms.ModelForm): #継承
#memoをforms.pyで上書きしてテキストボックスにすることもできる
memo = forms.CharField(
widget=forms.Textarea(attrs={'rows':30,'cols':20})
)
class Meta:
model = Post #models.pyのPostと紐付け
fields = '__all__' #全てのフィールドを表示
# fields = ['name','title'] #どのフィールドを表示させるのかを配列で指定
# exclude = ['title'] #表示しないフィールドを指定
views.pyで表示と保存の処理を記述する
from . import forms
#modelと紐づいたフォームをテンプレートに渡す
def form_post(request):
form = forms.PostModelForm() #インスタンスを作成
#送信された際にバリデーションを実行後に保存する(送信された処理はviews.pyに帰ってくるためこちらで保存処理をする)
if request.method =='POST':
form = forms.PostModelForm(request.POST)
if form.is_valid():
form.save()
return render(request,'formapp/form_post.html',context={
'form':form
})
saveメゾットをカスタマイズして実行する
saveメゾットをオーバーライドすることによってデータ保存前に処理を加えることができる
また親(ModelForm)、子(BaseForm)、孫(PostModelForm)クラスという構造にし、
BaseFormに保存時の処理を加え、子クラスにログを出力する処理を共通して持たせることはかなり頻繁に書くコードらしいので要チェック。
#ログを残すためのクラス。各フォームで共通の機能を持たせるためによくやることらしい。親と子と孫クラスをつくるかんじ。
#これを継承させることでModelFormの機能を持ちつつ、なおかつ新たに機能を追加したクラスを作成できる
class BaseForm(forms.ModelForm):
def save(self,*args,**kwargs):
print(f'Form:{self.__class__.__name__}実行')
return super(BaseForm,self).save(*args,**kwargs)
class PostModelForm(BaseForm):#BaseFormを継承したものに書き換え
#省略
class Meta:
#省略
#saveメゾットのカスタマイズ
def save(self,*args,**kwargs):
#オーバーライドするので親クラスのsaveメゾットを呼び出さないとそもそものsaveの機能がなされないのでこちらは必ず呼び出すcommit=Falseとするとこの時点では保存されない
obj = super(PostModelForm,self).save(commit=False,*args,**kwargs)
obj.name = obj.name.upper() #objはPostのインスタンス。保存時に大文字にする処理を加える
print(type(obj))
print('save実行')
obj.save()
return obj
form要素の個別表示
htmlにてform.as_pやform.as_tableとする以外にも個別にフィールドやエラーの表示ができる
{{form.name}} #nameフィールドの表示
{{form.name.label}} #nameフィールドのラベルの表示
{{form.name.errors}} #nameフィールドに対するエラーの表示
{{form.non_field_errors}} #バリデーションチェックで発生したフィールド単体でないエラーを表示(全体のエラーを表示)
#全フィールドのエラーを一箇所に表示
{% if form.errors %}
{#__all__のエラーを表示させない#}
{%if key != '__all__' %}
<p>{{key}}:{{value.as_text}}</p>
{%endif%}
{% endif %}
存在するデータを弾くcleanを設定
def clean(self):
cleaned_data=super().clean() #親のcleanメゾットを呼び出す
title = cleaned_data.get('title') #入力されたtitleを取り出す
is_exists=Post.objects.filter(title=title).first() #入力されたtitleがモデル内に存在した場合にis_existsにいれる
if is_exists:
raise validators.ValidationError('そのタイトルはすでに存在します')
外部のHTMLファイルをそのまま表示する(include)
HTMLファイルを読み込むことも変数を渡して読み込むこともできる
#main.html
{% include "other.html"%} #そのまま外部HTMLファイルを読み込み
#include時に変数に値を渡すことも可
#other.html
<p>{{var1}}</p>
#main.html
{% include "other.html" with var1 = '〇〇'%}
includeを利用すると変数に値を渡せるので値が存在する場合にはそちらを。
存在しない場合に渡したい変数に値を設定することができる
#form_template.html
#変数methodが渡された時はそちらで表示(デフォルトはPOST)
#action属性が変数で渡された時はそちらを適応する
<form method ="{% if method %}{{method}}{% else %}POST{%endif}" action="{% if action %}{{action}}{%endif%}">
#省略
</form>
#form_post.html
#フォームのテンプレートを読み込んだ時に変数に値を渡す
<body>
{% include 'formapp/form_template2.html' with as_table=True method='GET' action='/aaa'%}
</body>
formのaction属性とは
データを送信する送信先を指定するための属性
複数のFormを画面上に表示する(Formset)
#views.py
from django.forms import formset_factory
def form_set_post(request):
TestFormset = formset_factory(forms.FormSetPost,extra=3)# FormSetPostは複数表示したいフォーム #extraはいくつ表示したいのか
#views内でデータを取り出す
#インスタンス化
formset = TestFormset(request.POST or None) #request.POSTの値がある場合はそちらを、ない場合はNoneをいれてただインスタンスを作成する
if formset.is_valid():
for form in formset:
print(form.cleaned_data)
return render(request, 'formapp/form_set_post.html',context={
'formset':formset
})
#template.html
{{formset.as_p}}
#もしくは
{{formset.management_form}} #これをいれないとエラーになる
{% for form in formset %} #for文で書く
{{form.as_p}}
<hr> #forを使うと区切りをいれたりなんかもできる
{% endfor %}
複数のFormを表示してDBに保存(ModelFormset)
#models.pyでmodelを定義する
#viewの記述(基本形)
from django.forms import modelformset_factory
TestFormset = modelformset_factory(Model,form=ModelForm,extra=⚪︎) #対象のモデルとフォームを指定。こちらの場合はforms.pyでforms.ModelFormを継承したフォームクラスでMetaクラスで対象のモデルと紐付けする必要がある
#もしくは
TestFormset = modelformset_factory(Model,fields='__all__',extra=⚪︎)#対象のモデルと表示するフィールド
formset =TestFormset() #インスタンス化
#templateの記述
#Formsetと同じ
def modelform_set_post(request):
TestFormset = modelformset_factory(ModelSetPost,fields='__all__',extra=3)
#インスタンス化と送信された場合viewsで受けとって保存
formset = TestFormset(request.POST or None)#request.POSTの値がある場合はそちらを、ない場合はNoneをいれてただインスタンスを作成する
if formset.is_valid():
formset.save() #保存
return render(request,'formapp/modelform_set_post.html',context={
'formset':formset
})
ModelFormを使ってDBに保存した内容を表示する
#viewsにて
#その他省略
TestFormset = modelformset_factory(ModelSetPost,form=forms.ModelFormSetPost,extra=3)
formset = TestFormset(request.POST or None,queryset=ModelSetPost.objects.filter(id__gt=3))
#forms.py
#formset(ModelForm)ようのform
class ModelFormSetPost(forms.ModelForm):
title = forms.CharField(label='タイトル')
memo = forms.CharField(label = 'メモ')
#modelと紐づいたフォーム作成に必要
class Meta:
model = ModelSetPost
fields = '__all__'
ModelSetPostとはmodel名、ModeFormSetPostとはforms.pyでクラスメタにて対象のモデルと紐付けたフォーム。こちらはforms.ModelFormを継承している
querysetを設定することで元々model内に保存された内容を表示することができる。
ファイルのアップロード
#setting.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')
#views.py
#ファイルのアップロード
from django.core.files.storage import FileSystemStorage
def upload_sample(request):
#もしPOSTで送信されrequest.FILESが存在する場合は
if request.method =='POST' and request.FILES['upload_file']:
upload_file = request.FILES['upload_file'] #upload_fileはinputのname属性
fs = FileSystemStorage() #ファイルを保存する インスタンス化
file_path = os.path.join('upload',upload_file.name) #upload_file.nameはアップロードされたファイルの名前を取得している
#uploadフォルダの下という意味
file = fs.save(file_path,upload_file) #ファイルのパスとファイル
uploaded_file_url = fs.url(file) #保存されたファイルのURLを取得
return render(request,'formapp/upload_file.html',context={
'uploaded_file_url':uploaded_file_url #テンプレートでupload先を表示したいのでcontextで渡してあげる
})
#POST以外の場合こちらを表示
return render(request,'formapp/upload_file.html')
#upload_file.html
<form method = 'POST' enctype='multipart/form-data'>
{% csrf_token %}
<input type='file' name = 'upload_file'><br>
<input type='submit' value = '保存'>
</form>
{%if uploaded_file_url %} {# ファイルが存在する場合に表示 #}
<p>保存先: <a href="{{uploaded_file_url}}">{{uploaded_file_url}}</a></p>
{%endif%}
アップロードした画像のmodelへの格納と表示
#プロジェクトのurls.py
#開発環境のみこちらが必要
from django.conf import settings
from django.conf.urls.static import static
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
#models.py
class User(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
picture = models.FileField(upload_to='picture/')
#upload_toでファイルの保存先を指定。mediaフォルダの下にpictureフォルダが作られてその中に保存される
#picture = models.FileField(upload_to='picture/%Y/%m/%d')とすると2023/08/01という階層のフォルダが作られ日毎にフォルダを区切ることができる
#forms.py モデルと紐付け
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = '__all__'
#views.py
def upload_model_form(request):
user = None
if request.mothod =='POST':
form = forms.UserForm(request.POST,request.FILES)
if form.is_valid():
user = form.save()
else:
form = forms.UserForm()
return render(request,'formapp/upload_model_form.html',context={
'form':form,'user':user
})
#upload_model_form.html
<body>
<form method = 'POST' enctype='multipart/form-data'>
{% csrf_token %}
{{form.as_p}}
<input type='submit' value = '保存'>
</form>
{%if user%}
<p>name :{{user.name}}</p>
<p>age : {{user.age}}</p>
<img src = "{{user.picture.url}}">{#pictureのURL先を指定して表示させる#}
{% endif %}
</body>
演習課題
演習課題疑問
forms.py
StudentsUpdateFormがなぜModelFormからFormを継承するように変更するのか?DBの内容をいじるからなんとなくModelFormっぽいのに?
views.py
update_studentにて引数にrequestとidをとるがこれをどうやって受け取るのかを注目して学びたい
→
テンプレートにて
a href="{%url 'app:update_student' id=student.id%}"としてidを渡してあげる。そして
urls.pyにてurl指定は→update_student/<int:id>こうする
htmlのaタグのid=student.idからidを受け取って
urls.pyのint:idにそのidがはいり
views.pyのrequest,idを引数として受け取りそれを使って
idに対応するデータを取得して〜というかんじ。
ここから先はコードのメモ
from django.db import models
class Students(models.Model):
name = models.CharField(max_length=50)
age = models.IntegerField()
grade = models.IntegerField()
picture = models.FileField(upload_to='picture/')
from django.shortcuts import render
from . import forms
from .models import Students
from django.core.files.storage import FileSystemStorage
import os
from django.shortcuts import redirect
from django.forms import modelformset_factory
def index(request):
return render(request,'index.html')
def form_insert_data(request):
student = None
message = ''
if request.method =='POST':
form = forms.StudentsForm(request.POST,request.FILES) #POSTとFILESを受け取る
if form.is_valid():
form.save()
message = '登録完了しました'
form = forms.StudentsForm() #フォームを初期化
else:
print('バリデーションが実行されていません')
else:
form = forms.StudentsForm()
print('ページが読み込まれました')
return render(request,'form_insert_data.html',context={
'form':form,'message':message
})
#生徒一覧画面
def student_list(request):
students = Students.objects.all()
return render(request,'student_list.html',context={
'students':students
})
#更新用
def update_student(request,id):
student = Students.objects.get(id=id)
form = forms.StudentsUpdateForm( #初期値を与えてあげる
initial={
'name':student.name,
'age':student.age,
'grade':student.grade,
'picture':student.picture
}
)
if request.method =='POST':
form = forms.StudentsUpdateForm(request.POST or None,request.FILES or None)
if form.is_valid():
student.name=form.cleaned_data['name']
student.age=form.cleaned_data['age']
student.grade=form.cleaned_data['grade']
#pictureの場合はurlを保存と中身の保存が必要なのでこちらだけ処理がちがう
picture=form.cleaned_data['picture']
if picture:
fs = FileSystemStorage()
file_name=fs.save(os.path.join('student',picture.name),picture)
student.picture=file_name
student.save()
return render(request,'update_student.html',context={
'form':form,'student':student
})
#削除用
def delete_student(request,id):
student = Students.objects.get(id=id)
if request.method =='POST':
Students.objects.get(id=id).delete()
return redirect('app:student_list') #削除したらリストページへリダイレクトする
return render(request,'delete_student.html',context={
'student':student
})
#一括挿入
def modelform_set_student(request):
TestFormset = modelformset_factory(Students,fields='__all__',extra=3)
#インスタンス化と送信された場合viewsで受けとって保存
formset = TestFormset(request.POST or None,request.FILES or None)#request.POSTの値がある場合はそちらを、ない場合はNoneをいれてただインスタンスを作成する
message=''
if formset.is_valid():
formset.save() #保存
message = '一括登録完了しました'
return render(request,'modelform_set_student.html',context={
'formset':formset,'message':message
})
from django.urls import path
from . import views
app_name = 'app'
urlpatterns=[
path('',views.index,name='index'),
path('form_insert_data/',views.form_insert_data,name='form_insert_data'),
path('student_list/',views.student_list,name='student_list'),
path('update_student/<int:id>',views.update_student,name='update_student'),
path('delete_student/<int:id>',views.delete_student,name='delete_student'),
path('modelform_set_student/',views.modelform_set_student,name='modelform_set_student'),
]
from .models import Students
from django import forms
class StudentsForm(forms.ModelForm):
name = forms.CharField(label='名前')
age = forms.IntegerField(label='年齢')
grade = forms.IntegerField(label='学年')
picture = forms.FileField(label='ファイルアップロード')
class Meta:
model = Students
fields = '__all__'
#更新用のフォーム
class StudentsUpdateForm(forms.Form): #? ここはなぜModelFormではない?
name = forms.CharField(label='名前')
age = forms.IntegerField(label='年齢')
grade = forms.IntegerField(label='学年')
picture = forms.FileField(label='ファイルアップロード',required=False)
#ファイルはセキュリティ上の理由でデフォルトでファイルをいれられない。
#すると更新する際にファイルがないと怒られてしまうので更新する時はいらないように
#required=Falseにしておく
<! DOCTYPE html>
<html>
<head>
<meta charset = 'utf-8'>
<title>{%block title%}DjangoForm{%endblock%}</title>
</head>
<body>
<header>
<a href="{%url 'app:form_insert_data'%}">データ挿入画面</a>
<a href="{%url 'app:student_list'%}">メンバー一覧画面</a>
<a href="{%url 'app:modelform_set_student'%}">メンバー一括挿入画面</a>
</header>
{%block main%}
{%endblock%}
</body>
</html>
{%extends 'base.html' %}
{%block title%}メンバー挿入画面{{block.super}}{%endblock%}
{%block main%}
<form method = 'POST' enctype='multipart/form-data'>
{% csrf_token %}
{{form.as_p}}
<input type = 'submit' value='メンバー追加'>
</form>
{%if message%}
<p>{{message}}</p>
{%endif%}
{%if student%}
<p>name :{{student.name}}</p>
<p>age : {{student.age}}</p>
<p>grade : {{student.grade}}</p>
<img src = "{{student.picture.url}}">{#pictureのURL先を指定して表示させる#}
{% endif %}
{%endblock%}
{%extends 'base.html' %}
{%block title%}メンバー一覧画面{{block.super}}{%endblock%}
{%block main%}
<h2>メンバー一覧画面</h2>
<table>
<tr>
<th>名前</th>
<th>年齢</th>
<th>学年</th>
</tr>
{% for student in students %}
<tr>
<td><a href="{%url 'app:update_student' id=student.id%}">{{student.name}}</a></td>
<td>{{student.age}}</td>
<td>{{student.grade}}</td>
</tr>
{% endfor %}
</table>
{%endblock%}
{%extends 'base.html' %}
{%block title%}メンバー更新画面{{block.super}}{%endblock%}
{%block main%}
<form method = 'POST' enctype='multipart/form-data'>
{% csrf_token %}
{{form.as_p}}
<input type = 'submit' value='メンバー更新'>
</form>
<a href = "{%url 'app:delete_student' id=student.id%}">メンバー削除</a>
<p><img src ="{{student.picture.url}}" style=width:500px;></p>{#pictureのURL先を指定して表示させる#}
{%endblock%}
{%extends 'base.html' %}
{%block title%}メンバー削除画面{{block.super}}{%endblock%}
{%block main%}
<form method = 'POST' enctype='multipart/form-data'>
{%csrf_token%}
{{student.name}}
{{student.age}}
{{student.grade}}
{{student.picture}}
<p>本当に削除しますか?</p>
<input type = 'submit' value='メンバー削除'>
</form>
<p><img src ="{{student.picture.url}}" style=width:500px;></p>{#pictureのURL先を指定して表示させる#}
{%endblock%}
{%extends 'base.html' %}
{%block title%}メンバー一括挿入画面{{block.super}}{%endblock%}
{%block main%}
<form method = 'POST' enctype='multipart/form-data'>
{% csrf_token %}
{{formset.as_p}}
<input type = 'submit' value='メンバー追加'>
</form>
{%if message%}
<p>{{message}}</p>
{%endif%}
{%if student%}
<p>name :{{student.name}}</p>
<p>age : {{student.age}}</p>
<p>grade : {{student.grade}}</p>
<img src = "{{student.picture.url}}">{#pictureのURL先を指定して表示させる#}
{% endif %}
{%endblock%}
リダイレクト処理
from django.shortcuts import redirect
def delete_student(request):
#(略)
return redirect('app:student_list') #削除したらリストページへリダイレクトする
#return redirect('app:sample',id=1) #sampleの関数にidを渡してリダイレクトもできる