0
# Forms Integration
1
2
Form-based GraphQL mutations with Django form validation, error handling, and automatic input type generation. Provides seamless integration between Django forms and GraphQL mutations with comprehensive validation and error reporting.
3
4
## Capabilities
5
6
### BaseDjangoFormMutation
7
8
Abstract base class for Django form-based mutations with customizable form handling and validation logic.
9
10
```python { .api }
11
class BaseDjangoFormMutation(graphene.relay.ClientIDMutation):
12
"""
13
Base class for Django form-based mutations.
14
15
Provides foundation for form-based mutations with form instantiation,
16
validation, and error handling. Designed to be subclassed for specific
17
form types and validation requirements.
18
"""
19
20
@classmethod
21
def mutate_and_get_payload(cls, root, info, **input):
22
"""
23
Main mutation logic with form processing.
24
25
Parameters:
26
- root: GraphQL root object
27
- info: GraphQL execution info
28
- **input: Mutation input arguments
29
30
Returns:
31
Mutation result with form data or errors
32
"""
33
34
@classmethod
35
def get_form(cls, root, info, **input):
36
"""
37
Get form instance for validation.
38
39
Parameters:
40
- root: GraphQL root object
41
- info: GraphQL execution info
42
- **input: Form input data
43
44
Returns:
45
django.forms.Form: Form instance
46
"""
47
48
@classmethod
49
def get_form_kwargs(cls, root, info, **input):
50
"""
51
Get form constructor arguments.
52
53
Parameters:
54
- root: GraphQL root object
55
- info: GraphQL execution info
56
- **input: Form input data
57
58
Returns:
59
dict: Form constructor keyword arguments
60
"""
61
62
@classmethod
63
def perform_mutate(cls, form, info):
64
"""
65
Perform mutation with validated form.
66
67
Parameters:
68
- form: Validated Django form
69
- info: GraphQL execution info
70
71
Returns:
72
Mutation result object
73
"""
74
```
75
76
### DjangoFormMutation
77
78
Concrete form mutation with automatic error handling and input type generation from Django forms.
79
80
```python { .api }
81
class DjangoFormMutation(BaseDjangoFormMutation):
82
"""
83
Concrete form mutation with error handling.
84
85
Automatically generates GraphQL input types from Django forms and
86
provides standardized error reporting through ErrorType objects.
87
"""
88
errors = graphene.List(ErrorType)
89
90
class Meta:
91
"""
92
Meta options for form mutations.
93
94
Attributes:
95
- form_class: Django form class to use
96
- only_fields: Fields to include in GraphQL input (list or tuple)
97
- exclude_fields: Fields to exclude from GraphQL input (list or tuple)
98
- return_field_name: Name for successful result field
99
"""
100
form_class = None
101
only_fields = None
102
exclude_fields = None
103
return_field_name = None
104
105
@classmethod
106
def __init_subclass_with_meta__(cls, form_class=None, only_fields=None,
107
exclude_fields=None, return_field_name=None,
108
**options):
109
"""
110
Configure form mutation subclass.
111
112
Parameters:
113
- form_class: Django form class
114
- only_fields: Fields to include
115
- exclude_fields: Fields to exclude
116
- return_field_name: Result field name
117
- **options: Additional mutation options
118
"""
119
120
@classmethod
121
def perform_mutate(cls, form, info):
122
"""
123
Perform mutation with validated form data.
124
125
Parameters:
126
- form: Validated Django form
127
- info: GraphQL execution info
128
129
Returns:
130
Mutation result with form data
131
"""
132
```
133
134
### DjangoModelFormMutation
135
136
Model form-based mutations with CRUD operations and automatic model instance handling.
137
138
```python { .api }
139
class DjangoModelFormMutation(BaseDjangoFormMutation):
140
"""
141
Model form-based mutations with CRUD operations.
142
143
Automatically handles model instance creation and updates through
144
Django ModelForm validation with comprehensive error handling.
145
"""
146
errors = graphene.List(ErrorType)
147
148
class Meta:
149
"""
150
Meta options for model form mutations.
151
152
Inherits from DjangoFormMutation.Meta with additional model-specific
153
options for instance handling and field mapping.
154
"""
155
form_class = None
156
model = None
157
only_fields = None
158
exclude_fields = None
159
return_field_name = None
160
161
@classmethod
162
def get_form_kwargs(cls, root, info, **input):
163
"""
164
Get model form constructor arguments with instance handling.
165
166
Parameters:
167
- root: GraphQL root object
168
- info: GraphQL execution info
169
- **input: Form input data
170
171
Returns:
172
dict: Form constructor arguments including model instance
173
"""
174
175
@classmethod
176
def perform_mutate(cls, form, info):
177
"""
178
Perform mutation with model form save.
179
180
Parameters:
181
- form: Validated Django model form
182
- info: GraphQL execution info
183
184
Returns:
185
Mutation result with saved model instance
186
"""
187
```
188
189
### Form Field Types
190
191
Global ID form fields for GraphQL integration with proper ID encoding and validation.
192
193
```python { .api }
194
class GlobalIDFormField(forms.CharField):
195
"""Django form field for global IDs with automatic validation."""
196
197
def to_python(self, value):
198
"""
199
Convert global ID to Python value.
200
201
Parameters:
202
- value: Global ID string
203
204
Returns:
205
Decoded ID value
206
"""
207
208
class GlobalIDMultipleChoiceField(forms.MultipleChoiceField):
209
"""Multiple choice field for global IDs with batch processing."""
210
211
def to_python(self, value):
212
"""
213
Convert multiple global IDs to Python values.
214
215
Parameters:
216
- value: List of global ID strings
217
218
Returns:
219
List of decoded ID values
220
"""
221
```
222
223
## Usage Examples
224
225
### Basic Form Mutation
226
227
```python
228
from django import forms
229
from graphene_django.forms.mutation import DjangoFormMutation
230
import graphene
231
232
class ContactForm(forms.Form):
233
name = forms.CharField(max_length=100)
234
email = forms.EmailField()
235
message = forms.CharField(widget=forms.Textarea)
236
237
def save(self):
238
# Custom save logic
239
data = self.cleaned_data
240
# Send email, save to database, etc.
241
return data
242
243
class ContactMutation(DjangoFormMutation):
244
class Meta:
245
form_class = ContactForm
246
247
@classmethod
248
def perform_mutate(cls, form, info):
249
result = form.save()
250
return cls(errors=[], **result)
251
252
class Mutation(graphene.ObjectType):
253
contact_form = ContactMutation.Field()
254
255
# GraphQL mutation:
256
# mutation {
257
# contactForm(input: {
258
# name: "John Doe"
259
# email: "john@example.com"
260
# message: "Hello world"
261
# }) {
262
# errors {
263
# field
264
# messages
265
# }
266
# name
267
268
# }
269
# }
270
```
271
272
### Model Form Mutation
273
274
```python
275
from django.db import models
276
from django import forms
277
from graphene_django.forms.mutation import DjangoModelFormMutation
278
279
class User(models.Model):
280
username = models.CharField(max_length=150, unique=True)
281
email = models.EmailField()
282
first_name = models.CharField(max_length=30)
283
last_name = models.CharField(max_length=30)
284
285
class UserForm(forms.ModelForm):
286
class Meta:
287
model = User
288
fields = ['username', 'email', 'first_name', 'last_name']
289
290
def clean_email(self):
291
email = self.cleaned_data['email']
292
if User.objects.filter(email=email).exists():
293
raise forms.ValidationError("Email already exists")
294
return email
295
296
class CreateUserMutation(DjangoModelFormMutation):
297
user = graphene.Field('myapp.schema.UserType')
298
299
class Meta:
300
form_class = UserForm
301
return_field_name = 'user'
302
303
class UpdateUserMutation(DjangoModelFormMutation):
304
user = graphene.Field('myapp.schema.UserType')
305
306
class Meta:
307
form_class = UserForm
308
return_field_name = 'user'
309
310
@classmethod
311
def get_form_kwargs(cls, root, info, **input):
312
kwargs = super().get_form_kwargs(root, info, **input)
313
# Get existing user for update
314
user_id = input.get('id')
315
if user_id:
316
kwargs['instance'] = User.objects.get(pk=user_id)
317
return kwargs
318
```
319
320
### Custom Form with File Upload
321
322
```python
323
class ProfileForm(forms.Form):
324
avatar = forms.ImageField()
325
bio = forms.CharField(widget=forms.Textarea, required=False)
326
327
def save(self, user):
328
cleaned_data = self.cleaned_data
329
user.profile.avatar = cleaned_data['avatar']
330
user.profile.bio = cleaned_data['bio']
331
user.profile.save()
332
return user.profile
333
334
class UpdateProfileMutation(DjangoFormMutation):
335
profile = graphene.Field('myapp.schema.ProfileType')
336
337
class Meta:
338
form_class = ProfileForm
339
340
@classmethod
341
def get_form_kwargs(cls, root, info, **input):
342
kwargs = super().get_form_kwargs(root, info, **input)
343
# Add files from request
344
if hasattr(info.context, 'FILES'):
345
kwargs['files'] = info.context.FILES
346
return kwargs
347
348
@classmethod
349
def perform_mutate(cls, form, info):
350
profile = form.save(info.context.user)
351
return cls(profile=profile, errors=[])
352
```
353
354
### Form with Custom Validation
355
356
```python
357
class PasswordChangeForm(forms.Form):
358
old_password = forms.CharField(widget=forms.PasswordInput)
359
new_password = forms.CharField(widget=forms.PasswordInput)
360
confirm_password = forms.CharField(widget=forms.PasswordInput)
361
362
def __init__(self, user, *args, **kwargs):
363
self.user = user
364
super().__init__(*args, **kwargs)
365
366
def clean_old_password(self):
367
old_password = self.cleaned_data['old_password']
368
if not self.user.check_password(old_password):
369
raise forms.ValidationError("Invalid current password")
370
return old_password
371
372
def clean(self):
373
cleaned_data = super().clean()
374
new_password = cleaned_data.get('new_password')
375
confirm_password = cleaned_data.get('confirm_password')
376
377
if new_password and confirm_password:
378
if new_password != confirm_password:
379
raise forms.ValidationError("Passwords don't match")
380
381
return cleaned_data
382
383
def save(self):
384
self.user.set_password(self.cleaned_data['new_password'])
385
self.user.save()
386
387
class ChangePasswordMutation(DjangoFormMutation):
388
success = graphene.Boolean()
389
390
class Meta:
391
form_class = PasswordChangeForm
392
exclude_fields = ('old_password',) # Don't expose in GraphQL
393
394
@classmethod
395
def get_form_kwargs(cls, root, info, **input):
396
kwargs = super().get_form_kwargs(root, info, **input)
397
kwargs['user'] = info.context.user
398
return kwargs
399
400
@classmethod
401
def perform_mutate(cls, form, info):
402
form.save()
403
return cls(success=True, errors=[])
404
```
405
406
### Form with Global ID Fields
407
408
```python
409
from graphene_django.forms.forms import GlobalIDFormField, GlobalIDMultipleChoiceField
410
411
class AssignTaskForm(forms.Form):
412
task_id = GlobalIDFormField()
413
assignee_ids = GlobalIDMultipleChoiceField()
414
due_date = forms.DateTimeField()
415
416
def save(self):
417
task = Task.objects.get(pk=self.cleaned_data['task_id'])
418
assignees = User.objects.filter(pk__in=self.cleaned_data['assignee_ids'])
419
420
task.assignees.set(assignees)
421
task.due_date = self.cleaned_data['due_date']
422
task.save()
423
return task
424
425
class AssignTaskMutation(DjangoFormMutation):
426
task = graphene.Field('myapp.schema.TaskType')
427
428
class Meta:
429
form_class = AssignTaskForm
430
431
@classmethod
432
def perform_mutate(cls, form, info):
433
task = form.save()
434
return cls(task=task, errors=[])
435
```
436
437
### Nested Form Mutation
438
439
```python
440
class AddressForm(forms.Form):
441
street = forms.CharField(max_length=200)
442
city = forms.CharField(max_length=100)
443
postal_code = forms.CharField(max_length=10)
444
445
class UserWithAddressForm(forms.Form):
446
username = forms.CharField(max_length=150)
447
email = forms.EmailField()
448
449
def __init__(self, *args, **kwargs):
450
super().__init__(*args, **kwargs)
451
# Extract address data
452
address_data = {}
453
for key in list(kwargs.get('data', {}).keys()):
454
if key.startswith('address_'):
455
address_key = key.replace('address_', '')
456
address_data[address_key] = kwargs['data'].pop(key)
457
458
self.address_form = AddressForm(data=address_data)
459
460
def is_valid(self):
461
return super().is_valid() and self.address_form.is_valid()
462
463
def save(self):
464
user = User.objects.create(
465
username=self.cleaned_data['username'],
466
email=self.cleaned_data['email']
467
)
468
469
if self.address_form.is_valid():
470
Address.objects.create(
471
user=user,
472
**self.address_form.cleaned_data
473
)
474
475
return user
476
```