Django #4

Możesz sprawdzić działanie aplikacji tworząc darmowe konto tutaj.
Kod źródłowy można obrać z Githuba tutaj.

W tym wpisie przedstawię widoki odpowiedzialne za zarządzanie autami i naprawami.

▣ Zacznę od głównego widoku prezentującego auta wpisane przez użytkownika. Maksymalna liczba wpisów na stronie to 10 aut. Po kliknięciu na poszczególne auto wyświetlane są naprawy danego auta. Zarówno wpisy aut jak i napraw są posortowane według daty tzn. najnowsze wpisy są wyświetlane jako pierwsze.

Klasa widoku wyświetlająca auta dziedziczy po klasie ListView oraz po klasie LoginRequiredMixin (aby dostęp był możliwy tylko dla zalogowanych użytkowników).

Na początku definiuję wszystkie atrybuty klasy tzn. używany model – Car, używany szablon – cars.html, nazwa obiektu, pod którą są dostępne dane w kontekście – context_object_name – ‘cars’, a także liczba wpisów na stronie – 10 pojazdów.

W tej klasie przesłaniam dwie metody – get_queryset() odpowiedzialną za filtrowanie danych oraz get_context_data(), w której uzupełniam dane kontekstu o dane z pola wyszukiwania.

Kod klasy CarListView() wygląda następująco:

class CarsListView(LoginRequiredMixin, ListView):
    model = Car
    template_name = 'cars/cars.html'
    context_object_name = 'cars'
    paginate_by = 10

    def get_queryset(self):
        if self.request.GET.get('q'):
            q = self.request.GET.get('q')
            make_results = self.model.objects.filter(
                user=self.request.user, make=q).order_by('-pk')
            model_results = self.model.objects.filter(
                user=self.request.user, model=q).order_by('-pk')
            if make_results.exists():
                return make_results
            elif model_results.exists():
                return model_results
            else:
                return self.model.objects.none()
        return self.model.objects.filter(user=self.request.user).order_by('-pk')

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['q'] = self.request.GET.get('q', '')
        return context

Metoda get_queryset() wyświetla wszystkie pojazdy danego użytkownika. Ostatnio dodane auto jest widoczne jako pierwsze (sortowanie ‘-pk’). Jeśli w polu wyszukiwania została wpisana marka lub model pojazdu/pojazdów to zostaną wyświetlone tylko te pojazdy.

Metoda get_context_data() dodaje wpis z pola wyszukiwania do kontekstu, dzięki czemu poprawnie wyświetlane są dane o naprawach aut przy podziale aut na poszczególne strony.

▣ Dodawanie pojazdu odbywa się po wciśnięciu przycisku Add Car w menu aplikacji. Widok, który obsługuje dodawanie auta to klasa AddCarView(), a szablon, który wyświetla formularz dodawania auta to car_form.html (ten sam szablon obsługuje również aktualizację danych opisujących dane auto) .

Klasa AddCarView() dziedziczy funkcjonalność po klasie CreateView() oraz LoginRequiredMixin (tylko zalogowani użytkownicy mogą utworzyć nowy pojazd).

Definiuję następujące atrybuty klasy widoku: model, który używa klasa – Car, fields – pola formularza, które mają być widoczne, oraz success_url – adres url, który zostanie załadowany po pomyślnym wypełnieniu formularza.

Przesłaniam metodę form_valid(), która dodaje użytkownika, który utworzył nowe auto (model Car wymaga zdefiniowania atrybutu user).

Kod klasy AddCarView() wygląda następująco:

class AddCarView(LoginRequiredMixin, CreateView):
    model = Car
    fields = ['make', 'model', 'vrn', 'year']
    success_url = '/'

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

▣ Usuwanie auta jest realizowane za pomocą klasy widoku DeleteCarView(), a szablon, który wyświetla monit czy kasować auto to plik car_confirm_delete.html (wyświetla przycisk Delete, który kasuje auto i przycisk Cancel, który wraca do poprzedniej strony).

Klasa DeleteCarView() dziedziczy funkcjonalność po klasie DeleteView, LoginRequiredMixin (tylko zalogowany użytkownik może skasować auto) oraz UserPassesTestMixin(użytkownik może usunąć tylko auto utworzone przez siebie).

Definiuję argumenty model – używany model Car oraz success_url – adres, pod który będzie załadowany po pomyślnym usunięciu auta – w tym przypadku widok prezentujący wszystkie auta użytkownika.

Tworzę metodę test_func(), która sprawdza, czy użytkownik, który chce usunąć auto jest osobą, która utworzyła dane auto (użytkownik może usunąć tylko utworzone przez siebie auta).

Przesłaniam metodę delete(), która dodatkowo wyświetla wiadomość o skasowaniu auta.

Kod klasy DeleteCarView() wygląda następująco:

class DeleteCarView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Car
    success_url = '/'

    def test_func(self):
        if self.get_object().user == self.request.user:
            return True
        return False

    def delete(self, request, *args, **kwargs):
        success_message = f'Car {self.get_object()} has been deleted'
        messages.success(self.request, success_message)
        return super().delete(request, *args, **kwargs)

▣ Uaktualnienie danych opisujących konkretne auto odbywa się za pomocą klasy UpdateCarView(), natomiast szablon wyświetlający formularz do zmiany tych danych to car_form.html.

Klasa UpdateCarView() dziedziczy funkcjonalność po klasie UpdateView, jak również po klasach LoginRequiredMixin (uaktualnienie danych może dokonać tylko zalogowany użytkownik) oraz UserPassesTestMixin (wywoływana jest funkcja sprawdzająca, czy użytkownik, który chce dokonać modyfikacji jest tym, który utworzył dane auto).

Definiuję atrybuty klasy: model – określa model, który jest wykorzystywany do zmiany danych – w tym przypadku model Car. Atrybut fields – określający jakie pola formularza mają być dostępne. Atrybut success_message definiujący łańcuch tekstowy do wyświetlenia jako komunikat o uaktualnieniu danych auta.

Definiuję dwie metody: test_func() oraz get_success_url().

Metoda test_func() sprawdza, czy użytkownik, który chce dokonać modyfikacji danych auta jest tym, który utworzył dane auto.

Metoda get_success_url() przesłania metodę dokonującą modyfikacji danych auta i dodatkowo wyświetla komunikat o zmianie danych i wraca na stronę określoną nazwą: car_detail – wyświetlającą naprawy dla danego auta. Jako dodatkowe parametry przesyłane są za pomocą metody GET: row, p oraz q określające odpowiednio wiersz, stronę oraz łańcuch tekstowy z pola wyszukiwania.

Kod klasy UpdateCarView():

class UpdateCarView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Car
    fields = ['make', 'model', 'vrn', 'year']
    success_message = 'Car info has been updated'

    def get_success_url(self, **kwargs):
        row = self.request.GET.get('row')
        p = self.request.GET.get('p')
        q = self.request.GET.get('q')
        options = '?p=' + p + '&row=' + row
        options += '&q=' + q
        messages.success(self.request, self.success_message)
        return reverse_lazy('car_detail') + options

    def test_func(self):
        if self.get_object().user == self.request.user:
            return True
        return False

▣ Dodanie notatki o naprawie jest definiowane w klasie AddRepairView() widoku, a formularz jest zdefiniowany w szablonie repair_form.html.

W klasie tej występują następujące argumenty: model – określający wykorzystywany model – w tym przypadku Repair. Następny argument: fields określa jakie są wyświetlane pola formularza. Ostatni argument to łańcuch success_message wyświetlany po pomyślnym dodaniu notatki o naprawie.

Metody obecne w klasie AddRepairView() to: get_context_data(), form_valid() oraz get_success_url().

Metoda get_context_data() dodaje obiekt Car do kontekstu, dzięki czemu jest on widoczny w szablonie.

Metoda form_valid() wykorzystuje dane z formularza do utworzenia nowej instancji modelu. Do poprawnego utworzenia instancji klasy Repair jest konieczne podanie klucza obcego – obiektu Car określającego pojazd, którego dotyczy dana notatka o naprawie.

Metoda get_success_url() określa adres strony, która ma być wyświetlona po pomyślnym dodaniu nowej notatki. W tym przypadku wyświetlona zostanie strona o nazwie car_detail zdefiniowana w cars/urls.py.

class AddRepairView(LoginRequiredMixin, CreateView):
    model = Repair
    fields = ['date', 'description']
    success_message = 'New repair has been added'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['car'] = Car.objects.get(id=self.kwargs['pk'])
        return context

    def form_valid(self, form, **kwargs):
        form.instance.car = Car.objects.get(id=self.kwargs['pk'])
        return super().form_valid(form)

    def get_success_url(self, **kwargs):
        row = self.request.GET.get('row')
        p = self.request.GET.get('p')
        q = self.request.GET.get('q')
        options = '?p=' + p + '&row=' + row
        options += '&q=' + q
        return reverse_lazy('car_detail') + options

— c.d.n —

Django #3

We wcześniejszych wpisach opisałem zarządzanie użytkownikami w >>aplikacji<< tzn. nowy użytkownik może się zarejestrować – utworzyć konto w witrynie, użytkownik zarejestrowany może się zalogować, wylogować lub zmienić swoje hasło.

Następnie opiszę główną funkcjonalność witryny tzn. ewidencję aut i napraw. W tym celu w pliku urls.py projektu załączę mapowanie adresów związanych z aplikacją cars. Po zmianach plik urls.py projektu będzie wyglądał następująco:

from django.contrib import admin
from django.urls import path, include
from users import views as users_views
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('cars.urls')),
    path('register-user/', users_views.RegisterUser.as_view(
        template_name='users/register-user.html'), name='register_user'),
    path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name='logout'),
    path('change-password/', users_views.ChangePassword.as_view(
        template_name='users/change-password.html'), name='change_password'),
]

Można zauważyć, że mapowanie adresów na odpowiednie widoki uwzględnia też plik urls.py utworzony w aplikacji cars. Plik urls.py z aplikacji cars wyglada następująco:

from django.urls import path
from .views import (
    CarsListView,
    AddCarView,
    RepairsListView,
    UpdateCarView,
    DeleteCarView,
    AddRepairView
)
urlpatterns = [
    path('', CarsListView.as_view(), name='cars'),
    path('add-car/', AddCarView.as_view(), name='add_car'),
    path('car/', RepairsListView.as_view(), name='car_detail'),
    path('car/<int:pk>/update/', UpdateCarView.as_view(), name='update_car'),
    path('car/<int:pk>/delete/', DeleteCarView.as_view(), name='delete_car'),
    path('car/<int:pk>/new-repair/', AddRepairView.as_view(), name='add_repair'),
]

Główny widok definiowany w klasie CarListView wyświetla wszystkie auta użytkownika.

Należy pamiętać o dopisaniu aplikacji w pliku settings.py projektu w liście INSTALLED_APPS np. dla aplikacji cars będzie to element: ‘cars.apps.CarsConfig’.

W aplikacji users korzystałem z preinstalowanego modelu użytkownika. W aplikacji cars należy stworzyć modele – obiekty mapujące tabele bazy danych. Modele te będą definiować: pojazd – model Car i naprawę – model Repair. Kod pliku models.py aplikacji cars wygląda następująco:

from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator


class Car(models.Model):
    make = models.CharField(max_length=10)
    model = models.CharField(max_length=10)
    vrn = models.CharField(max_length=10)
    year = models.IntegerField(default=timezone.now().year,
                               validators=[MinValueValidator(timezone.now().year - 100),
                                           MaxValueValidator(timezone.now().year)])
    user = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.make} {self.model}'


class Repair(models.Model):
    date = models.DateField(default=timezone.now)
    description = models.TextField()
    car = models.ForeignKey(Car, on_delete=models.CASCADE)

    def __str__(self):
        lead = self.description[:10] + '...'
        return lead

Model Car zawiera atrybuty make, model, vrn, year i user opisujące markę pojazdu, model, nr rejestracyjny pojazdu i rok produkcji, a także klucz obcy, zawierający obiekt użytkownika, który utworzył wpis o pojeździe. Pole year posiada walidatory, uniemożliwiające wpisanie auta o roku produkcji o 100 lat starszym od bieżącej daty i nowszym niż bieżący rok. Opcja on_delete atrybutu user ma wartość domyślną CASCADE tzn. usunięcie użytkownika, który wprowadzić powoduje także usunięcie wszystkich wprowadzonych przez niego aut.

Model Repair zawiera atrybuty date – data naprawy, description – szczegóły naprawy oraz car, będący kluczem obcym – jakiego auta dotyczy dana naprawa. Metoda __str__() wyświetla nagłówek będący początkiem opisu naprawy (10 początkowych znaków).

Aby móc dokonywać edycji wpisów obu modeli z poziomu panelu administracyjnego trzeba dopisać oba modele do pliku admin.py aplikacji cars tzn.

from django.contrib import admin
from .models import Car, Repair

admin.site.register(Car)
admin.site.register(Repair)

W następnej części opiszę widoki definiujące poszczególne funkcjonalności tzn. wyświetlanie aut należących do użytkownika, dodawanie napraw, a także wyszukiwanie aut za pomocą pola wyszukiwania na stronie.

— c. d. n. —

car search
car search

Django #2

Widoki logowania i wylogowania są predefiniowane jako klasy LoginView i LogoutView więc wystarczy tylko umieścić odpowiednie wpisy w pliku urls.py (jak opisałem w części 1) i utworzyć pliki szablonów tzn.

Listing szablonu login.html:

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}


{% block content %}
  <div class="row justify-content-center">
    <div class="col-4">
      <form method="POST" class="form-control-sm">
       {% csrf_token %}
       <fieldset class="form-group">
         <legend class="border-bottom text-primary">
            Log In
          </legend>
          {{ form|crispy }}
        </fieldset>
        <button class="btn btn-secondary" type="submit">Login</button>
        <p class="text-muted text-small mt-5 ml-2">Need an Account? <a href="{% url 'register_user' %}">Sign up</a></p>
        </form>
      </div>
    </div>
{% endblock %}
Szablon login.html

Listing szablonu logout.html:

{% extends 'cars/base.html' %}

{% block content %}
 <div class="row justify-content-center mb-5">
    <div class="col-4">
	   <h4 class="border-bottom text-primary mt-2">You're logged out now</h4>	
  <div class="text-muted text-small ml-2 mt-5">Back to: <a href="{% url 'login' %}">login page</a></div>
  </div>
 </div>
{% endblock %}
Szablon logout.html

Pozostało jeszcze utworzenie widoku odpowiedzialnego za zmianę hasła. Klasa widoku będzie dziedziczyć po klasie CreateView jak również po klasie LoginRequiredMixin. Klasa LoginRequiredMixin dodaje funkcjonalność polegającą na tym, że zmiany hasła może dokonać tylko zalogowany użytkownik.

Kod klasy widoku ChangePassword():

class ChangePassword(LoginRequiredMixin, CreateView):
    form_class = PasswordChangeForm

    def get(self, request, *args, **kwargs):
        form = PasswordChangeForm(request.user)
        return render(request, 'users/change-password.html', {'form': form})

    def post(self, request, *args, **kwargs):
        form = PasswordChangeForm(request.user, request.POST)
        if form.is_valid():
            user = form.save()
            update_session_auth_hash(request, user)
            messages.success(request, 'Password changed!')
            return redirect('cars')
        return render(request, 'users/change-password.html', {'form': form})

W powyższej klasie przesłaniam metody get() i post(). Gdy strona jest wyświetlana za pomocą metody get() to wyświetlany jest pusty formularz zmiany hasła dla zalogowanego użytkownika. Gdy strona jest wyświetlana za pomocą metody post(), to dokonywana jest walidacja przesłanych danych i jeśli są one poprawne następuje zapisanie zmienionego hasła, wyświetlenie komunikatu o zmianie hasła i przekierowanie na stronę o nazwie cars.

Pozostaje dodać plik szablonu tzn. change-password.html:

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}


{% block content %}
  <div class="row justify-content-center">
   <div class="col-4">
	  <form method="POST" class="form-control-sm">
      {% csrf_token %}
      <fieldset class="form-group">
        <legend class="border-bottom text-primary">
            New Password Settings
        </legend>
        {{ form|crispy }}
      </fieldset>
      <button class="btn btn-secondary" type="submit">Save</button>
    </form>
   </div>
  </div>
{% endblock %}
Szablon change-password.html

>>część 3<<

Django #1

We >>wstępie<< opisałem główne założenia projektu.

Poniżej zamieszczam listing wymaganych pakietów do projektu (wygenerowany przez pip freeze):

asgiref==3.2.5
Django==3.0.4
django-crispy-forms==1.9.0
pytz==2019.3
sqlparse==0.3.1

Polecam utworzyć osobne środowisko wirtualne dla projektu (jak stworzyć wirtualne środowisko opisałem >>tutaj<<).

Wszystkie pakiety oprócz django-crispy-forms instalowane są podczas instalacji pakietu django.

W katalogu zawierającym również podkatalog venv tworzymy projekt za pomocą komendy: django-admin startproject nazwa_projektu

Startproject tworzy główny katalog projektu oraz plik manage.py, dzięki któremu można będzie zarządzać projektem.

Następnie można utworzyć administratora projektu, czyli użytkownika o maksymalnych uprawnieniach administracyjnych, dzięki czemu będzie można używać webowego panelu administracyjnego. Jednak najpierw należy przeprowadzić migracje: python manage.py migrate dzięki czemu zostają utworzone odpowiednie tabele w bazie danych np. admin, auth itd. Po przeprowadzonej migracji można utworzyć konto administracyjne za pomocą komendy: python manage.py createsuperuser

Katalog projektu zawiera następujące pliki i katalogi:
├── nazwa_projektu
│   ├── asgi.py
│   ├── init.py
│   ├── pycache
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
└── manage.py

Domyślnie dane umieszczane zostają w bazie sqlite3 (standardowo plik db.sqlite3), co jest dobrym rozwiązaniem podczas tworzenia projektu, a finalnie można użyć innej bazy (Django zawiera wsparcie dla PostgreSQL, MySQL i wielu innych). Zmiana domyślnej bazy wymaga tylko modyfikacji danych z sekcji DATABASES pliku settings.py projektu.

Aby odseparować poszczególne elementy składowe projektu wydzieliłem dwie części składowe tzn. odpowiedzialną za zarządzanie użytkownikami – users oraz za zarządzanie pojazdami – cars. W Django są to tzw. aplikacje. Aby je utworzyć należy wpisać: python manage.py startapp users oraz python manage.py startapp cars

Zacznę od aplikacji users, w której nowi użytkownicy będą mogli tworzyć konta, oraz logować się, wylogowywać, a także zmieniać swoje hasło dostępowe.

Aby określić mapowanie adresów URL na konkretne klasy widoków modyfikuję plik urls.py projektu tzn.

from django.contrib import admin
from django.urls import path
from users import views as users_views
from django.contrib.auth import views as auth_views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('register-user/', users_views.RegisterUser.as_view(
        template_name='users/register-user.html'), name='register_user'),
    path('login/', auth_views.LoginView.as_view(template_name='users/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(template_name='users/logout.html'), name='logout'),
    path('change-password/', users_views.ChangePassword.as_view(
        template_name='users/change-password.html'), name='change_password'),
]

Przykładowo po wpisaniu adresu kończącego się na: register-user/ zostaje wywołana klasa RegisterUser() z pliku views.py oraz do tego przypisania zostaje nadana nazwa, której będziemy mogli używać w szablonach. Aby klasa mogła być użyta jako widok, należy wywołać jej metodę as_view(). Jako argument podałem argument template_name, w którym podaję nazwę szablonu, który wyświetla podaną stronę. Równie dobrze można by podać ten argument jako atrybut klasy w widoku, ale aby zachować spójność z pozostałymi widokami umieściłem go w tym miejscu.

kod klasy RegisterUser() z pliku views.py aplikacji users wygląda następująco (na początku umieściłem wymagane importy)

from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm, PasswordChangeForm
from django.contrib.auth import login, authenticate
from django.views.generic import CreateView
from django.contrib import messages

class RegisterUser(CreateView):
    form_class = UserCreationForm

    def get(self, request, *args, **kwargs):
        return render(request, 'users/register-user.html', {'form': UserCreationForm()})

    def post(self, request, *args, **kwargs):
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            username = form.cleaned_data.get('username')
            password = form.cleaned_data.get('password1')
            user = authenticate(username=username, password=password)
            login(request, user)
            messages.success(request, f'Welcome, {username}!')
            return redirect('cars')
        return render(request, 'users/register-user.html', {'form': form})

Klasa RegisterUser() dziedziczy po klasie CreateView, a formularz jest definiowany przez klasę UserCreationForm. Gdy strona zostaje wyświetlona za pomocą metody GET to pojawia się pusty formularz, gdy natomiast podane są już jakieś dane w formularzu, to są one przekazane za pomocą metody POST. Aby obsłużyć GET i POST przesłaniam odpowiednio metody get() i post() klasy RegisterUser(). W metodzie post() sprawdzana jest poprawność formularza i jeśli formularz jest poprawny to następuje zapisanie danych z formularza tzn. utworzenie użytkownika za pomocą metody save(). Domyślnie jest to użytkownik pozbawiony przywilejów administracyjnych. Zostaje on zalogowany przy użyciu podanych przy rejestracji danych. Nowa wiadomość z tagiem Success zostanie wyświetlona po przekierowaniu na adres o nazwie cars. Wiadomość wyświetla się tylko jednokrotnie na stronie.

Pozostaje jeszcze utworzyć szablon register-user.html dla RegisterUser(). W tym celu tworzę katalog szablonów w aplikacji users zgodnie z konwencją – w katalogu templates należy utworzyć kolejny katalog z nazwą zgodną z nazwą aplikacji tzn.

nazwa_projektu/users/templates/users/register-user.html

Kod pliku register-user.html jest częścią większej całości, a ściślej nie utworzonego na razie pliku base.html z aplikacji cars. Aplikacje mogą korzystać ze swoich szablonów, a dzięki temu wygląd zdefiniowany w aplikacji cars w szablonie base.html jest zachowany w szablonie register-user. W szablonie jest też włączone korzystanie z tagów crispy_forms, dzięki którym formularz wygląda lepiej. Każdy formularz musi zawierać csrf_token, dzięki czemu strona jest automatycznie odporna na pewne formy ataków (szczegóły >>tutaj<<).

Listing pliku register-user.html wygląda następująco:

{% extends 'cars/base.html' %}
{% load crispy_forms_tags %}

{% block content %}
  <div class="row justify-content-center">
    <div class="col-4">
      <form method="POST" class="form-control-sm">
        {% csrf_token %}
        <fieldset class="form-group">
          <legend class="border-bottom text-primary">
            Registration
          </legend>
            {{ form|crispy }}
        </fieldset>
          <button class="btn btn-secondary" type="submit">Register</button>
      </form>
    </div>
  </div>
{% endblock %}
Szablon register-user.html

>>część 2<<

Django – framework webowy

W tej serii opiszę podstawy frameworka Django na przykładzie aplikacji do notowania napraw związanych z posiadanymi autami.

Możesz utworzyć własne konto i zalogować się tutaj.

Kod projektu jest do pobrania tutaj.

Aplikacja jest podobna do tej opisanej we  >>wpisie<<, ale zastosowanie frameworku webowego wymusiło dodatkowe funkcjonalności, np. obsługę kont użytkowników.

>>część 1<<