Creating forms to Add an/or Edit

Tue 21 July 2015

Filed under Django

CBV or FBV?

Class Based Views or Function Based Views? Both can accomplish the pretty much the same things, so what is the difference?

Instead of going into the pros and cons of each, I'll provide a few links on the bottom that other bloggers have so eloquently written. I'll go straight to the code.

Here's the requirement:
1. Create new students 2. Retrieve existing students and display as a list 3. Update (or edit) existing students 4. Delete students

This is so common, some developers call it CRUD - Create, Retreive, Update, Delete.

Function Based forms

First, the model on which all this is based:

#models.py
from django.db import models

GROUPS_LIST = (
        ('A', 'Group A'),
        ('B', 'Group B'),
        ('C', 'Group C'),
        ('D', 'Group D')
        )

GENDER_CHOICES = (
        ('M','Male'),
        ('F','Female'),
        )

class Student(models.Model):
    firstname = models.CharField(max_length=30)
    lastname = models.CharField(max_length=30)
    gender = models.CharField(max_length=1,
                             choices=GENDER_CHOICES,
                             blank=False)
    group = models.CharField(max_length=1,
                             choices=GROUPS_LIST)

    def __unicode__(self):
        return u'{}, {}'.format(self.lastname, self.firstname)

Quick explanation This assumes you have some basic background in django models.

For both `gender` and `group`, we are providing predefined choices.  These choices will be used in several places throughout the app.

Starting first with the Retrieve

the views.py

#views.py
from django.shortcuts import render

from .models import Student

def student_retrieve(request):
    students = Student.objects.all()

    template_name = 'students/student_list.html'
    context = {'students': students}
    return render(request, template_name, context)

and the html

#students/student_list.html

<h1>Students</h1>

    <table>
      <col width="300">

      {% for student in students %}
        <tr>
          <td><b>{{student.lastname}}, {{ student.firstname }}</b> </td>
        </tr>
      {% endfor %}
    </table>

<hr>
<a href="{% url "student_new" %}"><button>Add New Student</button></a>

and the urls.py

#urls.py

from django.conf.urls import url

urlpatterns = [
  url(r'^$', 'students.views.student_retrieve', name='student_list'),]

Quick explanation - In the html file, I used a table, because we will be adding more elements and a table will keep it looking just a tad bit neater.
- In the last line of the html <a href="{% url "student_new" %}"><button>Add New Student</button></a>, I created a button - with the text 'Add New Student'. This points to the url that is named student_new and will open the related view that is listed in the urlpatterns students.views.student_retrieve. (see it in the urls.py) - I really could have put the real url there. `'\'. But if I ever changed the url, i would have to find the url throughout my code, it is easier to use the name.

Next, the Delete

When deleting or updating, we need to know exactly which instance will be affected. We can pass that in to the view through the url. Notice the regex in the newly added url in url.py.

#urls.py

from django.conf.urls import url

urlpatterns = [
  url(r'^$', 'students.views.student_retrieve', name='student_list'),
  url(r'^delete/(?P<pk>\d+)$', 'students.views.student_delete', name='student_delete'),]

Quick explanation This url might look like www.example.com/delete/12. The (?P<pk>\d+)$ is what holds the primarykey of the instance or record. This means that the delete view will be called with a keyword argument pk=12, (or what ever number follows the delete). - The <pk> can be replaced with any other field, as applicable (name, date, etc.). - The d+ is regex for any number of digits. This can be replaced with any other applicable regex.

the views

#views.py
from django.shortcuts import render, get_object_or_404 <--- changed here
from django.http import  HttpResponseRedirect <--- add this
from django.core.urlresolvers import reverse <--- added this

def student_delete(request, pk):
    student = get_object_or_404(Student, pk=pk)
    if request.method == 'POST':
        student.delete()
        return HttpResponseRedirect(reverse('student_list'))

    template_name = 'students/student_confirm_delete.html'
    context = {'object': student}
    return render(request, template_name, context)

and the html

<form method="post">
    {% csrf_token %}
    Are you sure you want to delete: <b>{{ object }}</b>  ?
    <input type="submit" value="Submit" />
</form>

Quick explanation - def student_delete(request, pk): Here, the pk is passed to the method.

- `student = get_object_or_404(Student, pk=pk)`: We try to get the object from Student where the Student.pk is equal to the `pk` that was passed in. Or if it doesn't exist, display a 404 error page.
- the docs about `get_object_or_404`

- If we are coming to this page as a `POST`, then the student is deleted (`delete()`) is a built in method.
- And then we will redirect to the url/ view that is associated with `student_list`. See the urls.py.
- the docs about `HttpResponseRedirect`
- the docs about `revserse`

- If we are coming to this page as a `GET`, then it renders the template and context, which is `{{object}}`, the student's name as was defined in the  `__unicode__` in the model.

- When we click from another page to get to this page, or by typing directly into the url, it is a `GET`.  Then once we click the `submit` and are coming back to this page it is a `POST`.

Next the CREATE:

First, we will create the Create and Update separately and then show a way to combine them, if it works for the programming.

the forms.py

#forms.py

from django import forms

class StudentForm(forms.ModelForm):
    class Meta:
        model = Student
        fields = ['firstname', 'lastname', 'gender',
                  'avatar', 'group', ]
        widgets = {
            'firstname': forms.TextInput(
                attrs={'placeholder':'First Name', 'class':'form-control'}),
            'lastname': forms.TextInput(
                attrs={'placeholder':'First Name', 'class':'form-control'}),
            'gender': forms.RadioSelect(choices=GENDER_CHOICES),
            'group': forms.RadioSelect(choices=GROUPS_LIST)
        }

Quick explanation: - We create a new form, based on ModelForm. - we include the model, fields and widgets. There are other attributes that might be useful too. - the docs about ModelForm - the docs about widgets

the views

#views.py

def student_create(request):
    if request.method == 'POST':
        form = StudentForm(request.POST)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('student_list'))

    template_name = 'students/student_form.html'
    context = {'form': StudentForm()}
    return render(request, template_name, context)

Quick Explanation - We are declaring which form this view is based.
- When it is a GET, then the form is StudentForm(), essentially an empty form waiting for data. - When it is a POST, then the form is StudentForm(request.POST), which now has the data from the request.POST.

- When it is a `POST`, the form will try to validate all the fields on the form. If all `is_valid()`, then we can save and then will be redirected, as before.

the html

#students/student_form.html

<form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <input type="submit" value="Submit" />
</form>

Quick explanations - Here again, the first time we come to this view, it is a GET and it renders the template and context. Then when we click submit, and we come to this view again, then it is a POST and it attempts to save the data.

- In the html, `{{ form.as_p }}` takes all the fields on the form and displays them as html `<p>` - paragraphs. 
- the docs about `{{form.as_p}}

the urls.py

url(r'^new$', 'students.views.student_new_update', name ='student_new'),

Now the UPDATE

the form.py and html are the same. the view.py

#views.py

def student_update(request, pk):
    student = get_object_or_404(Student, pk=pk)
    if request.method =='POST':
        form = StudentForm(request.POST, instance=student)
        if form.is_valid():
            form.save()
            return HttpResponseRedirect(reverse('student_list'))

    template_name = 'students/student_form.html'
    context = {'form': StudentForm(instance=student)}
    return render(request, template_name, context)

the urls.py

url(r'^edit/(?P<pk>\d+)$', 'students.views.student_new_update', name='student_edit'),

Quick explanation - First thing, it tries to get the student instance, based on the pk. - If it is a GET, then the form is StudentForm(instance=student), the same StudentForm that was used in the Create. And this time, the form is populated with the instance. And the user can make changes. - If it is a POST, then the form is StudentForm(request.POST, instance=student). The form is populated with the instance and the data from request.POST. - If the data on the form is valid, it saves and redirects.

So in a sense, the CREATE and UPDATE are pretty similar. They both use the same form StudentForm and the same html students/student_form.html. So if possible, they could be combined.

def student_new_update(request, pk=None, student=None):
    if pk:
        student = get_object_or_404(Student, pk=pk)

    if request.method == 'POST':
        form = StudentForm(request.POST, instance=student)
        if form.is_valid():
             form.save()
             return HttpResponseRedirect(reverse('student_list'))
    else:
        form = StudentForm(instance=student)

    template_name = 'students/student_form.html'
    context = {'form': form}
    return render(request, template_name, context)

Quick explanation - def student_new_update(request, pk=None, student=None): We set the defaults of pk and student to None. In this way, we can use the same code whether we have a pk or student or not.

- `if pk:
    student = get_object_or_404(Student, pk=pk)`
    if there is a `pk` being passed in (then it overrides the default), then it tries to find the student instance.

- if it is a not a `POST`, in other words a`GET`, then `form = StudentForm(instance=student)`. This could be a `GET` with a `pk` being passed in - in other words, an update and the instance is populated. Or a `GET` without a `pk` and then `student=None` so the `form = StudentForm(instance=student)` is just like `StudentForm()`. And then it renders the template and the context.

- if it is a `POST`, then the `request.POST` is passed in. If a `pk` is passed in, then the instance is populated based on the `pk`. Or else, it is a new/ create and the instace is the default `student=None`.)


NEXT: How to do this as CBV.


Comments


DeeKras.com © Dee Kras Powered by Pelican and Twitter Bootstrap. Icons by Font Awesome and Font Awesome More