Aby utworzyć własne uprawnienia dla niestandardowego użytkownika utworzonego wcześniej tworzę klasę dziedziczącą po klasie BasePermission.
Utworzę dwie klasy uprawnień:
Klasę umożliwiającą dostęp użytkownikom, którzy są klientami (atrybut role ma wartość client). Dostawcy nie mają uprawnień do zasobu.
Klasę umożliwiającą użytkownikom, którzy są dostawcami tylko możliwość odczytu.
Na wstępie, w pliku permissions.py aplikacji contracts importuję klasę BasePermission, oraz listę „bezpiecznych metod” tzn. umożliwiających wylistowanie zasobu, bez możliwości edycji, usuwania czy dodania nowego kontraktu.
from rest_framework.permissions import BasePermission, SAFE_METHODS
"""Custom permissions classes"""
Klasa IsClient() umożliwia dostęp tylko klientom. Pozostali użytkownicy zobaczą komunikat message informujący o braku dostępu do zasobu:
class IsClient(BasePermission):
message = "Only clients can access"
def has_permission(self, request, view):
if request.user.role == "client":
return True
return False
Klasa IsClientOrReadOnly() umożliwia użytkownikom, którzy są klientami możliwość dodania, kasowania i edycji, pozostali użytkownicy maja możliwość wyświetlenia danych. tzn.:
class IsClientOrReadOnly(BasePermission):
message = "Only clients can modify, contractors can read only"
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
return request.user.role == "client"
Utworzone klasy uprawnień umieszczam w widokach np.
class ContractViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsClientOrReadOnly]
serializer_class = ContractSerializer
W tym wpisie opiszę testowanie API dla utworzonego kastomowego użytkownika. (wcześniejsze wpisy – >>część 1<< oraz >>część 2<<)
Dostęp do widoku ContactUserCreateRetrieveViewSet umożliwiającego tworzenie nowego użytkownika oraz wyświetlanie konkretnego użytkownika jest zarejestrowany w pliku urls.py aplikacji users tzn.
from rest_framework.routers import DefaultRouter
from .views import ContractUserCreateRetrieveViewSet
router = DefaultRouter()
router.register("", ContractUserCreateRetrieveViewSet, basename="user")
urlpatterns = router.urls
Jako parametr basename zdefiniowałem jako user, do której to nazwy będę się odwoływał podczas testów.
Plik urls.py aplikacji users jest następnie importowany w pliku urls.py projektu tzn.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api-auth/", include("rest_framework.urls")),
path("api/user/", include("users.urls")),
]
Kompletny punkt dostępowy do tworzenia użytkowników będzie więc miał postać /api/user/
Tworzenie użytkownika i wyświetlanie konkretnego użytkownika będzie realizowane przez konkretne metody z pliku views.py aplikacji users tzn. tworzenie – funkcja create(), wyświetlanie użytkownika – funkcja retrieve()
from django.shortcuts import get_object_or_404
from rest_framework import viewsets, mixins
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import ContractUser
from .serializers import ContractUserSerializer
class ContractUserCreateRetrieveViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
serializer_class = ContractUserSerializer
queryset = ContractUser.objects.all()
def retrieve(self, request, pk):
permission_classes = [IsAuthenticated]
queryset = ContractUser.objects.filter(id=request.user.id)
user = get_object_or_404(queryset, pk=pk)
serializer = ContractUserSerializer(user)
return Response(serializer.data)
def create(self, request):
serializer = ContractUserSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Klasa ContractUserCreateRetrieveViewSet dziedziczy po podstawowej klasie GenericViewSet oraz po klasach mixins, które dodają wymagane funkcjonalności – tworzenie i wyświetlanie użytkownika.
Funkcja create() serializuje podane dane i po sprawdzeniu, czy są one poprawne zapisuje je w bazie i zwraca obiekt Response i odpowiedni kod statusu.
Funkcja retrieve() sprawdza czy użytkownik jest zalogowany i czy filtruje użytkowników, aby zwracany queryset zawierał tylko tego użytkownika, który jest obecnie zalogowany, a następnie zwraca obiekt Response i odpowiedni kod statusu.
W pliku signals.py aplikacji users modyfikuję pole email udostępnione przez klasę AbstractUser, aby pole to było wymagane podczas tworzenia modelu ContractUser tzn.
Jeśli chcielibyśmy, aby pole email było unikalne, to możemy ustawić parametr unique jako True, tzn.
instance._meta.get_field("email")._unique = True
Wówczas jeśli użytkownik wpisze email, który już jest w bazie to wystąpi błąd walidacji. Niestety dzięki temu będzie wiadomo, że użytkownik o takim adresie email jest już obecny w bazie.
Aby przetestować tworzenie i wyświetlanie użytkownika tworzę katalog tests w aplikacji users.
Do niego przenoszę plik tests.py utworzony w poprzednim artykule, który zawierał testy modelu i zmieniam mu nazwę na test_model.py
W utworzonym katalogu tests tworzę nowy plik o nazwie test_api.py, który będzie zawierał testy api dla aplikacji users tzn.
from rest_framework.test import APITestCase
from rest_framework import status
from django.urls import reverse
from ..models import ContractUser
class ContractUserApiTestCase(APITestCase):
"""Testing ContractUser API"""
def test_create_user(self):
endpoint = reverse("user-list")
user_data = {
"username": "user_1",
"password": "123456789",
"email": "user_1@company.com",
}
response = self.client.post(endpoint, data=user_data, format="json")
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_retrieve_user(self):
user = ContractUser.objects.create(
username="user1", password="12345", email="user1@company.com"
)
self.client.force_authenticate(user)
endpoint = reverse("user-detail", args=[user.id])
response = self.client.get(endpoint, format="json")
self.assertEqual(response.status_code, status.HTTP_200_OK)
W metodzie test_create_user() tworzę endpoint przy użyciu funkcji reverse(). Funkcja reverse() bierze jako parametr basename nadany w pliku urls.py wraz z sufiksem -list. Następnie tworzę dane dla testowego użytkownika i przy wykorzystaniu domyślnego klienta obecnego w klasie APITestCase dokonuję zapytania przy użyciu metody POST. Asercja sprawdza, czy kod statusu odebranej odpowiedzi to 201, gdy pomyślnie utworzono użytkownika, czy też wynosi on 400 w przeciwnym przypadku.
W metodzie test_retrieve_user() tworzę testowego użytkownika i następnie zalogowuję go za pomocą funkcji force_authenticate(). Funkcja reverse() jako parametr bierze wartość basename nadany w pliku urls.py wraz z sufiksem -detail. Jako dodatkowy parametr załączam id zalogowanego użytkownika. Asercja sprawdza, czy kod statusu odpowiedzi to 200, gdy zapytanie zakończyło się sukcesem czy też status wynosi 400 w przeciwnym przypadku.
Aby przetestować kastomowy model ContractUser utworzony w poprzednim artykule, w pliku tests.py aplikacji users importuję klasę TestCase z modułu django.test oraz testowany model tzn.
Tworzę nową klasę ContractUserTestCase, która dziedziczy po klasie TestCase.
W metodzie setUp() tworzę dwóch tymczasowych użytkowników user1 i user2.
W metodzie test_create_user() przeprowadzam 1 test składający się z kilku asercji, w szczególności sugerowany przez narzędzie coverage test tekstowej reprezentacji utworzonej instancji tzn.
W podobny sposób testuję pozostałe modele z aplikacji contracts.
W modelu Contract obecny jest także kastomowy walidator, który sprawdza, czy data dostawy nie jest wcześniejsza niż data złożenia zlecenia.
część modelu Contract z models.py aplikacji contracts:
class Contract(models.Model):
class Meta:
ordering = ("-date_of_order",)
class StatusChoices(models.TextChoices):
OPEN = "open"
ACCEPTED = "accepted"
CANCELED = "cancelled"
class DeliveryDateValidator:
def validate(value):
if value < timezone.now().date():
raise validators.ValidationError(
"Date of delivery can't be earlier than date of order"
)
else:
return value
date_of_order = models.DateField(default=timezone.now)
date_of_delivery = models.DateField(
validators=(DeliveryDateValidator.validate,)
)
..........
Aby przetestować kastomowy walidator tworzę osobny test, który sprawdza, czy zostanie wywołany wywołany wyjątek ValidationError oraz czy zostanie zwrócona wpisana data, gdy podan0 poprawną datę (nie starszą od daty zamówienia).
Na wstępie tworzę nową aplikację o nazwie users tzn.
python manage.py startapp users
W pliku models.py tworzę niestandardowego użytkownika, który dziedziczy po klasie AbstractUser. Dodaję pole wyboru role, dzięki czemu użytkownik będzie należał albo do zleceniodawców, albo do zleceniobiorców (client lub contractor).
from django.contrib.auth.models import AbstractUser
from django.db import models
class ContractUser(AbstractUser):
class RoleChoices(models.TextChoices):
CLIENT = "client"
CONTRACTOR = "contractor"
role = models.CharField(
max_length=10,
choices=RoleChoices.choices, default=RoleChoices.CLIENT
)
def __str__(self):
return self.username
Nowy model user należy zarejestrować w pliku settings.py projektu, tzn.
AUTH_USER_MODEL = "users.ContractUser"
W aplikacji users tworzę plik serializers.py zawierający serializer nowego modelu użytkownika tzn.
from django.contrib.auth.hashers import make_password
from rest_framework import serializers
from .models import ContractUser
class ContractUserSerializer(serializers.ModelSerializer):
password = serializers.CharField(
min_length=8,
write_only=True,
style={"input_type": "password"}
)
class Meta:
model = ContractUser
fields = [
"id",
"username",
"password",
"email",
"role"
]
def create(self, validated_data):
validated_data["password"] = make_password(validated_data["password"])
return super().create(validated_data)
Serializer zawiera pole tekstowe umożliwiające wprowadzenie pola tekstowego dla hasła. Pole to zawiera parametr write_only ustawione na wartość True, aby możliwy był tylko zapis, a nie odczyt tego pola. Pole fields klasy wewnętrznej Meta zawiera listę wszystkich dostępnych pól do serializacji. Metoda create() tworzy hasło na podstawie wprowadzonych przez użytkownika danych przy użyciu funkcji make_password().
W pliku urls.py aplikacji users definiuję punkt dostępowy tzn.
from rest_framework.routers import DefaultRouter
from .views import ContractUserCreateRetrieveViewSet
router = DefaultRouter()
router.register("", ContractUserCreateRetrieveViewSet, basename="user")
urlpatterns = router.urls
Punkt dostępowy dla aplikacji users dołączam do głównego pliku urls.py projektu:
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("api/user/", include("users.urls")),
]
Punkt dostępowy /api/user/ będzie umożliwiał tworzenie użytkownika i podgląd danych dla danego użytkownika. W tym celu w pliku views.py tworzę klasę ContractUserCreateRetrieveViewSet(). Aby ograniczyć liczbę dostępnych metod nie korzystam z klasy ModelViewSet, ale dziedziczę po klasie GenericViewSet oraz odpowiednich klasach Mixin.
from django.shortcuts import get_object_or_404
from rest_framework import viewsets, mixins
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from .models import ContractUser
from .serializers import ContractUserSerializer
class ContractUserCreateRetrieveViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet,
):
serializer_class = ContractUserSerializer
queryset = ContractUser.objects.all()
def retrieve(self, request, pk):
permission_classes = [IsAuthenticated]
queryset = ContractUser.objects.filter(id=request.user.id)
user = get_object_or_404(queryset, pk=pk)
serializer = ContractUserSerializer(user)
return Response(serializer.data)
def create(self, request):
serializer = ContractUserSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Aby nowo utworzony użytkownik miał status nieaktywny skorzystałem z sygnału pre_save. W tym celu utworzyłem nowy plik signals.py w aplikacji users tzn.
from django.db.models.signals import pre_save
from .models import ContractUser
from django.dispatch import receiver
@receiver(pre_save, sender=ContractUser)
def create_inactive_user(sender, instance, **kwargs):
instance.is_active = False
Sygnał należy zaimportować w funkcji ready() klasy UsersConfig() z pliku apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
def ready(self):
import users.signals
W tym przykładzie dane pobrane do programu pochodzą z archiwum cen hurtowych Orlenu.
W związku z tym, że zmianie uległ sposób pobierania danych ze strony, musiałem zmienić także sposób działania mojego programu (poprzednia wersja wykorzystywała przeszukiwanie strony w celu odnalezienia na niej odpowiednich wartości – web scraping). Obecnie konieczne stało się skorzystanie z API, w celu uzyskania danych o cenach.
Aby ustalić, jak ma wyglądać poprawne zapytanie do API Orlenu skorzystałem z zakładki Sieć trybu developera przeglądarki.
Program będzie umożliwiał pobranie danych o cenach dla paliw, przy czym możliwe będzie wyświetlenie danych i wykresu z podziałem na konkretne lata i miesiące.
Projekt będzie składał się z głównego pliku programu: orlen-prices.py oraz pliku utils.py zawierającego funkcję do pobrania odpowiedzi z zewnętrznego API oraz funkcję do konwersji danych w formacie json do odpowiedniej ramki danych Pandas.
Pobrane dane będą w prosty sposób buforowane, aby ograniczyć liczbę zapytań do API.
Na początek opiszę funkcję fetch_data() z pliku utils.py odpowiadającą za pobranie odpowiedzi z zewnętrznego serwera. Jeśli zapytanie zakończy się sukcesem, funkcja zwraca odpowiedź w formacie json. Jeśli natomiast wystąpi wyjątek, to funkcja zwraca wartość None.
Następna funkcja z pliku utils.py dokonuje konwersję danych w formacie json do ramki danych Pandas. Wynikowa ramka zawierać będzie tylko dwie kolumny, pozostałe dane są pominięte. Kolumna effectiveDate będzie zawierać wartości typu datetime oraz będzie ustawiona jako index (w celu ułatwienia przeszukiwania po dacie). Kolumna value będzie miała format int. Cała ramka będzie natomiast posortowana względem wartości index.
Z menu będzie możliwe opcjonalne zamknięcie programu poprzez opcję File/Exit tzn.
def show_menu(self, root):
# Menu bar
menu_bar = tk.Menu(root)
root.config(menu=menu_bar)
# File menu
file_menu = tk.Menu(menu_bar, tearoff=0)
file_menu.add_command(label='Exit', command=self._exit)
menu_bar.add_cascade(label='File', menu=file_menu)
Metoda show_product_combobox() inicjalizuje widget Combobox, który będzie odpowiedzialny za wybór produktu, dla którego chcemy prześledzić zmianę cen. Wybór będzie przekazywany za pomocą zmiennej product_sv – obiekt StringVar. Zanim użytkownik wybierze odpowiedni produkt na widżecie jest wyświetlana podpowiedź.
def show_product_combobox(self, root):
self.product_sv = tk.StringVar(value='')
product_cb = ttk.Combobox(root, textvariable=self.product_sv)
product_cb['values'] = [x for x in self.products]
product_cb.set('Product')
product_cb.state = 'readonly'
product_cb.bind('<<ComboboxSelected>>', self.enable_show_chart_btn)
product_cb.grid(row=0, column=0, padx=10, pady=10)
Gdy użytkownik wybierze odpowiedni produkt to uruchamiana jest powiązana metoda: enable_show_chart_button(), gdyż domyślnie przycisk rysowania wykresu jest nieaktywny. Początkowo sprawdzany jest warunek, czy przycisk jest nieaktywny, następnie czy zostały wybrane produkt i rok. Jeśli wszystkie te warunki zostały spełnione to stan przycisku zmienia się na aktywny.
def enable_show_chart_btn(self, sender):
if str(self.show_chart_btn['state']) == 'disabled' and self.product_sv.get() != 'Product' and self.year_sv.get() != 'Year':
self.show_chart_btn['state'] = '!disabled'
Podobnie wygląda metoda wyboru roku, dla którego chcemy przeglądać dane. Wartości lat, z których można dokonywać wyboru są z zakresu od 2004 do obecnego roku, przy czym bieżący rok jest najwyżej na liście.
def show_year_combobox(self, root):
self.year_sv = tk.StringVar(value='')
year_cb = ttk.Combobox(root, textvariable=self.year_sv)
year_cb['values'] = [x for x in range(self.current_date.year, 2004-1, -1)]
year_cb.set('Year')
year_cb.state = 'readonly'
year_cb.bind('<<ComboboxSelected>>', self.enable_show_chart_btn)
year_cb.grid(row=0, column=1, padx=10, pady=10)
Pola radio służące do wyboru konkretnego miesiąca są osadzone w osobne ramce, a wybrana wartość jest przekazywana przez zmienną radio_sv. Jako że calendar.month_abbr zwraca listę, gdzie index 1 odpowiada za wartość stycznia itd, to wartość o indeksie 0 zawiara pusty łańcuch. Wykorzystałem to do umieszczenia opcji domyślnej – All – gdy chcemy wyświetlić wartości dla wszystkich miesięcy danego roku. Funkcja enumerate() użyta na liście umożliwiła umieszczenie odpowiedniego widgetu radio w kolejnej kolumnie.
def show_radiobuttons(self, root):
# Frame for Radiobuttons
rb_frame = tk.Frame(root)
rb_frame['borderwidth'] = 1
rb_frame['relief']='groove'
rb_frame.grid(row=0, column=2, sticky='w')
# Radiobuttons
self.radio_sv = tk.StringVar(value='All')
for i, month in enumerate(self.months):
if month=="":
month_rb = tk.Radiobutton(
rb_frame,
text='All',
value='All',
variable=self.radio_sv)
else:
month_rb = tk.Radiobutton(
rb_frame,
text=month,
value=month,
variable=self.radio_sv)
month_rb.grid(row=0, column=i)
Przycisk uruchamiający rysowanie wykresu jest domyślnie nieaktywny. Dopiero wybranie produktu i roku zmienia stan przycisku na aktywny. Przyciśnięcie przycisku uruchamia metodę show_chart_btn_clicked()
Program zawiera pasek statusu, w którym umieszczane są komunikaty o poprawnym wczytaniu danych, braku danych dla wybranego produktu w wybranym okresie lub o błędach związanych np. z brakiem połączenia z internetowego.
jeśli dane o produkcie są już wcześniej pobrane nie dokonuje zapytania do zewnętrznego API
jeśli dane są jeszcze nie pobrane to następuje próba pobrania ich z serwera. Gdy próba się nie powiedzie to jest wyświetlany komunikat o błędzie pobierania
dane zostają przefiltrowane na podstawie wybranych przez użytkownika wartości daty
przefiltrowane dane są parametrami przekazanymi do metod: _set_data_table(), która aktualizuje dane liczbowe oraz _show_chart(), która wyświetla wykres
def show_chart_btn_clicked(self):
self.msg.set('')
product_id = self.products[self.product_sv.get()]
if product_id in self.price_data:
data = self.price_data[product_id]
else:
data = fetch_data(product_id, self.current_date)
if data is not None:
self.price_data[product_id] = data
else:
self.msg.set('Error fetching data')
return
product_df = json_to_df(data)
year = self.year_sv.get()
month = self.radio_sv.get()
self.table.delete(*self.table.get_children())
try:
if month == 'All':
chart_df = product_df.loc[year]
else:
month_int = datetime.strptime(month, '%b').month
chart_df = product_df.loc[f'{year}-{month_int}']
except:
self.msg.set('No data for selected date')
for widget in self.chart_canvas.winfo_children():
widget.destroy()
else:
self.msg.set('OK')
self._set_data_table(chart_df[::-1])
self._show_chart(root, chart_df, year, month)
Aktualizacja danych w obiekcie Treeview wyświetlającym dane liczbowe o cenach:
def _set_data_table(self, df):
for i, row in enumerate(df.iloc):
date, value = row
if i % 2:
self.table.insert(
parent='', index=i, text='',
values=(date.date(), value),
tag='odd')
else:
self.table.insert(
parent='', index=i, text='',
values=(date.date(), value))
values – obiekt zawierający wartości wprowadzone do formularza i (po walidacji) przekazane do funkcji callback(), w której jest dokonywane zapytanie do zewnętrznego API
errors – obiekt zawierający wartości błędów walidacji, które będą wyświetlane pod polami formularza
validate – funkcja dokonująca walidacji wpisanych wartości podczas pisania
handleSubmit – funkcja dokonująca walidacji podczas wysyłania formularza (onSubmit)
W formularzu podajemy jako pierwszy argument obiekt event, a jako drugi argument listę nazw elementów formularza tzn.
import { useState } from 'react'
function useForm(callback) {
const [values, setValues] = useState({})
const [errors, setErrors] = useState({})
const pattern = new RegExp(
/^[a-zA-Z0-9]+@(?:[a-zA-Z0-9]+\.)+[A-Za-z]+$/
)
Na wstępie inicjowane są obiekty: values – przechowujący wprowadzone wartości do pól formularza oraz errors – przechowujący komunikaty błędów. Następnie tworzony jest obiekt RegExp przechowujący wzorzec pasujący do prawidłowego adresu email.
Funkcja useForm pobiera jako argument nazwę funkcji zwrotnej, która będzie wywołana po przesłaniu formularza i wykonaniu funkcji handleSubmit:
Powyższa funkcja pobiera jako argumenty obiekt event oraz listę pól formularza. Następnie wywoływana jest funkcja preventDefault() zapobiegająca domyślnemu działaniu podczas przesłania formularza, a dalej wywoływana jest funkcja validateOnSubmit() dla każdego pola formularza tzn.
const validateOnSubmit = (value) => {
if (values[value] === undefined) {
errors[[value]] = 'This field is required'
}
}
Powyższa funkcja sprawdza, czy w polu formularza są wprowadzone jakieś wartości. Jeśli nie generowany jest błąd.
Jeśli obiekt errors nie będzie pusty, tzn. będą obecne błędy, to nie nastąpi wykonanie funkcji zwrotnej.
Funkcja validate() dokonuje sprawdzania wprowadzonego tekstu do pola formularza podczas pisania, a także wyświetla odpowiednie komunikaty o błędach walidacji podczas pisania. Pobiera jako argumenty obiekt event oraz obiekt zawierający reguły walidacji.
Dla dowolnego pola formularza wywołana jest funkcja setValues(), która uzupełnia obiekt values o obiekt o kluczu pola formularza, do którego wprowadza się aktualnie tekst. Następnie wywoływane są poszczególne walidacje dla określonych reguł, tzn.
Jeśli wybrana jest reguła 'required’:true w formularzu to hook sprawdza czy ma do czynienia z tą metodą, oraz czy wartość wprowadzonego tekstu do pola formularza jest równa 0. W takim przypadku obiekt errors jest uzupełniany o kolejny element o kluczu będącym nazwą pola formularza.
// is required validation
if (rules.required === true && event.target.value.length === 0) {
setErrors(errors => ({...errors, [event.target.name]: 'This field is required'}))
}
Jeśli wybrana regułą to 'isEmail’:true w formularzu, to hook sprawdza czy wprowadzony tekst jest zgodny ze wzorcem tzn.
// is valid email address validation
else if (rules.isEmail === true && !pattern.test(event.target.value)) {
setErrors(errors => ({...errors, [event.target.name]: 'This email address is invalid'}))
}
Jeśli wybrana reguła to 'min’: liczba, to hook sprawdza czy wpisany tekst w polu formularza jest co najmniej o długości podanej jako wartość liczba np. 'min’: 6 zaakceptuje tekst o długości co najmniej 6 znaków. W przeciwnym przypadku nastąpi ustawienie odpowiedniego błędu. Do tekstu komunikatu błędu pobrana jest nazwa pola formularza tzn. tekst będzie się zmieniał w zależności jak będzie się nazywać zmienna.
// min value length validation
else if (rules.min && event.target.value.length < rules.min) {
setErrors(errors => ({...errors,
[event.target.name]:
[event.target.name.charAt(0).toUpperCase()]
+ [event.target.name.slice(1)]
+ ' is too short'}))
}
Jeśli wybrana reguła to match, to jako wartość dla klucza match podajemy nazwę pola formularza, z którym mają być porównywane wartości. Hook nie ma 'na stałe’ zapisanej nazwy porównywanego pola tzn.
// match validation
else if (rules.match && event.target.value!==values[rules.match]) {
setErrors(errors => ({...errors,
[event.target.name]: "Passwords don't match"}))
}
Jeśli żadne z powyższych warunków nie zostało spełnione, to wywoływany jest blok else:
We wpisie opiszę, jak uaktywnić komponent modal z frameworka Bulma, aby po wciśnięciu przycisku kasowania wiersza-kontraktu w tabeli kontraktów możliwe było potwierdzenie lub anulowanie usunięcia kontraktu.
Zmienna id przechowuje numer id wybranego kontraktu, natomiast zmienna modal przechowuje wartość boolean określającą stan okna modalnego (wyświetlony lub niewidoczny-domyślnie). Wywołanie funkcji setModal() zmienia wartość domyślną – false zmiennej modal na true.
Kod okna modalnego wyświetlającego potwierdzenie kasowania kontraktu wygląda następująco:
Funkcja setCursor pobiera wartości obiektu cursor i filtruje je w taki sposób, że odrzucany jest wybrany kontrakt. Następnie następuje wywołanie api we Flasku, oprócz id kontraktu przesyłany jest również token użytkownika. Wynik pomyślnego kasowania wiersz wyświetlany jest w konsoli.
Jak wygląda API we Flasku opisałem w poprzednim wpisie.
W ostatnim wierszu funkcji deleteContract() usuwam okno modalne.
W tabeli kontraktów po naciśnięciu odpowiedniego przycisku kasowania wiersza w tabeli zostaje aktywowana funkcja deleteContract, która jako parametr przyjmuje wartość id kontraktu tzn.:
Funkcja deleteContract filtruje dane zawarte w zmiennej cursor w taki sposób, że odrzucany jest wiersz, który chcemy usunąć. Następnie wywoływana jest zapytanie do backendu, gdzie w metodzie POST przesyłana jest wartość id kontraktu oraz token. Gdy usunięcie rekordu będzie zakończone sukcesem, to z API zostaje przesłana informacja w zmiennej message (w podanym przykładzie wartość tej zmiennej jest wyświetlana w konsoli).
Backend we Flask-RESTful z wykorzystaniem SQLAlchemy:
class DeleteContract(Resource):
def post(self):
req = request.json
contract_id = req['id']
token = req['token'].strip('"')
user = User.query.filter_by(token=token).first()
if user.role == 'Customer':
contract = Contract.query.filter_by(customer=user.username).filter_by(id=contract_id).first()
if contract:
contract.status ='cancelled'
db.session.commit()
return {"message": "Row deleted"}
Przesłane za pomocą metody POST wartości id i token przypisuję do odpowiednich zmiennych contract_id oraz token. Następnie znajduję użytkownika w bazie danych na podstawie przesłanego tokena. Jeśli użytkownik należy do grupy Customer, to dokonywane jest wyszukiwanie kontraktu na podstawie nazwy użytkownika i numeru id kontraktu. Jeśli kontrakt ten istnieje to zamiast go usuwać z bazy, zmieniam jego status na cancelled.
Uaktualniłem również metodę post() klasy AllContracts(Resource), aby utworzony obiekt kursora nie zawierał rekordów ze statusem cancelled tzn.:
class AllContracts(Resource):
def get(self, token):
token = token.strip('"')
user = User.query.filter_by(token=token).first()
if user.role == 'Customer':
contracts = Contract.query.filter(
Contract.status!='cancelled').
filter_by(customer=user.username).
order_by(Contract.id.desc()).all()
elif user.role == 'Contractor':
contracts = Contract.query.
filter(Contract.status!='cancelled').
filter_by(contractor=user.username).
order_by(Contract.id.desc()).all()
cursor = {}
for i, contract in enumerate(contracts):
cursor[i] = contract.serialize()
return cursor
Hook useEffect() do wczytania kontraktów po wyborze opcji Contracts z menu wygląda następująco:
Dane z backendu pobieram przy wybraniu z opcji menu komponentu Navbar frameworka Bulma. Aby dane były wczytane podczas randerowania komponentu używam hook useEffect. Tworzone jest wówczas zapytanie do backendu, a jako parametr przekazywany jest token, ustawiany podczas logowania użytkownika. Z backendu przesyłana jest odpowiedź w formie obiektu Promise, z którego jeśli zapytanie zostało zakończone sukcesem jest ustawiana zmienna cursor (zmiana stanu za pomocą setCursor). Cursor i funkcja setCursor przekazywane są do komponentu z komponentu nadrzędnego App.js jako parametry (props) tzn.
Aby komponent odświeżał dane po poprawnym zalogowaniu się w dodatkowym parametrze metody useEffect w tablicy umieściłem zmienną token, która zostaje ustawiona w komponencie LoginForm.js
Aby komponent odświeżał dane po zmianie zawartości cursora np. poprzez dodanie nowego kontraktu jako dodatkowy parametr metody useEffect umieściłem funkcję setCursor.
Poszczególne wiersze danych są umieszczane w tabeli tzn.
from api.resources.contracts import AllContracts
api.add_resource(AllContracts, '/api/contract/<token>', endpoint='all_contracts')
W pliku contracts.py definiuję klasę zasobów:
from datetime import datetime
from flask import request
from flask_restful import Resource
from .. import db
from ..common.models import User, Contract
class AllContracts(Resource):
def get(self, token):
token = token.strip('"')
user = User.query.filter_by(token=token).first()
contracts = Contract.query.filter_by(customer=user.username).order_by(Contract.id.desc()).all()
cursor = {}
for i, contract in enumerate(contracts):
cursor[i] = contract.serialize()
return cursor
W pliku models.py definiuję m.in. jak klasa Contract dziedzicząca po klasie Model z SQLAlchemy ma być serializowana:
from .. import db
from datetime import datetime
class Contract(db.Model):
'''Model of contract between a contractor and a customer'''
id = db.Column(db.Integer, primary_key=True)
status = db.Column(db.String(10), nullable=False, default='open')
contract_number = db.Column(db.String(20), nullable=False)
contractor = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
customer = db.Column(db.String(20), db.ForeignKey('user.username'), nullable=False)
date_of_order = db.Column(db.Date, nullable=False, default=datetime.utcnow)
date_of_delivery = db.Column(db.Date, nullable=False)
pallets_position = db.Column(db.Integer)
pallets_planned = db.Column(db.Integer, nullable=False)
pallets_actual = db.Column(db.Integer)
warehouse = db.Column(db.String(10), nullable=False)
def serialize(self):
return {'id': self.id,
'status': self.status,
'contract_number': self.contract_number,
'contractor': self.contractor,
'customer': self.customer,
'date_of_order': datetime.strftime(self.date_of_order, '%Y-%m-%d'),
'date_of_delivery': datetime.strftime(self.date_of_delivery, '%Y-%m-%d'),
'pallets_position': self.pallets_position,
'pallets_planned': self.pallets_planned,
'pallets_actual': self.pallets_actual,
'warehouse': self.warehouse
}