summaryrefslogtreecommitdiff
path: root/docs/topics/class-based-views/generic-editing.txt
blob: c1e73fc2fb27ea5a659e09ba0c178d4481458c4e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
====================================
Form handling with class-based views
====================================

Form processing generally has 3 paths:

* Initial GET (blank or prepopulated form)
* POST with invalid data (typically redisplay form with errors)
* POST with valid data (process the data and typically redirect)

Implementing this yourself often results in a lot of repeated boilerplate code
(see :ref:`Using a form in a view<using-a-form-in-a-view>`). To help avoid
this, Django provides a collection of generic class-based views for form
processing.

Basic forms
===========

Given a contact form:

.. code-block:: python
    :caption: forms.py

    from django import forms

    class ContactForm(forms.Form):
        name = forms.CharField()
        message = forms.CharField(widget=forms.Textarea)

        def send_email(self):
            # send email using the self.cleaned_data dictionary
            pass

The view can be constructed using a ``FormView``:

.. code-block:: python
    :caption: views.py

    from myapp.forms import ContactForm
    from django.views.generic.edit import FormView

    class ContactView(FormView):
        template_name = 'contact.html'
        form_class = ContactForm
        success_url = '/thanks/'

        def form_valid(self, form):
            # This method is called when valid form data has been POSTed.
            # It should return an HttpResponse.
            form.send_email()
            return super().form_valid(form)

Notes:

* FormView inherits
  :class:`~django.views.generic.base.TemplateResponseMixin` so
  :attr:`~django.views.generic.base.TemplateResponseMixin.template_name`
  can be used here.
* The default implementation for
  :meth:`~django.views.generic.edit.FormMixin.form_valid` simply
  redirects to the :attr:`~django.views.generic.edit.FormMixin.success_url`.

Model forms
===========

Generic views really shine when working with models.  These generic
views will automatically create a :class:`~django.forms.ModelForm`, so long as
they can work out which model class to use:

* If the :attr:`~django.views.generic.edit.ModelFormMixin.model` attribute is
  given, that model class will be used.
* If :meth:`~django.views.generic.detail.SingleObjectMixin.get_object()`
  returns an object, the class of that object will be used.
* If a :attr:`~django.views.generic.detail.SingleObjectMixin.queryset` is
  given, the model for that queryset will be used.

Model form views provide a
:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` implementation
that saves the model automatically.  You can override this if you have any
special requirements; see below for examples.

You don't even need to provide a ``success_url`` for
:class:`~django.views.generic.edit.CreateView` or
:class:`~django.views.generic.edit.UpdateView` - they will use
:meth:`~django.db.models.Model.get_absolute_url()` on the model object if available.

If you want to use a custom :class:`~django.forms.ModelForm` (for instance to
add extra validation), set
:attr:`~django.views.generic.edit.FormMixin.form_class` on your view.

.. note::
    When specifying a custom form class, you must still specify the model,
    even though the :attr:`~django.views.generic.edit.FormMixin.form_class` may
    be a :class:`~django.forms.ModelForm`.

First we need to add :meth:`~django.db.models.Model.get_absolute_url()` to our
``Author`` class:

.. code-block:: python
    :caption: models.py

    from django.db import models
    from django.urls import reverse

    class Author(models.Model):
        name = models.CharField(max_length=200)

        def get_absolute_url(self):
            return reverse('author-detail', kwargs={'pk': self.pk})

Then we can use :class:`CreateView` and friends to do the actual
work. Notice how we're just configuring the generic class-based views
here; we don't have to write any logic ourselves:

.. code-block:: python
    :caption: views.py

    from django.urls import reverse_lazy
    from django.views.generic.edit import CreateView, DeleteView, UpdateView
    from myapp.models import Author

    class AuthorCreate(CreateView):
        model = Author
        fields = ['name']

    class AuthorUpdate(UpdateView):
        model = Author
        fields = ['name']

    class AuthorDelete(DeleteView):
        model = Author
        success_url = reverse_lazy('author-list')

.. note::
    We have to use :func:`~django.urls.reverse_lazy` instead of
    ``reverse()``, as the urls are not loaded when the file is imported.

The ``fields`` attribute works the same way as the ``fields`` attribute on the
inner ``Meta`` class on :class:`~django.forms.ModelForm`. Unless you define the
form class in another way, the attribute is required and the view will raise
an :exc:`~django.core.exceptions.ImproperlyConfigured` exception if it's not.

If you specify both the :attr:`~django.views.generic.edit.ModelFormMixin.fields`
and :attr:`~django.views.generic.edit.FormMixin.form_class` attributes, an
:exc:`~django.core.exceptions.ImproperlyConfigured` exception will be raised.

Finally, we hook these new views into the URLconf:

.. code-block:: python
    :caption: urls.py

    from django.urls import path
    from myapp.views import AuthorCreate, AuthorDelete, AuthorUpdate

    urlpatterns = [
        # ...
        path('author/add/', AuthorCreate.as_view(), name='author-add'),
        path('author/<int:pk>/', AuthorUpdate.as_view(), name='author-update'),
        path('author/<int:pk>/delete/', AuthorDelete.as_view(), name='author-delete'),
    ]

.. note::

    These views inherit
    :class:`~django.views.generic.detail.SingleObjectTemplateResponseMixin`
    which uses
    :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix`
    to construct the
    :attr:`~django.views.generic.base.TemplateResponseMixin.template_name`
    based on the model.

    In this example:

    * :class:`CreateView` and :class:`UpdateView` use ``myapp/author_form.html``
    * :class:`DeleteView` uses ``myapp/author_confirm_delete.html``

    If you wish to have separate templates for :class:`CreateView` and
    :class:`UpdateView`, you can set either
    :attr:`~django.views.generic.base.TemplateResponseMixin.template_name` or
    :attr:`~django.views.generic.detail.SingleObjectTemplateResponseMixin.template_name_suffix`
    on your view class.

Models and ``request.user``
===========================

To track the user that created an object using a :class:`CreateView`,
you can use a custom :class:`~django.forms.ModelForm` to do this. First, add
the foreign key relation to the model:

.. code-block:: python
    :caption: models.py

    from django.contrib.auth.models import User
    from django.db import models

    class Author(models.Model):
        name = models.CharField(max_length=200)
        created_by = models.ForeignKey(User, on_delete=models.CASCADE)

        # ...

In the view, ensure that you don't include ``created_by`` in the list of fields
to edit, and override
:meth:`~django.views.generic.edit.ModelFormMixin.form_valid()` to add the user:

.. code-block:: python
    :caption: views.py

    from django.contrib.auth.mixins import LoginRequiredMixin
    from django.views.generic.edit import CreateView
    from myapp.models import Author

    class AuthorCreate(LoginRequiredMixin, CreateView):
        model = Author
        fields = ['name']

        def form_valid(self, form):
            form.instance.created_by = self.request.user
            return super().form_valid(form)

:class:`~django.contrib.auth.mixins.LoginRequiredMixin` prevents users who
aren't logged in from accessing the form. If you omit that, you'll need to
handle unauthorized users in :meth:`~.ModelFormMixin.form_valid()`.

AJAX example
============

Here is an example showing how you might go about implementing a form that
works for AJAX requests as well as 'normal' form POSTs::

    from django.http import JsonResponse
    from django.views.generic.edit import CreateView
    from myapp.models import Author

    class AjaxableResponseMixin:
        """
        Mixin to add AJAX support to a form.
        Must be used with an object-based FormView (e.g. CreateView)
        """
        def form_invalid(self, form):
            response = super().form_invalid(form)
            if self.request.is_ajax():
                return JsonResponse(form.errors, status=400)
            else:
                return response

        def form_valid(self, form):
            # We make sure to call the parent's form_valid() method because
            # it might do some processing (in the case of CreateView, it will
            # call form.save() for example).
            response = super().form_valid(form)
            if self.request.is_ajax():
                data = {
                    'pk': self.object.pk,
                }
                return JsonResponse(data)
            else:
                return response

    class AuthorCreate(AjaxableResponseMixin, CreateView):
        model = Author
        fields = ['name']