Dane z XML do DataFrame #3

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 #2

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 #1

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

Krótki skrypt do obróbki danych z pliku Excela

W bieżącym wpisie opiszę skrypt, który wczytuje dane z listy tankowań w nowszym formacie MS Excel – xlsx.

Skrypt sumuje zakupione paliwo w danym okresie, następnie wyświetla, jaką ilość paliwa zatankowano do poszczególnych pojazdów. W razie rozbieżności z danymi źródłowymi – kartami kierowcy, można wyświetlić szczegółową listę tankowań dla konkretnego pojazdu.

Katalog ze skryptem zawiera również podkatalog “Dane”, w którym umieszczam pliki kolejnych list tankowań.

W pliku Excela komórki pierwszego wiersza definiują nazwy poszczególnych kolumn z danymi m.in. “Ilość” – określa ile paliwa zatankowano podczas jednego tankowania, “Data” – określa datę tankowania, “Numer rejestracyjny” – zawiera numer rejestracyjny pojazdu.

Skrypt zawiera jedną klasę Reports, która zawiera dwie metody: __init__() oraz parser().

W metodzie __init__() określam kolejno katalog, w którym jest umieszczony skrypt main.py i wartość tą przypisuję do zmiennej script_dir. Następnie definiuję zmienną data_dir, która zawiera ścieżkę do podkatalogu “Dane”, który zawiera pliki *.xlsx z danymi. Z plików tych jest sporządzana lista plików, z której jest wybierany najnowszy plik. Dane z najnowszego pliku są wczytywane jako obiekt ramki pandas, który to jest przypisane jako atrybut klasy – zmienna self.df, dzięki czemu obiekt ten będzie dostępny także dla innych metod klasy, gdy metoda __init__() zakończy swoje działanie. Następnie jest wywoływana metoda parser().

W metodzie parser() na początku czyszczę terminal z poprzednich wpisów:

os.system('cls' if os.name == 'nt' else 'clear')

W zależności od systemu, na którym jest uruchamiany skrypt wykonywana jest komenda cls – dla Windows lub clear dla Linux lub MacOS.

Następnie definiuję dwie zmienne: f_date oraz l_date, które przechowują daty odpowiednio pierwszego i ostatniego tankowania na liście tankowań. W kolejnym wierszu wyświetlam nagłówek zestawienia, a następnie zamieniam znak przecinka z danych z kolumny “Ilość” – separatora części dziesiętnej na znak kropki.

Całe pobrane paliwo na wszystkie samochody obliczam za pomocą:

self.df['Ilość'].sum()

W nieskończonej pętli while pobieram tekst zawierający dowolny fragment numeru rejestracyjnego lub “/q” – wyjście za pomocą wywołania wyjątku SystemExit lub “/m” – powrót do głównego zestawienia za pomocą wywołania metody self.parser().

W pętli – for key, value in cars_group[‘Ilość’]: w kolejnych iteracjach sprawdzane jest, czy wybrany fragment tekstu wpisanego przez użytkownika (po zmianie na wielkie litery) jest obecny w numerze rejestracyjnym pojazdu. Jeśli powyższy warunek jest spełniony to zostają wyświetlone wszystkie daty i ilości tankowań. Oprócz wszystkich tankowań dla wybranego pojazdu, wyświetlane jest także podsumowanie zatankowanych litrów dla pojazdu.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
@ author: Sławomir Kwiatkowski
@ date: 2019.10.11
'''

import os
import glob
import pandas as pd


class Reports:
	
    def __init__(self):
        script_dir = os.path.dirname(os.path.abspath(__file__))
        data_dir = os.path.join(script_dir, 'Dane')
        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.parser()

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

        print('>>>Zestawienie paliwa za okres od {} do {}.{}.{}<<<'.format(f_date.day, 
        l_date.day, l_date.month, l_date.year))
        self.df['Ilość'] = self.df['Ilość'].astype(str)
        self.df['Ilość'] = self.df['Ilość'].str.replace(',', '.')
        self.df['Ilość'] = self.df['Ilość'].astype(float)
        print()
        print('>>>Zakup paliwa: ', self.df['Ilość'].sum())
        cars_group = self.df.groupby('Numer rejestracyjny')
        print()
        print(cars_group['Ilość'].sum())
        print()
        while True:
            choice = input('Podaj nr rej. pojazdu (lub /q-wyjście, /m-ekran główny):  ')
            os.system('cls' if os.name == 'nt' else 'clear')
            if choice == '/q': raise SystemExit
            if choice == '/m': self.parser()
            for key, value in cars_group['Ilość']:
                if choice.upper() in key:
                    print('>>>Tankowania dla pojazdu: ', key)
                    total = 0
                    for i, v in zip(value.index, value.values):
                        print(self.df.loc[i, 'Data'], v)
                        total +=v
                    print(f'>>>Suma tankowań dla {key}: {total} ltr')
                    print()


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

Obiekt ramki w Pandas na podstawie danych z pliku pdf

Wczytanie danych z pliku pdf wymaga zainstalowanego modułu tabula-py. Moduł ten umożliwia także zapis wczytanych danych do pliku z danymi w formacie csv lub json.

import tabula
df_list = tabula.read_pdf('file.pdf')

Funkcja read_pdf wczytuje domyślnie jedną stronę z pliku pdf, jeśli nie podano wartości dla parametru pages (jeśli chcemy wczytać wszystkie strony to należy wpisać parametr pages=’all’).

Powyższa funkcja zwraca obiekt listy zawierającej kolejne obiekty typu DataFrame, na przykład:

df = df_list[0]        # pierwszy obiekt ramki

Wczytanie danych z pliku csv do numpy.ndarray

Gdy w źródłowych danych nie ma brakujących wartości to możemy użyć funkcję numpy.loadtxt().

Jeśli jednak we wczytywanym pliku brak jest jakiejś wartości to zamiast powyższej funkcji możemy użyć funkcję numpy.genfromtxt() tzn.

import os
import numpy as np

script_dir = os.path.dirname(__file__)
path_to_file = os.path.join(script_dir, 'data_file.csv')

data_array = np.genfromtxt(path_to_file, dtype='str')

Funkcja genfromtxt() zwraca obiekt typu numpy.ndarray. Jako dodatkowe parametry funkcji możemy dodać np.

  • delimiter - określa jaki znak oddziela poszczególne wartości
  • skip_header - określa ile linii od początku pliku ma zostać pominiętych
  • autostrip - parametr typu bool określający czy spacje mają być automatycznie usuwane

Listę wszystkich parametrów można znaleźć tutaj.

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 —

Matplotlib – osadzanie wykresu w aplikacji #2

Drobne zmiany w kodzie.

Wykresy zapisywane są na lokalnym dysku, w katalogu data/ projektu. Jeśli na dysku jest już zapisany wykres dla danego roku, to jest on wczytywany z lokalnych zasobów, bez tworzenia zapytań do serwera, w celu przyspieszenia wyświetlania wykresu.

Alternatywnie można byłoby pobrać wszystkie dane za jednym razem asynchronicznie np. korzystając z obiektu ThreadPoolExecutor() i następnie odwoływać się już tylko do lokalnych zasobów.