This article describes how to solve a simple but interesting task in a short time using the Django framework: creating a bug tracking system (ticket system) for your project. Our system will be integrated with the Django user authentication system and administrative interface. Despite its primitiveness, such an application, with some improvement, may well be useful for a single developer or a small group.

I will describe actions in the linux environment using Django version 1.6, so you need to keep in mind that for other operating systems and framework versions, something may work differently (but without significant changes). The level of the article is designed for beginners, but I will not focus on preparing the working environment and chew on quite basic things, and if you don’t understand what to do at first, I recommend reading
this excellent article and going through the
Django tutorial .
So, what tasks should our application perform:
- Editing bug reports (available for administrators through the standard Django admin panel)
- View bug reports in a list or separately (available to all visitors)
- The ability to register as a new user
- Accordingly, login and logout via the web interface
- Add bug reports via web interface (available only for logged in users)
')
In order to comply with the concept of minimalism, a bug report (or ticket) will contain the title (title), description (description), date and time of creation (created), author (author), and the status opened / closed (closed).
The process of working on the application will be divided into several stages, and in order to simplify moving from one stage to another, I created a
repository on GitHub with all the source codes and commits at each stage. Of course, you can just copy-paste the code from the article, but I suggest to do otherwise:
- Prepare your work environment, install django
- If you do not have an account on GitHub - create it
- Fork your repository
- In the working folder (in the shell) execute the git clone <url command of your repository>
- Further, at the beginning of each stage, I will specify the git checkout -f <part - #> command, which must be executed in order for the code in the working folder to be synchronized with the corresponding repository commit.
You will receive the complete code of the finished project in your work environment. Can run
cd django-tutorial-bugreport ./manage.py runserver
and in the browser, go to
127.0.0.1:8000/bugs to “touch” the final result.
Stage # 0 - creating an application
The beauty of git is that you can easily roll back to any stage of project development, and for your convenience, I marked these steps with tags. So, we execute the command:
git checkout -f part-0
- thereby dropping the project to the initial state, what would it be if you just executed the django-admin.py startproject command ...
Go to the project folder and create the bugtracker application.
./manage.py startapp bugtracker
We bring our application to project / settings.py (by adding
'bugtracker'
in the tuple
INSTALLED_APPS
)
Immediately configure directories for templates and statics:
TEMPLATE_DIRS = (os.path.join(BASE_DIR, 'templates/'),)
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'static/'),)
Add this urlpattern to project / urls.py -
url(r'^bugs/', include('bugtracker.urls'))
, after which all the other urls we need will be described in bugtracker \ urls.py, which we create it like this, while almost empty, otherwise django will give an error:
Then we create a model for the bug report in the bugtracker / models.py file:
Let's take a closer look:
title is the name of the ticket, for example
, Pepelats does not take off ,
text - description and steps to play the bug,
created - time and date of creation of the ticket will be automatically filled due to
auto_now=True
,
closed - the simplest version of the description of the status of it is closed or open, by default (during creation)
closed = False
, that is, the ticket is not closed,
user is the user who reported the bug. The model returns the
title as a unicode view.
Let's edit bugtracker / admin.py to make it possible to manage tickets from admin panel:
And run
./manage.py syncdb
As a result of this, the necessary tables will be created in the database, and a login, e-mail and password will be requested to create an administrator user.
Attention, further in the text, with the git checkout command from the git-repository db.sqlite3, and the administrator has a login: admin, password: 123 (one-two-three)
Having created it, you can run:
./manage.py runserver
And go to the address
http://127.0.0.1:8000/admin , and after entering the login and password,
create, edit and delete tickets.Stage # 1 - ticket list
We carry out:
git checkout -f part-1
- by this command, the db.sqlite3 file will appear containing the same database, in which some tickets have already been created for testing, and the administrator’s login: admin, password: 123. Looking at the contents of the tickets, you will see that I used not enough functionality of our application in as a todo list for him. The view of course in the administrative interface is ugly, and we will try to fix it. Add a new class to bugtracker / admin.py, which defines the ticket list interface in the admin panel and register it:
class TicketAdmin(admin.ModelAdmin): list_display = ('closed', 'title', 'text', 'created', 'user') list_filter = ['created', 'closed'] search_fields = ['title', 'text'] admin.site.register(Ticket, TicketAdmin)
After that, the admin panel will be much more convenient. You can mark the item "Edit bug reports" as completed.
The next thing we will do is add the ability to view the list of tickets without entering the administrative interface. To do this, we need to create a basic html template and a template for the actual ticket list. The base template will be in the templates / base.html file:
<!doctype html> <html> <head> <meta charset="utf-8"> <title>Bugtracker - {% block title_block %}{% endblock %}</title> </head> <body> {% block content_block %} {% endblock %} </body> </html>
As you can see, it is very simple and contains two blocks for the title and content.
Create a list template in the templates / list.html file
{% extends 'base.html' %} {% block title_block %}Main{% endblock %} {% block content_block %} <div> <h1>Bug list</h1> <table> <thead> <tr> <td>Title</td> <td>Created</td> <td>Author</td> <td>Status</td> </tr> </thead> <tbody> {% for ticket in object_list %} <tr> <td>{{ ticket.title }}</td> <td>{{ ticket.created|date }}</td> <td>{{ ticket.user }}</td> <td>{{ ticket.closed|yesno:"CLOSED, OPENED" }}</td> </tr> {% empty %} <tr> <td colspan=4>No tickets yet.</td> </tr> {% endfor %} </tbody> </table> </div> {% endblock %}
All this will be substituted into the basic template instead.
{% block content_block %} {% endblock %}
. The content is a table of four columns, which will be filled with ticket data, and if they are missing (empty), the table will contain the inscription
No tickets yet.
Now we will write a class-based view for this function in the bugtracker / views.py file, it is very simple:
from django.views.generic import ListView from .models import Ticket class BugListView(ListView): model = Ticket template_name = 'list.html'
Here the model is defined for display in the form of a list, and the template for display.
It remains only to create a new pattern for the URL "/ bugs /" in the bugtracker / urls.py file: adding
from .views import BugListView
and
url(r'^$', BugListView.as_view(), name='index'),
(as we remember, in the project / urls.py file, which is common to the entire project, we determined that the patterns for
"/bugs"
are in
bugtracker/urls.py
, respectively, the
bugtracker/urls.py
pattern for the URL
"/bugs/"
will look like this :
r'^$'
.
Go to
http://127.0.0.1:8000/bugs/ and see a scary table with a list of all our tickets. Since we will fasten the design later, we can safely put the status “Closed” for this stage in the admin panel.
Stage # 2 - ticket details
We carry out:
git checkout -f part-2
Add the ability to view each tag separately, using the DetailView, we act according to the old scheme.
Create templates templates / detail.html:
{% extends 'base.html' %} {% block title_block %}{{ object.title }}{% endblock %} {% block content_block %} <div> <h1>{{ object.closed|yesno:"CLOSED, OPENED" }} - {{ object.title }}</h1> <p>{{ object.user }} - {{ object.created|date }}</p> <p>{{ object.text }}</p> </div> {% endblock %}
Create a view in bugtracker / views.py:
from django.views.generic import DetailView class BugDetailView(DetailView): model = Ticket template_name = 'detail.html'
and in bugtracker / urls.py:
from .views import BugDetailView
add to urlpattens
url(r'^(?P<pk>[0-9]+)/$', BugDetailView.as_view(), name='detail'),
In addition, we add to the templates / list.html template a link to such a URL for each ticket, for example, you can do this:
<td><a href="{% url 'detail' ticket.pk %}">{{ ticket.title }}</a></td>
thanks to the url 'detail', our urls will be generated automatically, and even if the structure of the urls changes, the pattern does not "break"
We check by going to
http://127.0.0.1:8000/bugs/ and then clicking on any of the links. If everything works, we note in the admin that the ticket is closed.
Stage â„–3 - user registration
git checkout -f part-3
It is more correct, of course, to work with users in a separate application and use, for example,
django-registration for convenient work with activation, changing and restoring passwords, etc., but since we are learning, we will not use batteries in order not to multiply essence beyond necessary.
When registering users, we will use the built-in django functions for working with forms, for this we will create the templates templates / register.html:
{% extends 'base.html' %} {% block title_block %}Registration{% endblock %} {% block content_block %} {% if user.is_authenticated %} <a href="{% url 'index' %}">Back to main page</a> {% else %} <form action="{% url 'register' %}" method="post"> {% csrf_token %} {{ form.as_p }} <input type="submit" value="Submit" /> </form> {% endif %} {% endblock %}
In case the user is not logged in, then we display the form, which is transmitted to the template from the following view (bugtracker / views.py):
from django.views.generic import CreateView from django.core.urlresolvers import reverse_lazy from django.contrib.auth.forms import UserCreationForm class RegisterView(CreateView): form_class = UserCreationForm template_name = 'register.html' success_url = reverse_lazy('index')
success_url is where the user will be sent if the registration is successful, the url will be cured from urlpatterns using reverse_lazy ('index'), that is, in our case "/ bugs /"
Let's add the basic template with a link to the registration form, which will be displayed only if the user is not logged in:
{% if user.is_authenticated %} <div>Welcome, {{ user.username }}!</div> {% else %} <div><a href="{% url 'register' %}">Register</a></div> {% endif %}
Do not forget to add the RegisterView import in bugtracker / urls.py and in urlpattern -
url(r'^register/$', RegisterView.as_view(), name='register'),
Now you can check the performance and proceed to the next step.
Stage â„–4 - login and logout
git checkout -f part-4
Everything is simple here, we will use the built-in jung shortcuts. There is no need for a logout template, since there is nothing to display in this function, we will simply be transferred to the main page. Therefore, we create a template for the login only (templates / login.html):
{% extends 'base.html' %} {% block title_block %}Login{% endblock %} {% block content_block %} <form action="{% url 'login' %}" method="post"> {% csrf_token %} {% if form.non_field_errors %} <p> {% for error in form.non_field_errors %} {{ error }} {% endfor %} </p> {% endif %} {% for field in form %} <div> {{ field.label_tag }} {{ field }} {% if field.errors %} <p> {% for error in field.errors %} {{ error }} {% endfor %} </p> {% endif %} </div> {% endfor %} <input type="submit" value="Submit" /> </form> {% endblock %}
In bugtracker / urls.py we write the following:
from django.core.urlresolvers import reverse_lazy
and
url(r'^login/$', 'django.contrib.auth.views.login', {"template_name" : "login.html"}, name="login"), url(r'^logout/$', 'django.contrib.auth.views.logout', {"next_page" : reverse_lazy('login')}, name="logout"),
And templates / base.html redo it like this:
{% if user.is_authenticated %} <div>Welcome, {{ user.username }} | <a href="{% url 'logout' %}">Logout</a></div> {% else %} <div><a href="{% url 'register' %}">Register | <a href="{% url 'login' %}">Login</a></div> {% endif %}
What after login us redirects on the main page in settings.py we will register
from django.core.urlresolvers import reverse_lazy LOGIN_REDIRECT_URL = reverse_lazy('index')
Let's play enough, creating new users, logging in and logging out under them.
Stage 5 - adding a new ticket
git checkout -f part-5
The templates / add.html template is very similar to the one we created for RegisterView, only the other way around - the functionality is available to an authorized user:
{% extends 'base.html' %} {% block title_block %}Add ticket{% endblock %} {% block content_block %} {% if user.is_authenticated %} <a href="{% url 'index' %}">Back to main page</a> <form action="{% url 'add' %}" method="post"> {% csrf_token %} {{ form.as_p }} <input type="submit" value="Submit" /> </form> {% else %} <p>You should be logged in to add tickets!</p> {% endif %} {% endblock %}
Add somewhere to the base template a link to add:
<div><a href="{% url 'add' %}">Add ticket</a></div>
View will be such
class BugCreateView(CreateView): model = Ticket template_name = 'add.html' fields = ['title', 'text'] success_url = reverse_lazy('index') def form_valid(self, form): form.instance.user = self.request.user return super(BugCreateView, self).form_valid(form)
That is, only the title and text fields will be visible to the user, and due to the redefinition of the form_valid method, the current user index will be substituted in the user field.
Register the pattern for url 'add /' in bugtracker / urls.py, remembering to import BugCreateView:
url(r'^add/$', BugCreateView.as_view(), name='add'),
Let's try to add new tickets, and proceed to the next stage.
Stage number 6 - make elegant
git checkout -f part-6
All this works more or less satisfactorily, but it looks scary. In my opinion, the easiest and most convenient way to improve our interface, accessible even to a layman (like me), is to use twitter bootstrap. To do this, download the appropriate library and unpack it in the static folder. After which we proceed to the layout of templates. Bootstrap has a lot of examples, so there should not be any particular difficulties. Since it does not make sense to bring here the sheets of the resulting HTML, and the nuances of the layout for bootstrap are beyond the scope of this article (and my knowledge), just look at the sources in the
repository , or run
git checkout -f final
to get the final version of the code.
It is worth noting that the third version of bootstrap requires you to add class = "form-control" to inputs in forms. The easiest method I know of, to do this in django templates (without using extra batteries) is a custom filter, which is described
here . I would be grateful if more sophisticated readers of my article will share their experiences in the comments.
Total
In just a couple of hours, or even less, we have created quite a functional application that, with further improvement, tuning and development, can serve well, in some other projects, and maybe as a standalone service. Here are some of my very realizable ideas for improving it:
- HTTP API (immediately there is an opportunity to interact with almost anything, such as programs, console scripts, other web services, etc.
- More status for tickets
- Pagination in a list output pattern
- Opportunity to charge the execution of a ticket to another user and control the status
- Upload files and images
- Easy setup and customization of additional ticket fields
- Plugin system
Waiting for your comments, ideas and suggestions in the comments. Thank!
UPD: While I was preparing the article, django 1.7 was released, which introduced migration support, which was previously done with the help of south. The syncdb command is now deprecated, so after changes in models we do:
./manage.py makemigrations <application_name>
./manage.py migrate
To view the SQL query of the corresponding migration, execute
./manage.py sqlmigrate <application_name> <immigration number>