Przekazanie wartości do i z modala (framework Bulma) 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>

Przekazanie nazw kolumn tabeli do widoku jinja2 (Flask)

Najpierw utworzę model – używając sqlalchemy tworzę klasę dziedziczącą po klasie model. Atrybuty tej klasy odpowiadają nazwom kolumn tabeli bazy danych. Przykładowa deklaracja klasy Contract będzie wyglądać następująco:

class Contract(db.Model):

Następnie w kontrolerze przekazuję nazwy kolumn z tak utworzonego modelu do szablonu:

@bp.route('/contract', methods=['GET'])
@login_required
def contracts(page=1, per_page=5):
    columns = = [m.key for m in Contract.__table__.columns]
   # some other code here, not important for this example
    return render_template('contracts.html', title='Contracts', header=columns)

Dane z nagłówka przekazanego do szablonu można np. umieścić w nagłówku tabeli html:

<thead>
    <tr>
        {% for item in header %}
            <th class="is-info">
                {{ item }}
             </th>
        {% endfor %}
     </tr>
 </thead>

Klasy abstrakcyjne w Pythonie

Klasy abstrakcyjne są przeznaczone do wydzielenia funkcjonalności, która będzie następnie implementowana w klasach, które dziedziczą po klasie abstrakcyjnej.

Przypuśćmy, że mamy klasę Employee opisującą pracownika oraz klasę Manager, która jest klasą potomną względem klasy Employee. Chcemy utworzyć nową klasę Intern, która będzie opisywać osoby odbywające staż w firmie. Klasa Intern nie należy do wcześniej utworzonych klas Employee oraz Manager, jednak dzieli z nimi pewne atrybuty i metody.

W celu wydzielenia wspólnej funkcjonalności dla wszystkich powyższych klas tworzę klasę Person, która będzie zawierać metodę init() oraz metodę get_full_name(). Będą to metody abstrakcyjne. tzn.

import abc
class Person(abc.ABC):
    ''' Abstract Person class '''

    @abc.abstractmethod
    def __init__(self, first, last):
        self.first = first
        self.last = last

    @abc.abstractmethod
    def get_full_name(self):
        pass

Przykładowo klasa Employee będzie implementować powyższą klasę abstrakcyjną:

class Employee(Person):
    ''' Sample Employee class '''

    def __init__(self, first, last, salary):
        super().__init__(first, last)
        self.salary = salary

    def get_full_name(self):
        return f'{self.__class__.__name__}: {self.first} {self.last}'

Próba utworzenia instancji z klasy abstrakcyjnej Person zakończy się natomiast wyrzuceniem wyjątku TypeError.

Również nie zaimplementowanie wszystkich metod abstrakcyjnych w klasie potomnej powoduje wyjątek TypeError.

Colorama – zmiana kolorów w konsoli

Dzięki pakietowi Colorama można zmieniać kolor czcionki i tła w konsoli.

Pakiet instalujemy standardowo:

pip install colorama

I importujemy go w programie:

import colorama

albo konkretnie wymagane metody np.

from colorama import Fore, Back, Style

Na początku należy wywołać funkcję init(), podając ewentualne parametry do funkcji np. podanie parametru autoreset=True powoduje, że po każdym wyświetleniu tekstu z wybranym kolorowaniem zostają przywrócone wartości domyślne. Jeżeli jednak chcemy wyświetlić więcej tekstów sformatowanych przy użyciu wybranego stylu, to możemy nie podawać parametru autoreset=True, a zamiast tego, aby przywrócić wartości domyślne wyświetlania należy podać użyć:

print(Style.RESET_ALL)

Na przykład, aby wyświetlić kolorowanie linii w programie wyświetlającym szczegółowe dane o tankowaniach konkretnego pojazdu (wczytanego z najnowszego pliku *.xlsx z programu flotowego) można wpisać:

for key, value in cars_group['Ilość']:
    if choice.upper() in key:
        print(colorama.Fore.YELLOW + '>>>Tankowania dla pojazdu: ' + key)
        total = 0
        line = 0
        for i, v in zip(value.index, value.values):
            fuel_descr_str = f"{self.df.loc[i, 'Data']:%d-%m-%Y}"\
                 f" {v} ltr"\
                 f" (Cena NETTO: {self.df.loc[i, 'Cena netto']:.2f} PLN)"
            total +=v
            if (line % 2) == 1:
                print(colorama.Fore.BLACK +
                                    colorama.Back.WHITE +
                                    fuel_descr_str)
            else:
                print(fuel_descr_str)
            line +=1 
    print(f'>>>Suma tankowań dla {key}: {total} ltr')

Efekt końcowy przedstawia się następująco:

Chwila relaksu

Przedstawiam dwa rozwiązania do najciekawszych zadań ze wspaniałego kursu o programowaniu w języku Python z bloga FLYNERD, który szczerze polecam.

Zadanie 1
Napisz program, który dla 10 kolejnych liczb naturalnych wyświetli sumę poprzedników.
Oczekiwany wynik: 1, 3, 6, 10, 15, 21, 28, 36, 45, 55

Moje rozwiązanie:

[x+ sum(range(1, x)) for x in range(1, 11)]

Zadanie 5
Spróbuj wyświetlić choinkę z trójkątów w taki sposób, aby każdy poziom choinki był o 1 wiersz dłuższy:

#
##
#
##
###
#
##
###
####

Moje rozwiązanie:

list = [[a*'#' for a in range(1, 10)][0:x] for x in range(2, 5)]
for element in list:
    for item in element:
        print(item)

Narzędzie konsolowe dla Flaska

Moje narzędzie do tworzenia bazy danych, konta administratora i uruchamiania serwera.

kod pliku manage.py

from sys import argv
from getpass import getpass
from app_name import create_app, db, bcrypt
from app_name.config import Config, ConfigProduction, ConfigTesting
from app_name.auth.models import User

app = create_app(Config) 


def create_db():
    app.app_context().push()
    db.create_all()

def create_admin():
    app.app_context().push()
    username = input('Enter admin name: ')
    email = input('Enter admin email: ')
    password = getpass('Enter admin password: ')
    confirm_password = getpass('Retype admin password: ')
    # Simple validation here:
    if '' in (username, email, password, confirm_password): raise ValueError('Empty value occurred')
    if password != confirm_password: raise ValueError("Passwords don't match")
    if User.query.filter_by(username=username).first(): raise ValueError('User already exists in db')

    hashed = bcrypt.generate_password_hash(password).decode('utf-8')
    admin = User(username=username, email=email, password=hashed, admin=True, confirmed=True)
    db.session.add(admin)
    db.session.commit()


if __name__ == '__main__':
    if len(argv) == 2:
        _, command = argv
        if command == 'run': app.run()
        if command == 'create_db': create_db()
        if command == 'create_admin': create_admin()

    else:
        print('Invalid command.')

Dane z XML do DataFrame – minidom

W artykule opiszę jak wczytać dane z pliku XML do obiektu DataFrame przy użyciu modułu minidom – minimalnej implementacji modelu DOM.

Pliki projektu są do pobrania: >>tutaj<<

Używany jest identyczny plik XML, jaki opisałem we wcześniejszym wpisie.

Oprócz xml.dom.minidom używam jeszcze podobnie jak poprzednio moduł pandas oraz klasę defaultdict z modułu collections.

Przy użyciu menadżera kontekstu otwieram plik XML, który zostaje wczytany i z którego utworzony jest dokument DOM.

W utworzonym dokumencie wyszukuję listę elementów z pliku XML o nazwie person i po kolei wszystkie pobieram dla każdego z nich wartość atrybutu id, który zapisuję w słowniku persons. Ponadto dla każdego tagu potomnego tj. position, first_name, last_name itp. zapisuję jego wartość.

Tak utworzony słownik persons jest podawany jako argument przy tworzeniu obiektu ramki DataFrame.

kod źródłowy:

import xml.dom.minidom
from collections import defaultdict
import pandas as pd

persons = defaultdict(list)
with xml.dom.minidom.parse(open('persons.xml')) as tree:
    persons_list = tree.getElementsByTagName('person')
    for person in persons_list:
        persons['id'].append(person.getAttribute('id'))
        for tag in ('position', 'first_name', 'last_name', 'email', 'salary'):
            persons[tag].append(person.getElementsByTagName(tag)[0].firstChild.data)


df = pd.DataFrame(persons, columns=persons.keys()).set_index('id')
df['salary'] = df['salary'].astype(float)
print(df.sort_values(by='salary', ascending=False))

Dane z XML do DataFrame – sax

W artykule opiszę jak wczytać dane z pliku XML do obiektu DataFrame przy użyciu modułu xml.sax.

Pliki projektu są do pobrania: >>tutaj<<

Używany jest identyczny plik XML, jaki opisałem we wcześniejszym wpisie.

Oprócz xml.sax używam jeszcze podobnie jak poprzednio moduł pandas oraz klasę defaultdict z modułu collections.

Pierwszym etapem przy odczycie danych z pliku XML przy użyciu SAX jest implementacja własnej klasy, która dziedziczy po klasie ContentHandler(). Klasa handlera przesłania trzy metody bazowej klasy: metodę startElement(), która wywoływana jest przy rozpoczęciu odczytu kolejnego taga, metodę characters(), która odczytuje wartości zapisane dla poszczególnego elementu oraz metodę endElement(), która jest wywoływana po zakończeniu odczytu odpowiedniego tagu. Oprócz tego w metodzie __init__() tworzę egzemplarz słownika, w którym będą zapisywane dane odczytane z pliku XML.

W metodzie startElement() definiuję atrybut klasy o nazwie tag, a także jeśli przetwarzanym tagiem jest ‘person’ zapisuję numer id dla osoby.

Następnie w metodzie characters() zapisuję wartości przechowywane w poszczególnych elementach jako odpowiednie zmienne klasy.

W metodzie endElement() zapisuję pobrane wartości do słownika persons.

Parsowanie pliku XML za pomocą modułu xml.sax polega na wywołaniu metody make_parser(), która zwraca instancję parsera. Następnie do utworzonego parsera jest jako argument funkcji setContentHandler przypisywana jest instancja handlera – w tym przypadku klasy PersonsHandler.

Następnie wywoływana jest metoda parse(), która dokonuje parsowania źródłowego dokumentu XML.

Zmienna persons przechowuje wartości słownika utworzonego przez handlera i ten słownik jest podawany jako parametr przy tworzeniu ramki DataFrame.

kod źródłowy:

import xml.sax
from collections import defaultdict
import pandas as pd

class PersonsHandler(xml.sax.ContentHandler):
    def __init__(self):
        self.persons = defaultdict(list)
    def startElement(self, tag, attr):
        self.tag = tag
        if tag == 'person':
            self.persons['id'].append(attr['id'])
            
    def characters(self, content):
        if content.strip():
            if self.tag == 'position': self.position = content
            elif self.tag == 'first_name': self.first_name = content
            elif self.tag == 'last_name': self.last_name = content
            elif self.tag == 'email': self.email = content
            elif self.tag == 'salary': self.salary = content
    
    def endElement(self, tag):
        if tag == 'position': self.persons['position'].append(self.position)
        elif tag == 'first_name': self.persons['first_name'].append(self.first_name)
        elif tag == 'last_name': self.persons['last_name'].append(self.last_name)
        elif tag == 'email': self.persons['email'].append(self.email)
        elif tag == 'salary': self.persons['salary'].append(self.salary)

parser = xml.sax.make_parser()
parser.setContentHandler(PersonsHandler())
parser.parse(open('persons.xml'))
persons = parser.getContentHandler().persons


df = pd.DataFrame(persons, columns=persons.keys()).set_index('id')
df['salary'] = df['salary'].astype(float)
print(df.sort_values(by='salary', ascending=False))

Dane z XML do DataFrame – ElementTree

W artykule opiszę jak wczytać dane z pliku XML do obiektu DataFrame przy użyciu modułu xml.etree.ElementTree.

Pliki projektu są do pobrania: >>tutaj<<

Przykładowy plik XML będzie opisywał osoby zatrudnione w firmie i będzie miał następującą postać:

<persons>
    <person id="">
        <position></position>
        <first_name></first_name>
        <last_name></last_name>
        <email></email>
        <salary></salary>
    </person>
</persons>

Dla każdej osoby, która posiada unikalny atrybut id zapisane są następujące dane: stanowisko, imię, nazwisko, email i wynagrodzenie.

Na początek importuję niezbędne moduły tj. xml.etree.ElementTree do parsowania dokumentu XML. Z modułu collections importuję defaultdict, który będzie przechowywał listy zawierające imiona, nazwiska , wynagrodzenia itd., który to słownik podaję następnie jako argument klasy DataFrame.

import xml.etree.ElementTree as et
from collections import defaultdict
import pandas as pd

W kolejnym wierszu tworzę słownik, który będzie przechowywał dane o pracownikach pozyskane z pliku XML:

persons = defaultdict(list)

Następnie wczytuję plik XML i pobieram element nadrzędny – root – persons, a następnie dla każdego elementu podrzędnego – person pobieram wartość jego atrybutu id, a także wartości jego elementów: position, first_name, last_name itd. Każda z tych wartości jest dodawana jako kolejny element odpowiedniej listy słownika.

tree = et.parse("persons.xml")
root = tree.getroot()
for child in root:
    id = child.attrib.get('id')
    position = child.find('position').text
    first_name = child.find('first_name').text
    last_name = child.find('last_name').text
    email = child.find('email').text
    salary = child.find('salary').text
    
    persons['id'].append(id)
    persons['position'].append(position)
    persons['first_name'].append(first_name)
    persons['last_name'].append(last_name)
    persons['email'].append(email)
    persons['salary'].append(salary)

Tak utworzony słownik podaję jako argument do tworzonego obiektu ramki, przy czym jako nazwy kolumn podaję nazwy kluczy słownika, a jako indeks ramki podaję kolumnę id.

Następnie typ danych kolumny salary zmieniam na float, aby posortować ramkę względem malejących wartości z tej kolumny. Ewentualnie konwersji na float można by dokonać przy tworzeniu obiektu DataFrame podając dodatkowo argument dtype.

Raport tankowań z pliku xlsx do Pandas + obsługa zasobów (zlokalizowanych napisów)

Pliki źródłowe programu wraz z przykładowym raportem ze stacji paliwowej można znaleźć >>tutaj<<

Klasa Reports posiada trzy metody: __init__(), load_resources() oraz my_parser().

W metodzie __init__() odczytywane są wszystkie pliki z katalogu Data, które mają rozszerzenie xlsx. Ostatnio dodany plik z tej listy plików jest wczytywany – utworzony jest obiekt ramki pandas.

W metodzie load_resources() wczytuję zlokalizowane łańcuchy znaków w zależności od locale użytkownika. Jeśli brak jest odpowiedniego pliku, wczytywana jest wersja z komunikatami w języku angielskim.

W metodzie my_parser() dokonuję sumowania pobranego paliwa z podziałem na pojazdy. W razie rozbieżności z dokumentami źródłowymi można wyświetlić szczegółową listę tankowań dla konkretnego pojazdu.

kod źródłowy skryptu: main.py

import os
import glob
import locale
import calendar
import pandas as pd
import importlib_resources


class Reports:
	
    def __init__(self):
        self.load_resources()
        script_dir = os.path.dirname(os.path.abspath(__file__))
        data_dir = os.path.join(script_dir, 'Data')
        list_of_files = glob.glob(os.path.join(data_dir, '*.xlsx'))
        latest_file = max(list_of_files, key=os.path.getctime)
        path_to_file = os.path.join(data_dir, latest_file)
        self.df = pd.read_excel(path_to_file)
        self.my_parser()
    
    def load_resources(self):
        self.locale, encoding = locale.getdefaultlocale()
        r = importlib_resources.files('Resources')
        try:
            r_strings = (r / f'strings_{self.locale}.txt').read_text(encoding='utf-8').splitlines()
        except:
            r_strings = (r / f'strings_en_US.txt').read_text(encoding='utf-8').splitlines()
            self.locale = 'en_US'
        self.r_str = dict(x.split(':') for x in r_strings)


    
    def my_parser(self):
        os.system('cls' if os.name == 'nt' else 'clear')
        print()
        f_date = self.df['Date'].min()
        l_date = self.df['Date'].max()

        with calendar.different_locale(self.locale):
            print(self.r_str['title'], f'({f_date.day} -  {l_date.day} {calendar.month_abbr[int(l_date.month)]} {l_date.year})')
        self.df['Amount'] = self.df['Amount'].astype(str)
        self.df['Amount'] = self.df['Amount'].str.replace(',', '.')
        self.df['Amount'] = self.df['Amount'].astype(float)
        print()
        print(self.r_str['fuel_total'], self.df['Amount'].sum())
        cars_group = self.df.groupby('Registration number')
        print()
        # print(cars_group['Amount'].sum()) #standard output
        for item in zip(cars_group.groups, cars_group['Amount'].sum().values):
            name, value = item
            print(f'{name}    {value}')


        print()
        while True:
            choice = input(self.r_str['choice'] + ': ')
            os.system('cls' if os.name == 'nt' else 'clear')
            if choice == '/q': raise SystemExit
            if choice == '/m': self.my_parser()
            for key, value in cars_group['Amount']:
                if choice.upper() in key:
                    print(self.r_str['refueling'], key)
                    total = 0
                    for i, v in zip(value.index, value.values):
                        print(f"{self.df.loc[i, 'Date']}   {v}")
                        total +=v
                    print(self.r_str['ref_sum'] + f' {key}: {total}')
                    print()


if __name__ == "__main__":
    reports = Reports()