回答ではありません。
質問者さんはview+テンプレートの使い方が分からないようなので、タイトルにあるリレーションとは関係ない部分に引っかかっているようです。なので今回の質問は考慮せず、現在タイトルにある「Django リレーション(一対多 → 一対多)について教えてください。」に対して回答します。
モデル抜粋
質問者さんのモデルは必要以上に複雑なので、単純化しました。
mysite/myapp/models.py
python
1 from django . db import models
2 from django . contrib . auth . models import AbstractUser
3 class CustomUser ( AbstractUser ) :
4 def __str__ ( self ) :
5 return f' { self . username } ( { self . first_name } { self . last_name } )'
6 class Corp ( models . Model ) :
7 user = models . ForeignKey ( CustomUser , on_delete = models . PROTECT )
8 name = models . CharField ( max_length = 50 )
9 def __str__ ( self ) :
10 return self . name
11 class Staff ( models . Model ) :
12 corp = models . ForeignKey ( Corp , on_delete = models . PROTECT )
13 name = models . CharField ( max_length = 10 )
14 def __str__ ( self ) :
15 return self . name
名前やメンバ自体は削除・変更していますが骨格自体は変えていません。
クエリセット
viewからmodelを参照する際は、Modelオブジェクトを元にQuerySetを作成して、データベースから値を取得します。詳細は以下に記載があります。
https://docs.djangoproject.com/en/4.1/ref/models/querysets/
例えば、CustomUserのusernameが'user1'であるユーザーのCustomUser-Corp-Staffリストの取得は以下のようになります。
mysite/queryset_check_script.py
python
1 from myapp . models import CustomUser , Corp , Staff
2 queryset_user1 = Staff . objects . filter ( corp__user__username = 'user1' ) . values_list ( 'corp__user__username' , 'corp__name' , 'name' )
3 print ( queryset_user1 )
4 print ( queryset_user1 . query )
表の中身は以下のとおりです(本回答ではsqlite3を使っています)。
sql
1 sqlite > select * from myapp_customuser ;
2 id password last_login is_superuser username first_name last_name email is_staff is_active date_joined
3 ---------- ---------- ---------- ------------ ---------- ---------- ---------- ---------- ---------- ---------- --------------------------
4 1 0 user1 0 1 2023 - 03 - 20 21 : 34 : 33.274055
5 2 0 user2 0 1 2023 - 03 - 20 21 : 34 : 33.274081
6 sqlite > select * from myapp_corp ;
7 id name user_id
8 ---------- ---------- ----------
9 1 corp1 1
10 2 corp2 2
11 3 corp3 1
12 sqlite > select * from myapp_staff ;
13 id name corp_id
14 ---------- ---------- ----------
15 1 staff1 1
16 2 staff2 2
17 3 staff3 3
18 4 staff4 2
19 sqlite >
この表のデータを先のスクリプトを使い、django shell から流すと以下のように出力されます。
bash
1 $ python manage.py shell < queryset_check_script.py
2 < QuerySet [ ( 'user1' , 'corp1' , 'staff1' ) , ( 'user1' , 'corp3' , 'staff3' ) ] >
3 SELECT "myapp_customuser" . "username" , "myapp_corp" . "name" , "myapp_staff" . "name" FROM "myapp_staff" INNER JOIN "myapp_corp" ON ( "myapp_staff" . "corp_id" = "myapp_corp" . "id" ) INNER JOIN "myapp_customuser" ON ( "myapp_corp" . "user_id" = "myapp_customuser" . "id" ) WHERE "myapp_customuser" . "username" = user1
4 $
環境構築スクリプト
python
1 import os , venv , subprocess
2 ENV = 'env'
3 if os . name == 'posix' :
4 BIN = 'bin'
5 else :
6 BIN = 'Scripts'
7 builder = venv . EnvBuilder ( with_pip = True )
8 builder . create ( ENV )
9 cwd = os . getcwd ( )
10 python = os . path . join ( cwd , ENV , BIN , 'python' )
11 os . environ [ 'VIRTUAL_ENV' ] = os . path . join ( cwd , ENV )
12 os . environ [ 'PATH' ] = os . path . join ( cwd , ENV , BIN ) + os . pathsep + os . environ [ 'PATH' ]
13 p = subprocess . run ( [ python , '-m' , 'pip' , 'install' , '--upgrade' , 'pip' , 'setuptools' ] )
14 p = subprocess . run ( [ python , '-m' , 'pip' , 'install' , 'django==4.1.7' , 'whatthepatch==1.0.4' ] )
15 with open ( 'generated.py' , 'wt' , encoding = 'utf8' ) as f :
16 f . write ( """\
17 import subprocess, os
18 p = subprocess.run('django-admin startproject mysite', shell=True)
19 os.chdir('mysite')
20 p = subprocess.run('python manage.py startapp myapp', shell=True)
21 p = subprocess.run('python ../patch.py ../patch.diff', shell=True)
22 p = subprocess.run('python manage.py makemigrations', shell=True)
23 p = subprocess.run('python manage.py migrate', shell=True)
24 p = subprocess.run('python manage.py shell <insert_script.py', shell=True)
25 p = subprocess.run('python manage.py shell <queryset_check_script.py', shell=True)
26 """ )
27 with open ( 'patch.diff' , 'wt' , encoding = 'utf8' ) as f :
28 f . write ( """\
29 diff --git a/insert_script.py b/insert_script.py
30 new file mode 100644
31 index 0000000..c47446e
32 --- /dev/null
33 +++ b/insert_script.py
34 @@ -0,0 +1,7 @@
35 +from myapp.models import CustomUser, Corp, Staff
36 +users = [CustomUser(username=f'user{i+1}') for i in range(2)]
37 +corps = [Corp(name='corp1', user=users[0]), Corp(name='corp2', user=users[1]), Corp(name='corp3', user=users[0])]
38 +staffs = [Staff(name='staff1', corp=corps[0]), Staff(name='staff2', corp=corps[1]), Staff(name='staff3', corp=corps[2]), Staff(name='staff4', corp=corps[1])]
39 +for l in [users, corps, staffs]:
40 + for o in l:
41 + o.save()
42 diff --git a/myapp/models.py b/myapp/models.py
43 index 71a8362..aa03eab 100644
44 --- a/myapp/models.py
45 +++ b/myapp/models.py
46 @@ -1,3 +1,16 @@
47 from django.db import models
48 +from django.contrib.auth.models import AbstractUser
49 +class CustomUser(AbstractUser):
50 + def __str__(self):
51 + return f'{self.username}({self.first_name} {self.last_name})'
52 +class Corp(models.Model):
53 + user = models.ForeignKey(CustomUser, on_delete=models.PROTECT)
54 + name = models.CharField(max_length=50)
55 + def __str__(self):
56 + return self.name
57 +class Staff(models.Model):
58 + corp = models.ForeignKey(Corp, on_delete=models.PROTECT)
59 + name = models.CharField(max_length=10)
60 + def __str__(self):
61 + return self.name
62
63 -# Create your models here.
64 diff --git a/myapp/urls.py b/myapp/urls.py
65 new file mode 100644
66 index 0000000..4c9189f
67 --- /dev/null
68 +++ b/myapp/urls.py
69 @@ -0,0 +1,8 @@
70 +from django.urls import path
71 +
72 +from . import views
73 +
74 +urlpatterns = [
75 + path('', views.index, name='index'),
76 +]
77 +
78 diff --git a/myapp/views.py b/myapp/views.py
79 index 91ea44a..59a44a4 100644
80 --- a/myapp/views.py
81 +++ b/myapp/views.py
82 @@ -1,3 +1,4 @@
83 from django.shortcuts import render
84 -
85 -# Create your views here.
86 +from .models import CustomUser, Corp, CustomUser
87 +def index():
88 + pass
89 diff --git a/mysite/settings.py b/mysite/settings.py
90 index 19d4403..80db486 100644
91 --- a/mysite/settings.py
92 +++ b/mysite/settings.py
93 @@ -31,6 +31,7 @@ ALLOWED_HOSTS = []
94 # Application definition
95
96 INSTALLED_APPS = [
97 + 'myapp.apps.MyappConfig',
98 'django.contrib.admin',
99 'django.contrib.auth',
100 'django.contrib.contenttypes',
101 @@ -103,9 +104,9 @@ AUTH_PASSWORD_VALIDATORS = [
102 # Internationalization
103 # https://docs.djangoproject.com/en/4.1/topics/i18n/
104
105 -LANGUAGE_CODE = 'en-us'
106 +LANGUAGE_CODE = 'ja'
107
108 -TIME_ZONE = 'UTC'
109 +TIME_ZONE = 'Asia/Tokyo'
110
111 USE_I18N = True
112
113 @@ -121,3 +122,5 @@ STATIC_URL = 'static/'
114 # https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field
115
116 DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
117 +
118 +AUTH_USER_MODEL = "myapp.CustomUser"
119 diff --git a/mysite/urls.py b/mysite/urls.py
120 index 8bc0cd4..7229155 100644
121 --- a/mysite/urls.py
122 +++ b/mysite/urls.py
123 @@ -14,8 +14,9 @@ Including another URLconf
124 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
125 \"\"\"
126 from django.contrib import admin
127 -from django.urls import path
128 +from django.urls import path, include
129
130 urlpatterns = [
131 + path('myapp/', include('myapp.urls')),
132 path('admin/', admin.site.urls),
133 ]
134 diff --git a/queryset_check_script.py b/queryset_check_script.py
135 new file mode 100644
136 index 0000000..25edd00
137 --- /dev/null
138 +++ b/queryset_check_script.py
139 @@ -0,0 +1,4 @@
140 +from myapp.models import CustomUser, Corp, Staff
141 +queryset_user1 = Staff.objects.filter(corp__user__username='user1').values_list('corp__user__username', 'corp__name', 'name')
142 +print(queryset_user1)
143 +print(queryset_user1.query)
144 """ )
145 with open ( 'patch.py' , 'wt' , encoding = 'utf8' ) as f :
146 f . write ( """\
147 import whatthepatch, os, sys
148 with open(sys.argv[1], 'rt', encoding='utf8') as fp:
149 for diff in whatthepatch.parse_patch(fp):
150 header = diff.header
151 if header.new_path == '/dev/null':
152 os.remove(header.old_path)
153 else:
154 if header.old_path == '/dev/null':
155 old = []
156 else:
157 with open(header.old_path, 'rt', encoding='utf8') as fo:
158 old = [line.rstrip(os.linesep) for line in fo]
159 new = whatthepatch.apply_diff(diff, old)
160 try:
161 dir_path = os.path.dirname(header.new_path)
162 if dir_path == '':
163 dir_path = '.'
164 os.makedirs(dir_path)
165 except FileExistsError:
166 pass
167 with open(header.new_path, 'wt', encoding='utf8') as fn:
168 for line in new:
169 print(line, file=fn)
170 """ )
171 p = subprocess . run ( [ python , 'generated.py' ] )
使い方は空のディレクトリで上記スクリプトを流すだけです。
中身はおよそ以下のとおりになっています。
venvを使いカレントディレクトリにenvという名前で仮想環境を構築
仮想環境の中に入る
generated.py, patch.diff, patch.pyをカレントに出力
generated.pyを実行してpipを更新してdjangoとパッチ用のツールをインストール
djangoのプロジェクト(mysite)を作成し、その中にアプリ(myapp)を作成
patch.pyとpatch.diffを使ってアプリ用の実装を生成
dbをmigrate(初回)
django shellとmysite/insert_script.pyを使用し、サンプルデータを作成
django shellとmysite/queryset_check_script.pyを使用し、クエリを発行
仮想環境から出る