Bulma – modal komponent + React +Flask-RESTful

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.

<tbody>
  {cursor && Object.keys(cursor).map(
      (keyName, keyIndex) =>
      <tr  className="has-text-centered is-size-7"
           key={keyIndex}>
          <td>{keyIndex+1}</td>
          <td>{(cursor[keyName].status)}</td>
          <td>{(cursor[keyName].contract_number)}</td>
          <td>{(cursor[keyName].contractor)}</td>
          <td>{(cursor[keyName].customer)}</td>
          <td>{(cursor[keyName].date_of_order)}</td>
          <td>{(cursor[keyName].date_of_delivery)}</td>
          <td>{(cursor[keyName].pallets_position)}</td>
          <td>{(cursor[keyName].pallets_planned)}</td>
          <td>{(cursor[keyName].pallets_actual)}</td>
          <td>{(cursor[keyName].warehouse)}</td>
          <td>
            <FaEdit />
            <FaTrashAlt onClick={() => 
               showModal(cursor[keyName].id)}
               title={`Delete contract ${keyIndex+1}`} />
          </td>
      </tr>)}   
</tbody>
Deleting contract

Zamiast bezpośrednio wywołać funkcję deleteContract() zostaje wywołana funkcja showModal() pobierająca jako parametr id wybranego kontraktu.

Funkcja showModal() ustawia odpowiednio zmienne id oraz modal tzn.

const [modal, setModal] = useState(false)
const [id, setId] = useState()

const showModal = (id) => {
        setId(id)
        setModal(!modal)
    }

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:

<div class={`modal ${modal && 'is-active'}`}>
  <div class="modal-background"></div>
  <div class="modal-card column is-one-quarter">
     <header class="modal-card-head">
        <p class="modal-card-title has-text-danger">
           Delete Contract?
        </p>
      </header>
      <section class="modal-card-foot">
         <button class="button is-danger" 
             onClick={() => deleteContract(id)}>Delete
         </button>
         <button class="button"
             onClick={() => setModal(!modal)}>Cancel
         </button>
       </section>
  </div>
</div>

Wybranie przycisku Cancel powoduje, że okno modalne staje się niewidoczne. Wybranie przycisku Delete aktywuje właściwą funkcję kasowania wiersza tzn.

const deleteContract = (id) => {
  setCursor(Object.values(cursor)
    .filter((row) => row.id !== id))
  fetch('/api/contract/delete', {
    method: 'POST',
    headers: {
      'Content-type': 'application/json',
     },
       body: JSON.stringify({'id': id, 'token': token})
  })
  .then(res => res.json())
  .then(data => console.log(data))
  .catch((err) => console.log(err));
  setModal(!modal)
}

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.

Kasowanie rekordu (Flask-RESTful, SQLAlchemy, React i Bulma CSS framework)

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.:

<td>
    <FaTrashAlt onClick={() => deleteContract(cursor[keyName].id)} />
</td>

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).

const deleteContract = (id) => {
    setCursor(Object.values(cursor).filter((row) => row.id !== id))
    fetch('/api/contract/delete', {
            method: 'POST',
            headers: {
                'Content-type': 'application/json',
            },
            body: JSON.stringify({'id': id, 'token': token})
        })
        .then(res => res.json()).then(data => console.log(data)).catch((err) => console.log(err));
    }

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:

    useEffect( 
        () => {
            const token = sessionStorage.getItem('token')
            fetch('/api/contract/' + token)
            .then(res => res.json()).then(data => setCursor(data)).catch((err) => console.log(err))
        }, [token, setCursor],
    );

React, Flask-restful i SQLAlchemy – pobranie danych z bazy danych

Frontend:

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.

import LoginForm from '../Forms/LoginForm/LoginForm'
import { useEffect } from "react";

function Contracts({token, putToken, cursor, setCursor}) {

    useEffect( 
        () => {
            const token = sessionStorage.getItem('token')
            fetch('/api/contract/' + token)
            .then(res => res.json()).then(data => setCursor(data)).catch((err) => console.log(err))
        }, [token, setCursor],
    );
    
    if (!token) {
        return <LoginForm putToken={putToken} />
    };

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.

<tbody>
    {cursor && Object.keys(cursor).map((keyName, keyIndex) => 
    <tr key={keyIndex}>
            <td>{keyIndex+1}</td>
            <td>{(cursor[keyName].status)}</td>
            <td>{(cursor[keyName].contract_number)}</td>
            <td>{(cursor[keyName].contractor)}</td>
            <td>{(cursor[keyName].customer)}</td>
            <td>{(cursor[keyName].date_of_order)}</td>
            <td>{(cursor[keyName].date_of_delivery)}</td>
            <td>{(cursor[keyName].pallets_position)}</td>
            <td>{(cursor[keyName].pallets_planned)}</td>
            <td>{(cursor[keyName].pallets_actual)}</td>
            <td>{(cursor[keyName].warehouse)}</td>
     </tr>)}   
</tbody>

Backend we Flask-restful:

W pliku __init__.py dodaję kolejną klasę zasobów:

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 
                }

React i Bulma navbar

Rozwiązane problemy:

  1. Jak zwinąć burger menu przy wyborze opcji z w/w menu. (domyślnie wybór opcji z burger menu pozostawia rozwinięte menu)
  2. Jak zwinąć menu rozwijalne przy wyborze którejś z opcji. (domyślnie menu rozwijalne nie zamyka się, gdy wybrano którąś z opcji)

ad. 1

Menu nareszcie zwija się po wybraniu opcji
<div id="navbarBasicExample" className={`navbar-menu ${burgerActive? "is-active": ""}`}>
                <div class="navbar-start">
                <Link to="/" class="navbar-item" onClick={handleOnClick}>
                    Home
                </Link>

przy czym:

    const [burgerActive, setBurgerActive] = useState(false)

    const handleOnClick = () => {
        setBurgerActive(false)
    }

ad2.

Menu rozwijalne zwija się, gdy wybrano którąś z opcji.
<div class="navbar-item has-dropdown is-hoverable" key={location.pathname}>

przy czym:

import {
    Link, useLocation
  } from 'react-router-dom' 

const Navbar = () => {
    let location = useLocation();

Obsługa zamówień dla Centrów logistycznych

Aplikacja umożliwia wystawianie nowych zamówień przez Odbiorcę oraz akceptację zamówień przez Dostawcę i generowanie rezerwacji tzn. zarządzanie oknami czasowymi z myślą o dostawach do magazynów centralnych i centrów logistycznych.

Kod źródłowy aplikacji do pobrania – tutaj.

Aplikację można wypróbować tutaj.

W tym wpisie przedstawię funkcję new_booking(id), która obsługuje rezerwację terminu dla konkretnego zamówienia. Jako parametr przyjmuje ona numer id zamówienia tzn.

@bp.route('/booking/<int:id>', methods=['GET', 'POST'])
@login_required
def new_booking(id):

Następnie w funkcji new_booking() tworzę formularz do rezerwacji oraz pobieram obiekt rezerwacji dla danego zamówienia tzn.

    form = BookingForm()
    result = Booking.query.filter_by(contract_id=id).first()

Dalej ustalam obiekt zamówienia dla rezerwacji tzn.

    current_contract = Contract.query.get(id)     # Returns contract for current booking

W kolejnych wierszach uzyskuję listę zamówień, które były wygenerowane dla konkretnego magazynu na dzień dostawy tzn.

# Filtering by one column gives a list of tuple(s) so I converted it to a list of values
    contracts = [ids[0] for ids in Contract.query.with_entities(Contract.id).filter_by(
        date_of_delivery=current_contract.date_of_delivery).filter_by(
        warehouse=current_contract.warehouse).all()]

Następnie przeprowadzam walidację, jeśli formularz został przesłany. W zależności od tego, czy rezerwacja dla danego zamówienia jest już utworzona to uaktualniam dane w bazie danych na podstawie danych z formularza. Jeśli obiekt rezerwacji nie istnieje to tworzę nowy obiekt rezerwacji i zapisuję go do bazy danych tzn.

    if form.validate_on_submit():
        if result:
            result.booking_time = form.booking_time.data
            result.driver_full_name = form.driver_full_name.data
            result.driver_phone_number = form.driver_phone_number.data
            result.truck_reg_number = form.truck_reg_number.data
            db.session.commit()
        else:
            booking = Booking(booking_time=form.booking_time.data, 
                            contract_id = id,
                            driver_full_name=form.driver_full_name.data,
                            driver_phone_number=form.driver_phone_number.data,
                            truck_reg_number=form.truck_reg_number.data)
            db.session.add(booking)
            db.session.commit()

W dalszej kolejności zmieniam status zamówienia na zaakceptowane i przekierowuję do funkcji wyświetlającej wszystkie zamówienia dla danego kontrahenta tzn.

        contract = Contract.query.get(id)
        contract.status = 'accepted'
        db.session.commit()
        page = session.get('page')
        per_page = session.get('per_page')
        return redirect(url_for('contracts.contracts', page=page, per_page=per_page))

Jeśli strona zostaje wczytana za pomocą metody GET to w zależności czy rezerwacja jest już obecna w systemie, czy dopiero jest tworzona to uzupełniane są dane w formularzu tzn. jeśli rezerwacja jest dostępna i otwieramy ją np. w celu edycji danych to formularz zostanie uzupełniony o poprzednio wpisane dane. Natomiast jeśli rezerwacja jest dopiero tworzona to formularz będzie zawierał modyfikację, których terminów nie można wybrać, bo już zostały wybrane przez innych dostawców tzn.

    if result is not None:
        reserved_booking_time = [times[0] for times in 
            Booking.query.with_entities(Booking.booking_time).filter(
            Booking.contract_id.in_(contracts)).all() if times[0]!=result.booking_time]   
        form.booking_time.data = result.booking_time
        form.driver_full_name.data = result.driver_full_name
        form.driver_phone_number.data = result.driver_phone_number
        form.truck_reg_number.data = result.truck_reg_number   
    else:
        reserved_booking_time = [times[0] for times in 
            Booking.query.with_entities(Booking.booking_time).filter(
            Booking.contract_id.in_(contracts)).all()] 
    return render_template('booking.html', form=form, reserved_booking_time=reserved_booking_time)

Kompletny kod funkcji new_booking() jest dostępny tutaj.

Tak zaakceptowane zamówienie może być edytowane tylko przez wystawiającego zamówienie, przy czym jeśli zamówienie zostało już zaakceptowane i została dokonana rezerwacja, to zmiana zamówienia zmienia status zamówienia z zaakceptowanego z powrotem na otwarty i wymaga powtórnego zaakceptowania zmian przez dostawcę (ale wpisane dane są obecne i rezerwacja nie wymaga powtórnego uzupełnienia danych).

Dostawca może również pobrać dokument pdf rezerwacji jeśli zamówienie jest zaakceptowane (dla otwartych zamówień opcja pobrania pdf-a jest nieaktywna).

Rezerwacja dokonana – można teraz wygenerować pdf-a

Reportlab – wygenerowanie i pobranie pdf-a we Flasku bez tworzenia pliku na serwerze

Rozwiązany problem: jak wygenerować pdf z danymi zamówienia i kodem paskowym rezerwacji bez zapisu pliku na dysku, który może być następnie pobrany przez użytkownika serwisu we Flasku.

Do osadzenia elementów dokumentu takich jak teksty, tabele itp. możemy użyć obiekt Canvas. Jako argument możemy podać nazwę, jaką będzie posiadał utworzony pdf, ale w tym przypadku zamiast tworzyć plik na serwerze użyję bufora. Jawnie podaję rozmiar strony jako A4.

from io import BytesIO
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.pagesizes import A4

buffer = BytesIO()
canvas = Canvas(buffer, pagesize=A4)

Aby użyć niestandardowych znaków np. polskich liter z ogonkami należy zarejestrować odpowiednie czcionki, które obsługują wymagane znaki. W przykładowym pliku pdf użyłem czcionki Vera.

from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont

pdfmetrics.registerFont(TTFont('Vera', 'Vera.ttf'))

Tak zarejestrowaną czcionkę można już użyć:

canvas.setFont("Vera", size=10)

Obrazy wygenerowane na podstawie danych z aplikacji (w tym przypadku kod kreskowy rezerwacji) umieszczam w tworzonym dokumencie za pomocą metody drawImage(), która jako pierwszy argument pobiera obiekt ImageReader tzn.

im = ImageReader(image)
canvas.drawImage(im, x=0, y=-5*cm, width=150, height=100)

Listę zawierającą łańcuchy tekstowe można dodać do dokumentu wykorzystując obiekt tekstowy tzn.

txt_obj = canvas.beginText(14, -6.5 * cm)
txt_lst = ["line of text 1", "line of text 2", "line of text 3"]
for line in txt_lst:
        txt_obj.textOut(line)
        txt_obj.moveCursor(0, 16)
canvas.drawText(txt_obj)

Tabelę do dokumentu można dodać za pomocą obiektu klasy Table tzn. osobno definiuję listę zawierającą dane poszczególnych wierszy tabeli (zmienna table_data). Osobno również definiuję style obowiązujące w całej lub w części tabeli.

t = Table(table_data, colWidths=[60, 230, 70, 60, 50], rowHeights=30)
style = [('BACKGROUND',(0,0),(-1,-2),colors.lightblue),
            ('ALIGN',(0,-1),(-1,-1),'CENTER'),
            ('BOX', (0,0), (-1,-1), 0.25, colors.black),
            ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black),
            ('FONTSIZE', (0,0), (-1,-1), 10),
            ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
            ('VALIGN', (0, 0), (-1, -1), 'MIDDLE')]
t.setStyle(tblstyle=style)
t.wrapOn(canvas, 10, 10)
    t.drawOn(canvas=canvas, x=20, y=-22*cm)

Następnie zapisuję tak utworzony obiekt Canvas i zwracam powstały bufor do kontrolera. W celu przesłania pliku pdf na podstawie danych z bufora wykorzystuję funkcję send_file() Flaska:

from flask.helpers import send_file

@bp.route('/get-pdf/<int:id>', methods=['GET'])
@login_required
def get_pdf(id):
    contract = Contract.query.get(id)
    contractor = User.query.get(contract.contractor_id)
    booking = Booking.query.filter_by(contract_id=contract.id).first()
    pdf = create_pdf(booking_no=booking.id,
                        contractor=contractor.username,
                        contractor_no=contractor.id,
                        truck_plate=booking.truck_reg_number,
                        warehouse=contract.warehouse,
                        date=contract.date_of_delivery,
                        time=booking.booking_time,
                        pallets_pos=contract.pallets_position,
                        pallets=contract.pallets_actual)
    pdf.seek(0)
    return send_file(pdf, as_attachment=True, mimetype='application/pdf',
        attachment_filename='booking.pdf', cache_timeout=0)
przykładowy plik wygenerowany po stronie serwera, pobrany przez zleceniobiorcę.

System zarządzania oknami czasowymi we Flasku #2

Kod źródłowy aplikacji do pobrania – tutaj.

Aplikację można wypróbować tutaj.

Plan aplikacji opisanej we wcześniejszym artykule wygląda następująco:

├───flask_contracts
│ ├───api
│ ├───auth
│ │ ├───static
│ │ │ └───logos
│ │ ├───templates
│ │ │ └───auth
│ │ ├───utils
│ │ │ ├───templates
│ │ │ │ └───utils
│ ├───contracts
│ │ ├───static
│ │ ├───templates
│ │ │ └───contracts
│ │ │ └───errors
│ │ ├───utils
│ ├───static
│ ├───templates
├───tests
│ ├───functional
│ ├───unit
└───venv

Każda część aplikacji jest osobną całością. Podobne rozwiązanie jest zastosowane w Django. Do zarządzania aplikacją napisałem plik manage.py. Każda część składowa posiada własne podkatalogi szablonów – konwencja podobna do obecnej w Django.

System zarządzania oknami czasowymi za pomocą Flask

Dzięki aplikacji: minimalizuje się przestoje oraz optymalizuje się pracę magazynu, co wpłynie na redukcję kosztów i brak przestojów.

Aplikacja umożliwia zarządzanie oknami czasowymi z myślą o dostawach do magazynów centralnych i centrów logistycznych.

Kod źródłowy aplikacji do pobrania – tutaj.

Aplikację można wypróbować tutaj.

Projekt będzie zawierał aplikację we Flasku (z użyciem frameworka css – Bulma).

Do aplikacji będą mogli się logować dostawcy i odbiorcy dostaw (weryfikacja konta mailem na podany adres) .

Zlecenia będą mogli wystawiać tylko odbiorcy dostaw, natomiast dostawcy będą mogli samodzielnie rezerwować dostępne okna czasowe i wygenerować dokument dostawy z kodem paskowym jako plik pdf.

część 2

Przekazanie wartości do i z modala (framework Bulma) we Flasku

Rozwiązany problem: jak przekazać wartość do okna modalnego oraz jak przekazać wartość z okna modalnego do aplikacji we Flasku.

W przedstawianym przypadku każdy wiersz tabeli html zawiera przycisk umożliwiający anulowanie kontraktu. Dane dla poszczególnych wierszy są przekazywane z kontrolera za pomocą zmiennej contracts:

result = Contract.query.filter_by(
            customer_id=current_user.id).filter(
                Contract.status!='cancelled').order_by(
                    Contract.id.desc()).paginate(page=page, per_page=per_page)
return render_template('contracts.html', title='Contracts', header=columns, contracts=result)

Dane dla wierszy są dostępne w szablonie:

{% for contract in contracts.items %}
    <tr>
        <td>{{  loop.index  }}</td>
        {% for item in header %}
            <td>
                 {{ contract[item] }}
            </td>
        {% endfor %}
    </tr>
{% endfor %}

Oprócz danych przekazanych z kontrolera należy dla każdego wiersza tabeli utworzyć przycisk uruchamiający odpowiedni modal – różniący się wartością parametru data-target tzn.

<button class="button mr-2 modal-button" data-target="modal{{contract.id}}"  title="Cancel Contract">

Wartość parametru data-target będzie się różnić dla każdego wiersza w zależności od wartości contract.id , która to wartość jest unikatowa – klucz tabeli Contract.

W zależności od wartości parametru data-target uruchamiany jest modal o takiej samej wartości parametru id, tzn.

<div id="modal{{contract.id}}" class="modal">

Modal zawiera przycisk do zatwierdzenia zmian, w tym przypadku do anulowania kontraktu. Aby przekazać, jaki kontrakt ma zostać anulowany należy podać id kontraktu jako parametr do funkcji url_for() , tzn.

<form action="{{ url_for('contracts.cancel_contract', id=contract.id) }}">
            <button class="button is-danger is-rounded" formmethod="POST">Yes</button>
</form>