Attention! If you're having trouble following the previous step, you can visit relevant lesson , download the archive of the previous step, install it, and start this lesson exactly where the previous one ended!
In this course, we will go through all the stages of creating a new project together:
As has been repeatedly mentioned, the format of this course is purely practical. But without defining the concepts of session, cookies, GET- and POST requests it will be very difficult to understand the essence what we will do next. Therefore, a few words about this, nevertheless, are worth saying.
How does the site "distinguish" "its" users from "strangers"?
Let's open the Storage → Cookies tab in the browser and try to register on a site. And we will see that at the time of registration, a new key session_id appears, which exists exactly as long as we save our authorization on this site. And it is this very session code that we can see in the django_session table if we open the database of this site.
However, if we log out, this session_id will disappear from both the database and browser cookies. The fact is that after the user authorizes on the site, the server creates a special digital label in its database, a copy of which it sends to the user's browser, and the browser stores this digital label in its memory. This is the session key, which is stored in the browser's memory. These data stored in the browser's memory are called cookies.
(Just in case, the SQLite browser mentioned in the video can be installed using these commands:
$ sudo add-apt-repository -y ppa:linuxgndu/sqlitebrowser
$ sudo apt-get update
$ sudo apt-get install sqlitebrowser )
As long as the user is logged in to the site, the session_id in the cookies of his browser matches the session_id stored in the database. That allows the user to visit any pages of the site to which he has access.
To do this, the browser sends session_id there with each new request to the server, and as long as this code matches the code in the server database, the user remains authorized.
If the user logs out, the server will delete his session_id from its database. And on those pages of the site where an authorized login is required, this user will not be able to log in.
If the user logs in again, he will again be able to visit any page of the site. But it will already be a completely different session with a different session_id.
All tables in a Django database (DB) are described using models. These are special Python classes that inherit from the Model class.
One model describes one database table. And each line of this model describes one field of the table.
In more detail we will analyze examples of models in the next 5th lesson. For now, let's just take note that the User user model, which has a certain set of basic fields and methods, already exists by default. It is always created in Django during the first migration. In particular, there are already fields username, email, password, is_staff (whether the user is an employee) and < b>is_superuser.
New unfamiliar abbreviations usually inspire fear. To get rid of at least half of this fear, we immediately note that all the requests that we have talked about so far are GET requests. Really, isn't it scary at all? Then let's move on!
So, when accessing the server, different types of requests are possible. In general, the HTTP protocol has a lot of them, but in this course we will use only two:
To send a GET request, it is enough to specify a regular url. But the transmission of a POST request MANDATORY implies the presence of a form and a csrf token.
A form in HTML is a special construct that is enclosed in a <form></form> tag. This can also contain input fields with a single <input> tag, which is usually paired with a <label></label> tag, which explains that it is necessary to enter in the field input.
The form also contains a button (input field) with type="submit", clicking on which is exactly the command to send data to the server.
The current form looks like this in HTML code:
<form method="post" action="/your-url/">
<label for="username">User name: </label>
<input id="username" type="text" name="username" value="{{ username }}">
<input type="submit" value="OK">
</form>
In the previous lesson, we have already learned how to pass control from the configurator main/urls.py to the view. However, another option is also good practice: passing the url from the main configurator to another application's configurator.
To do this, in main/urls.py we will add a line, thanks to which all authorization requests starting with 'auth/' will be transferred for processing to another configurator - authentication /urls.py:
from django.contrib import admin
from django.urls import path, include
from company import views as views_company
urlpatterns = [
path('', views_company.home_page, name='index'),
path('about/', admin.site.urls),
path('admin/', admin.site.urls),
path('auth/', include('authentication.urls')),
]
Therefore, in authentication/urls.py there will be requests corresponding to authorization, registration and logout:
from django.urls import path
from authentication import views
urlpatterns = [
path('login/', views.login_user, name='login'),
path('register/', views.register, name='register'),
path('logout/', views.logout_user, name='logout'),
]
Now, to begin with, we will make simple views that will be launched on these three requests in the authentication application:
def login_user(request):
return render(request, ‘auth/login.html’)
def register(request):
return render(request, ‘auth/register.html’)
def logout_user(request):
pass
Nothing is clear yet about logging out - there is no page for it, so instead of the code we will simply write pass.
Well, actually, the template is not needed here. And you need a special logout function logout (request).
However, if we limit the view to just this function, we get an error: The view authentication.views.logout_user didn't return an HttpResponse object. It returned None instead.
Therefore, at the end of the view, we will have to return the transition to some page. For example, home. This can be done using the redirect('index') function, where 'index' is the address name in authentication/urls.
Obviously, now the construction for all views in the authorization application will not be as simple as it was for the index view. Because now we have a new task: to read the data from the HTML page, transfer it to the server and then to the User table in our database. And, as mentioned above, everything that is transferred to the database should only be transmitted using a form and a POST request.
To create forms in Django, a special forms.py module is provided, in which you can first create an "input form" object and describe its properties and characteristics in detail. The form is usually associated with a specific table in the database. Therefore, when creating a form, you should:
Since we are using a ready-made Django's User table with a rather voluminous list of standard fields, it is imperative to specify in our form which fields will be used:
from django import forms
from django.contrib.auth.models import User
class LoginForm(forms.Form):
username = forms.CharField()
password = forms.CharField(widget=forms.PasswordInput())
The last line says that when entering a password, a special widget will be used that allows you to hide the symbol when filling out the field.
In the next lesson, we'll see that creating a form looks very similar to creating a database table.
Thus, the following options will need to be added to our login_user view:
You can pass any data to the page using an additionally created context dictionary. In our version, by the login_form key, we will add an instance of the class of the newly created form LoginForm():
def login_user(request):
context = {'login_form': LoginForm()}
return render(request, 'auth/login.html', context)
This is quite enough to transfer data to an html page, but to receive data from the page, you will need to add data processing on a POST request.
def login_user(request):
context = {'login_form': LoginForm()}
if request.method == 'POST':
login_form = LoginForm(request.POST)
if login_form.is_valid():
username = login_form.cleaned_data['username']
password = login_form.cleaned_data['password']
user = authenticate(username=username, password=password)
if user:
login(request, user)
return redirect('index')
return render(request, 'auth/login.html', context)
At the same time, on the html page, you will need to add:
That is, the form fragment in the HTML code will look something like this:
<form method="post" action="{% url 'login' %}">
{% csrf_token %}
<label class="form-label" for="id_username">Login</label>
<input type="text" class="form-input" id="id_username"
name="username" placeholder="Login" required>
<label class="form-label" for="id_password">Password</label>
<input type="password" class="form-input" id="id_password"
name="password" placeholder="Password" required>
<button type="submit" class="request-quote-btn">Log In</button>
</form>
The transfer of data from a user is a very sensitive operation with which a potential attacker can try to break into the server. Therefore, for additional security, in addition to the already existing session key, another secret key csrf_token is passed to the page. It is valid only for one data transfer from a particular form, and only for the data transfer of this particular form. If something goes wrong and a new form input is required, the server will generate a new csrf_token on a new request.
Filling out the form may be with errors - this is completely normal. It is not normal if the server "silently" does not accept this data and does not "explain" to the user why.
There are at least two ways.
1.) The simplest solution is to simply tell the user that the input is not correct. To do this, a new attention field is simply added to the context dictionary. In addition, it will be correct to return the form with exactly the data that did not pass.
if user:
login(request, user)
return redirect('index')
context = {
'login_form': LoginForm(request.POST),
'attention': f'The user with name {username} is not registered in the system!'}
2.) Another, more elegant and neat way: tell the user exactly what errors occurred during the validation process. And here you will need changes directly in the form itself:
def clean(self):
cleaned_data = super().clean()
username = cleaned_data.get('username')
password = cleaned_data.get('password')
try:
self.user = User.objects.get(username=username)
except User.DoesNotExist:
raise forms.ValidationError(f'User with username: [{username}] does not exist!')
if not self.user.check_password(password):
raise forms.ValidationError('Could not log in using these email and password')
return cleaned_data
Now these errors will be added to the LoginForm() form class instance. True, in order to read them on the authorization page, minor changes to the HTML code will also be required:
{% for error in login_form.non_field_errors %}
<div class="alert alert-danger">{{ error }}</div>
{% endfor %}
At this point, the mission of creating an authorization page can be considered completed!
In addition to the view functions already well known to us, there are also view classes that inherit from generic views. Moreover, the latter is preferred when solving complex problems, since generic views look more compact and are easier to read.
We will start our acquaintance with the topic of generic views with only one option for now - the TemplateView class. (Later in this course, we will look at several more types of generic views).
As an example, let's rewrite our register function-based view into a RegisterView using the TemplateView class:
from django.views.generic import TemplateView
class RegisterView(TemplateView):
template_name = 'auth/register.html'
This class has get and post methods to handle GET and POST requests:
def get(self, request):
user_form = RegisterForm()
context = {'user_form': user_form}
return render(request, 'auth/register.html', context)
def post(self, request):
user_form = RegisterForm(request.POST)
if user_form.is_valid():
user = user_form.save()
user.set_password(user.password)
user.save()
login(request, user)
return redirect('index')
context = {'user_form': user_form}
return render(request, 'auth/register.html', context)
It is worth paying special attention to the method of creating a new user record in the User table, since we will continue to use this method further:
The form RegisterForm, which is used here in the post method, should be mentioned separately.
The previous LoginForm form was created based on the Form class, which is a fairly simple variant that looks very similar to the Model class of models.
Forms created with the ModelForm class are more advanced. Here we no longer need to prescribe each field of the form - just specify the name of the model that will be used in this form, and the list of required fields. After that, the format of the fields from the model attached to the form will automatically be transferred to the form itself.
New form example:
class RegisterForm(forms.ModelForm):
class Meta:
model = User
fields = ('username', 'email', 'password')
In the previous LoginForm example, we attempted to embed a completed form into an HTML page using the {{ login_form }} tag. However, we quickly abandoned this idea - the styles of the standard Django form and the layout styles of our login.html page were too different.
But there is a solution. It is only necessary to display the fields not “at once”, but to do it gradually, in a cycle. Moreover, in this case, Django allows you to “break” the individual output for each form field into three components:
An example of HTML code that implements the above:
{% for field in user_form %}
<div class="row">
<div class="col-12 col-lg-4 col-md-4 offset-md-4">
<div class="quote-form-wrapper">
<span style="color: red">{{ field.errors }}</span>
{{ field.label_tag}}
{{ field }}
</div>
</div>
</div>
{% endfor %}
You can learn more about all the details of this stage from this video (RU voice):