Introduction
this post is an english version [of this post made on sam et max made last week.
The goal of that post will be to show how, without changing anything in a form, we can track the modifications of the data made in the application
the first part will set the scene by starting to show you how articulates an application with a form, additionally, composed by a sub form. I will explain when later.
To do so, I takes you into the world of the 7th art, come, we will redo StarWars!
One model, one form, a view, a template and that will be finished
the models.py
from django.db import models
class Movie(models.Model):
"""
Movie
"""
name = models.CharField(max_length=200, unique=True)
description = models.CharField(max_length=200)
def __str__(self):
return "%s" % self.name
class Episode(models.Model):
"""
Episode - for Trilogy and So on ;)
"""
name = models.CharField(max_length=200)
scenario = models.TextField()
movie = models.ForeignKey(Movie)
def __str__(self):
return "%s" % self.name
the forms.py, very mini mini
from django import forms
from django.forms.models import inlineformset_factory
from starwars.models import Movie, Episode
class MovieForm(forms.ModelForm):
class Meta:
"""
As I have to use : "exclude" or "fields"
As I'm very lazy, I dont want to fill the list in the "fields"
so I say that I just want to exclude ... nothing :P
"""
model = Movie
exclude = []
# a formeset based on the model of the Mother "Movie" and Child "Episode" + 1 new empty lines
EpisodeFormSet = inlineformset_factory(Movie, Episode, fields=('name', 'scenario'), extra=1)
the views.py very very very DRY :)
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.views.generic import CreateView, UpdateView, ListView
from starwars.models import Movie
from starwars.forms import MovieForm, EpisodeFormSet
class MovieMixin(object):
model = Movie
form_class = MovieForm
def get_context_data(self, **kw):
context = super(MovieMixin, self).get_context_data(**kw)
if self.request.POST:
context['episode_form'] = EpisodeFormSet(self.request.POST)
else:
context['episode_form'] = EpisodeFormSet(instance=self.object)
return context
def get_success_url(self):
return reverse("home")
def form_valid(self, form):
formset = EpisodeFormSet((self.request.POST or None), instance=self.object)
if formset.is_valid():
self.object = form.save()
formset.instance = self.object
formset.save()
return HttpResponseRedirect(reverse('home'))
class Movies(ListView):
model = Movie
context_object_name = "movies"
template_name = "base.html"
class MovieCreate(MovieMixin, CreateView):
"""
MovieMixin manage everything for me ...
"""
pass
class MovieUpdate(MovieMixin, UpdateView):
"""
... and I'm DRY :D
"""
pass
To finish to set the scene and the costumes : the templates
base.html
<!DOCTYPE html>
<html lang="fr">
<head>
<title>Manage stories for StarWars</title>
</head>
<body>
<h1>Stories Manager for Starwars</h1>
{% block content %}
<a href="{% url 'movie_create' %}">Add a movie</a><br/>
<h2>Movie list</h2>
<ul>
{% for movie in movies %}
<li><a href="{% url 'movie_edit' movie.id %}">{{ movie.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}
</body>
</html>
movie_form.htlm (the template used by UpdateView & CreateView)
{% extends "base.html" %}
{% block content %}
<form method="post" action="">
{% csrf_token %}
{{ formset.management_form }}
<table>
{{ form.as_table }}
</table>
<table>
{{ episode_form.as_table }}
</table>
<button>Save</button>
</form>
{% endblock %}
Update of the database
this is necessary :
(starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $ ./manage.py migrate
Operations to perform:
Synchronize unmigrated apps: messages, starwars, staticfiles
Apply all migrations: contenttypes, admin, sessions, auth
Synchronizing apps without migrations:
Creating tables...
Creating table starwars_movie
Creating table starwars_episode
Running deferred SQL...
Installing custom SQL...
Here we are, ready, I can now create my double Trilogy like Georges Lucas
Tracking the ungodly
But a day comes when me, George Lucas, I sell StarWars to Walt Disney, but I want to miss what they will do my "baby", I add a "tracker changes" in my application, not to lose the "field" of history.
Installation of Tracking Fields
as a prerequisites, if you want to also track who change thing, and not what data have been whanged, you will need
django-current-user, so the pip command to use will be this one
(starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $ pip install django-tracking-fields django-cuser
Collecting django-tracking-fields
Downloading django-tracking-fields-1.0.6.tar.gz (58kB)
100% |████████████████████████████████| 61kB 104kB/s
Collecting django-cuser
Downloading django-cuser-2014.9.28.tar.gz
Requirement already satisfied (use --upgrade to upgrade): Django>=1.5 in /home/foxmask/DjangoVirtualEnv/starwars/lib/python3.5/site-packages (from django-cuser)
Installing collected packages: django-tracking-fields, django-cuser
Running setup.py install for django-tracking-fields ... done
Running setup.py install for django-cuser ... done
Successfully installed django-cuser-2014.9.28 django-tracking-fields-1.0.6
the necessary changes in settings.py :
INSTALLED_APPS = (
...
'cuser',
'tracking_fields',
...
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'cuser.middleware.CuserMiddleware', ## <=== do not forget to catch the badass who make change on my movies;)
)
the little migrate that fit our needs, to add the tables for our modeles of django-tracking-fields
(starwars) foxmask@foxmask:~/DjangoVirtualEnv/starwars/starwars $ ./manage.py migrate
Operations to perform:
Synchronize unmigrated apps: staticfiles, messages, cuser, starwars
Apply all migrations: auth, sessions, contenttypes, tracking_fields, admin
Synchronizing apps without migrations:
Creating tables...
Running deferred SQL...
Installing custom SQL...
Running migrations:
Rendering model states... DONE
Applying tracking_fields.0001_initial... OK
Applying tracking_fields.0002_auto_20160203_1048... OK
and here we are ready to play with the trackers
Usage
We cant dream a more simpler way to do, this can be summarize to a decorator on the model which identifies which data are changed,
and on field ̀histo
which will link the model TrackingEvent of the application TrackingFields, to my tacle to watch.
And here, even if my model has been changed with this new field, it's unecessary to do a new "python manage.py migrate", nothing will happen,
because histo will be a GenericRelation().
Effectivly, TrackingEvent is based of ContenType aka
En effet, TrackingEvent repose sur ContenType aka "The contenttypes framework".
If you already played with the permission management, you should have already meet it before;)
To make it short, this will give :
models.py
from django.db import models
from django.contrib.contenttypes.fields import GenericRelation
from tracking_fields.decorators import track
from tracking_fields.models import TrackingEvent
@track('name', 'description')
class Movie(models.Model):
"""
Movie
"""
name = models.CharField(max_length=200, unique=True)
description = models.CharField(max_length=200)
histo = GenericRelation(TrackingEvent, content_type_field='object_content_type')
def episodes(self):
return Episode.objects.filter(movie=self)
def __str__(self):
return "%s" % self.name
@track('name', 'scenario')
class Episode(models.Model):
"""
Episode - for Trilogy and So on ;)
"""
name = models.CharField(max_length=200)
scenario = models.TextField()
movie = models.ForeignKey(Movie)
histo = GenericRelation(TrackingEvent, content_type_field='object_content_type')
def __str__(self):
return "%s" % self.name
so, here, it is very simple like a pancake recipe: 3 imports, the decorator, the GenericRelation, we mix all of them and that give what follow.
I have, in the meantime, added a function episodes
to my Movie class, I will explain it too later ;)
the template of the expected DetailView
<table>
<caption>History of the modification of {{ object }} </caption>
<thead>
<tr><th>Old Value</th><th>New Value</th><th>By</th><th>at</th></tr>
</thead>
<tbody>
{% for h in object.histo.all %}
{% for f in h.fields.all %}
<tr><td>{{ f.old_value }}</td><td>{{ f.new_value }}</td><td>{{ h.user }}</td><td>{{ h.date }}</td></tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
Now if I go the my page to modify the story of one episode, my template below, wont display thoses modifications ! But Why god ? Because until here, I just display the "histo" of Movie and not of the Episode. We now understand here my interest for the sub form. The issue
Let's fix it
this is here, that enter if the game, the function episodes
of my Movie class to permit to loop on it and display of the needed stuff
the template of the expected DetailView (again :)
<table>
<caption>History of the modifications of {{ object }} </caption>
<thead>
<tr><th>Old Value</th><th>New Value</th><th>By</th><th>at</th></tr>
</thead>
<tbody>
{% for h in object.histo.all %}
{% for f in h.fields.all %}
<tr><td>{{ f.old_value }}</td><td>{{ f.new_value }}</td><td>{{ h.user }}</td><td>{{ h.date }}</td></tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
{% for ep in object.episodes %}
{% if ep.histo.all %}
<table>
<caption>history of the modifications of Episode</caption>
<thead>
<tr><th>Old Value</th><th>New Value</th><th>By</th><th>at</th></tr>
</thead>
<tbody>
{% for h in ep.histo.all %}
{% for f in h.fields.all %}
{% if f.old_value == f.new_value %} {# they are the same when the new value is created to avoid to display "null" #}
{% else %}
<tr><td>{{ f.old_value }}</td><td>{{ f.new_value }}</td><td>{{ h.user }}</td><td>{{ h.date }}</td></tr>
{% endif %}
{% endfor %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endfor %}
And voilà ! Voili voilou ! As a bonus, if you're curious, of the admin side, you also have a list of all the changes if needed;)
For the advanced users who would say:
why have recoded the front side since this is already managed on the admin side without lifting a finger?
Because George Lucas wants to show changes to his baby StarWars by Walt Disney, to the world of course
Ho, and a last detail : in the admin, the view which displays the list of changes gives : "Episode Object" or "Movie Object". To avoid that, you shoud have notice that I have added the function str in my model which will return a more readable value of what have been changed
Conclusion :
IRL, I didnt see myself, create a History model linked physically by a FK on each model, I decide to search, arround the web, some ressources
It's finally on #django-fr@freenode that I asked my question and got from Gagaro the grââl : one application named tracking-fields, his author.
For once I do my lazy by not coding all by myself, it's nice to come across such an app !
If you want to play with the code of this Movie manager here is the tasty soup