Ściąga z PYTHONga

  1. Część 1: Zapoznanie się z językiem Python
  2. Część 2: Aplikacje internetowe
 _                      _             _ _
| |_ ___ _ __ _ __ ___ (_)_ __   __ _| | | ___   __ _
| __/ _ \ '__| '_ ` _ \| | '_ \ / _` | | |/ _ \ / _` |
| ||  __/ |  | | | | | | | | | | (_| | | | (_) | (_| |
 \__\___|_|  |_| |_| |_|_|_| |_|\__,_|_|_|\___/ \__, |
			                        |___/

Część 1: Zapoznanie się z językiem Python

1.1. Wstęp.

Informacje zawarte tutaj odnoszą się do wersji Pythona powyżej 3.6. Python powinien być zainstalowany domyślnie we wiodących dystrybucjach systemu Linux. Jeśli nie ma w ogóle Pythona w wersji >= 3. To warto sprawdzić czy istnieją pakiety w repozytorium dystrybucji z wersją powyżej 3.6. Jeśli tak to instalujemy te pakiety. Jeśli nie, to wtedy należałoby pobrać kod źródłowy najnowszego Pythona i go skompilować. Zazwyczaj nie kompiluje, ale w przypadku kiedy użyłem dystrybucji BunsenLabs Helium, to pre-instalowanego Pythona miałem w wersji 3.5. Za mało na najnowszy Crash Course. Kompilacje wykonujemy w następujących krokach:

  1. Instalujemy odpowiednie dla naszej dystrybucji build-essential oraz dwa pakiety, których nieobecność została zauważona przy wykonywaniu polecenia make altinstall i zwrócona jako błąd, mianowicie chodzi o zlib1g oraz zlib1g-dev.
  2. Pobieramy najnowszą wersję kodu z https://www.python.org/ftp/python/3.8.3/Python-3.8.3.tar.xz.
  3. Rozpakowujemy: tar -xvf Python-3.8.3.tar.xz
  4. Przechodzimy do katalogu: cd Python-3.8.3.tar.xz
  5. Ja w swoim systemie zachowałem obecną wersję Pythona, wskazując skryptowi konfiguracyjnemu opcje prefix wskazującą na katalog utworzony w moim katalogu domowym: ./configure --prefix="/home/xf0r3m/bin/python3.8". Użycie opcji prefix, przekazuje skryptowi, aby przygotował środowisko do kompilacji względem podanej ścieżki. Jeśli nie zostanie zwrócony żaden błąd podczas pracy skryptu, zostanie nam wygenerowany tzw. plik makefile.
  6. Wydajemy polecenie kompilacji kodu: make. Po poprawnie zakończonej kompilacji, to znaczy po wyświetlonych przez polecenie make następnych kompilacjach, zostanie nam zwrócony prompt, możemy przejść do ostatniej części kompilacji czyli instalacji pakietu w systemie. Ze względu na to iż zostawiłem pre-instalowaną wersję 3.5, wydaje polecenie 'make altinstall po wykonaniu tego polecenia nasz Python 3.8 znajduje się w podkatalogu bin na ścieżce, którą podaliśmy w opcji prefix.
  7. Ostatni punkt jest dla wygodnych. W pliku .bashrc możemy sobie zdefiniować alias python dla naszego nowo zainstalowanego Pythona. Po uruchomieniu polecenia python powiśmy zobaczyć prompt powłoki wraz z informacjami odnośnie wersji.
Python 3.8.2 (default, Apr 27 2020, 15:53:34)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Poniżej znajdują się zagadnienia z książki które warto mieć przy sobie przy pierwszych projektach tworzonych w tym języku. Kolejność jest identyczna z kolejnością występowania w książce.

1.2. Witaj w świecie Pythona.

print() - instrukcja wyświetlająca wszystko co umieścimy pomiędzy nawiasami.

Konwencja nadawania wartości zmiennym w Pythonie wygląda w ten sposób:

greetinsg_message = "Hello, World!"

Warto pamiętać o spacjach pomiędzy operatorem. Podobnie zresztą wygląda konwencja stosowania jakichkolwiek operatorów.

1.2.1. Ciągi tekstowe w Pythonie

Ciągi tekstowe f - formatowane ciągi tekstowe. Litera f pochodzi od słowa format, ich głównym zadaniem jest zamiana nawiasu klamrowego wraz z nazwą zmiennej na jej wartość w ciągu. Taki ciąg tworzy się w następujący sposób:

imie = 'xf0r3m'
print(f"Witaj {imie}! Czy chcesz zanurzyć się w świat Pythona?")

Czy w innej zmiennej czy poleceniu print() zapisanie litery f przed znakami cudzysłowu lub apostrofu, spowoduje że dany ciąg będzie ciągiem f.

Uwaga! Ciągi f są dostępne dopiero od wersji 3.6.

Formatowanie ciągu przed wersją Pythona 3.6. Takie formatowanie polega na użyciu wbudowanej metody format(). Przykład poniżej.

imie = 'xf0r3m'
print("Witaj {}! Czy chcesz zanurzyć się w świat Pythona?".format(imie))

Dodatkowa zmienna do wyświetlenia, to dodatkowa para nawiasów i kolejna zmienna jako argument metody format.

Do obróbki ciągów tekstowych, a konkretnie ich normalizacji mogą służyć nam poniższe metody:

Warto zaznaczyć że te metody nie zmieniają wartości ciągu, o ile nie przypiszemy nie ich wartości zwrotnych.

Białe znaki w ciągach tekstowych. Białe znaki w naszych ciągach uzyskuje się w bardzo prosty sposób, po prostu po przez umieszczenie w ciągu:

imie = 'xf0r3m'
print(f"Witaj \t{imie}!\nCzy chcesz zanurzyć się w świat Pythona?")

Usuwanie białych znaków. Może zdarzyć się taka sytuacja, że weźmie się skądś dane, i nagle gdzieś nasz algorytm nie działa, bo ma nieprawidłowe dane. Gdzieś na początku albo na końcu ciągu znajdują się nadmiarowe białe znaki np. skopiowaliśmy jakiś ciąg ze spacją na końcu. To zresztą zdarza się dość często. W tym przypadku korzystamy z metody strip() jeśli nie potrafimy jasno określić czy z lewej lub prawej strony ciągu znajdują się nadmiarowe białe znaki. Dla znaków po lewej stronie możemy użyć lstrip() a dla znaków po prawej rstrip(). Warto mieć na uwadze, że te metody nie dokonują w zmiennej żadnych zmian, aby zapisać zmiany musimy nadać zmiennej wartość zwracaną przez tą metodę.

dirty_strings='  python  '
washed_strings=dirty_strings.strip()

1.2.2. Więcej o zmiennych

Wielkie liczby możemy zapisywać za pomocą grup rozdzielonych przy pomocy znaku podkreślenia (_), na przykład:

one_bilion = 1_000_000_000

Z poziomu Pythona nie ma znaczenia, czy jest 1000000000 czy 1_000_000_000. Podobnie jest z mniejszymi liczbami, takimi jak 1000 (1_000). Uwaga! Ta funkcjonalność dostępna jest od Pythona 3.6 w górę.

Wiele przypisań wartość możemy skrócić sobie do praktycznego, aczkolwiek nie zawsze czytelnego zapisu. Na przykład:

x, y, z = 0, 0, 0
a, b, c = 3, 4, 5

W Pythonie nie ma stałych, jednak wśród programistów nie tylko Pythona przyjęło się, że każdą zmienną, której nazwa (identyfikator) będzie zapisany wielkimi literami traktuje się jako stałą.

UNIX_EPOCH = "01-01-1970"

1.2.3.Komentarze

Komentarze w Pythonie zaczynają się od krzyżyka (#). Wszystko co zostanie umieszczone po tym znaku zostanie zignorowane w przez interpreter Pythona.

NIGGA_AM = 300
#BANG!

1.2.4. Kilkanaście zasad.

Zen Pythona - to kilka zasad stworzonych przez społeczność Pythona. Cenne uwagi, które warto wziąć sobie do serca programując nie tylko w Pythonie. Warto sobie zrobić z tego swoisty kodeks programisty - 18 zasad.

>>> import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!

1.3. Listy

Listy - bardzo podobne do tablic. Do poszczególnych elementów uzyskujemy dostęp poprzez podanie indeksu w nawiasie kwadratowym:

cars = ['audi', 'bmw', 'renault', 'honda']
print(cars[2])

Dostęp do końcowych elementów listy następuje poprzez podanie ujemnego indeksu, począwszy od -1 wskazujący ostatni element listy, kolejno -2 to przedostatni itd.

Nadając zmiennej parę nawiasów kwadaratowych możemy utworzyć pustą listę, do której wrazie potrzemy będziemy mogli dodać elementy.

cars=[]

1.3.1. Dodawanie elementów do listy

Dodawanie elementów na końcu listy możemy zrealizować za pomocą wbudowanej metody append(). Jak argument metoda przyjmuje wartość dodawanego elementu.

cars = ['audi', 'bmw', 'renault', 'honda']
cars.append('toyota')

Za pomocą metody insert(), możemy wstawić element w dowolny indeks listy. Metoda jako argumenty przyjmuje indeks dla nowego elementu oraz jego wartość.

cars = ['audi', 'bmw', 'renault', 'honda']
cars.insert(2, 'daewoo')

1.3.2. Usuwanie elementów z listy

Usuwanie elementów z listy możemy przeprowadzić na trzy sposoby. Po prostu usuwając element poprzez podanie nazwy listy oraz indeksu (tak jak byśmy uzyskiwali dostęp do wartości) instrukcji del.

del cars[1]

Kolejnym sposobem może być zabranie danej wartości z listy za pomocą metody pop(). Jako argument metoda przyjmuje indeks, tak więc możemy pobrać dowolny element, jeśli nie podamy żadnych argumentów metoda pobierze ostatni element.

cars = ['audi', 'bmw', 'renault', 'honda']
fura = cars.pop()

Ostatnim, chyba najciekawszym sposobem jest metoda remove(), która wyszukuje element na podstawie wartości. Poszukiwaną wartość podajemy jak argument metody.

cars = ['audi', 'bmw', 'renault', 'honda']
swag_gablota = 'bmw'
cars.remove(swag_gablota)

Uwaga! Metoda remove() usuwa tylko pierwsze wystąpienie danej wartości na liście, więc jeśli jest więcej niż jedna taka wartość to tylko pierwsza odnaleziona zostanie usunięta.

1.3.3. Sortowanie list

Elementy na liście mogą być innej kolejności niż moglibyśmy sobie tego życzyć, w Pythonie istnieje kilka sposób na uporządkowanie listy (sortowanie).

Pierwszą z nich jest użycie metody sort(), metoda ta wyróżnia się tym, że wprowadza zmiany samej liście, czyli jej użycie w kodzie zmieni kolejność wartości na liście. Domyślnie metoda sort() sortuje rosnąco, jednak możemy mieć na to wpływ, podając jej argument reverse=True, teraz metoda posortuje listę malejąco.

Kolejnym sposobem na sortowanie jest sposób tymczasowy, na przykład gdy kolejność danych ma znaczenie i nie możemy jej od tak sobie zmieniać, ale jednak wypadałoby aby użytkownikowi wypisać posortowane dane. Za sortowanie tymczasowe odpowiada funkcja sorted(). Funkcja od metody tym kontekście różni się tym że musimy podać jako argument listę, a następnie zrobić coś z danymi zwróconymi albo przechować je w zmiennej albo wypisać za pomocą polecenia print(). Oczywiście funkcja sorted() również przyjmuje argument reverse=True.

cars = ['audi', 'bmw', 'renault', 'honda']
cars.sort()
print(cars)
cars.sort(reverse=True)
print(cars)

cars = ['audi', 'bmw', 'renault', 'honda']
print(sorted(cars))
print(sorted(cars, reverse=True))

W Pythonie sortowanie jest nieco bardziej skomplikowane, ponieważ ciągi tekstowe sortowane są za pomocą wartości tablicy ASCII. Mniejszą wartość (liczbę całkowitą przyporządkowaną do znaku w tablicy) mają wielkie litery, dlatego jeśli elementy listy mają wartość nie jednakowe (wszystkie z wielkiej/wszystkie z małej) to możemy spodziewać się, że wartość zapisane wielką literą będą miały pierwszeństwo na posortowanej liście.

1.3.4. Odwracanie listy.

Inną metodą na organizację listy jest jej odwrócenie. To znaczy, że ostatni element będzie pierwszym, przedostatni drugim itd, do odwracania listy służy metoda reverse().

cars = ['audi', 'bmw', 'renault', 'honda']
cars.reverse()

Warto wspomnieć, że reverse() trwale zmienia kolejność listy.

1.3.5. Długość listy

Wielkość listy możemy określić przy pomocy funkcji len(), funkcja jako argument przyjmuje naszą listę.

cars = ['audi', 'bmw', 'renault', 'honda']
len(cars)

1.4. Pętla for

Pętla for w Pythonie jest podobna konstrukcyjnie do pętli for z BASH-a, również używa się słowa kluczowego in. Jeśli chcielibyśmy przetłumaczyć na język potoczny pętlę for to będzie to mniej więcej tak: "Dla zmiennej value przypisz element z listy, następnie wykonaj blok kodu, na koniec przesuń się na kolejny element listy". Ten rodzaj pętli służy głównie iteracjom przez listy lub listy tworzone przez funkcję range() (określone zbiory danych). Przy pętli for po raz pierwszy spotkamy się z zagnieżdżaniem bloków kodu. Przy każdej konstrukcji operującej na bloku kodu pojawia się dwukropek (:), oznaczający początek blok kodu. Wszystkie linie, które mają znaleźć się w owym bloku przesuwamy o jeden tab. Ponieważ wcięcia Pythonie są wykorzystywane do oznaczania bloku kodu, trzeba uważać przy ich stosowaniu.

cars = ['audi', 'bmw', 'renault', 'honda']
for car in cars:
	print(car)

1.5. Funkcja range

Za generowanie serii liczb ujętych w listę odpowiedzialna jest funkcja range(). Jako argumenty przyjmuje ona zakres dla generowanej serii liczb. Cechą funkcji, o której warto wiedzieć to to, że ostatnia liczba z zakresu jest traktowana w sposób wyłączny, to znaczy, że jeśli użyjemy funkcji range() do wygenerowania listy od 1 do 10, to jeśli podamy argumenty 1,10 to listę otrzymamy od 1 do 9.

for number in range(1,10):
print(number)

Funkcja range() może przyjmować jeden argument, zakres końcowy.

for number in range(5):
	print(number)

Dane wyjściowe takiej pętli będą od 0 od 4 - 5 elementów.

Funkcję range()możemy wykorzystać wraz inna funkcją - list() do tworzenia list liczbowych.

numbers = list(range(1,11))
for number in numbers:
	print(number)

Po za określeniem zakresu funkcja range() przyjmuje również krok, czyli o ile ma zwiększyć kolejne elementy.

even_numbers = list(range(2,11,2))
for evenn in even_numbers:
	print(evenn)

1.6. Proste funkcje statystyczne

W Pythonie mamy dostępnych kilka bardzo prostych wybudowanych funkcji statystycznych. Mianowicie:

1.7. Lista składana

Aby ograniczyć do minimum proces tworzenie list. Python wprowadza coś takiego co nazywa się listą składaną. Taka lista składa się z modelu wartości elementu (np. value**2) oraz z pętli for wraz z funkcją range(). Liczba stworzonych elementów znajduje się w argumencie funkcji range(). Elementy będą miały wartość obliczoną w modelu.

squares = [value**2 for value in range(1,11)]
print(squares)

1.8. Wycinek listy.

Powiedzmy, że do obliczeń potrzebujemy dwóch środkowych wartości z listy. Aby wydobyć je w najbardziej efektywny sposób możemy użyć wycinka listy. Wycinek tworzymy, podając w miejscu indeksu przy normalnym odwoływaniu się do listy, zakres składający się z wartości początkowej (uwaga, liczonej od 0), dwukropka (:) oraz wartości końcowej, z tym, że z wartością końcową jest identycznie co w przypadku funkcji range(). Chcąc stworzyć wycinek z drugiego oraz trzeciego elementu, należy policzyć od 0, czyli drugi element będzie mieć indeks 1, zakres końcowy trzeba podać o 1 większy więc licząc od 0 trzeci element to 2 oraz jeszcze 1 więc koniec, końców zakres końcowy to 3.

even_numbers = list(range(2,11,2))
evenn_slice = even_numbers[1:3]

Zakres wartości w wycinkach, jest nieco bardziej elastyczny. Na przykład chcemy wszystkie elementy do czwartego to zamiast [0:4] podajemy po prostu [:4] możemy pominąć wartość początkową, wtedy domyślnie będzie 0. Podobnie jest z wartością końcową [2:] ten zakres oznacza, że wycinek będzie zawierał wszystkie elementy od 3 do końca listy. Podobnie jak w przypadku indeksów, możemy podać ujemne wartości, przyczym podajemy tylko wartość początkową. Taki wycinek będzie zawierać elementy od tego wskazanego do końca listy. Przy podawaniu warto pamiętać że ostatni element to -1, im większa wartość ujemna tym bliżej początku listy jesteśmy.

even_numbers = list(range(2,11,2))
end_values = even_numbers[-2:]

1.8.1. Krok w wycinku listy

Zakres wycinku ma możliwość podania trzeciego argumentu, wartości kroku przy tworzeniu wycinka.

numbers = list(range(1,11))
non_even_numbers=numbers[0::2]

1.8.2. Iteracja przez wycinek listy

Do wskazania pętli for możemy użyć wycinka listy. Na poniższym przykładzie zaprezentowano jak należy to wykonać:

numbers = list(range(1,11))
for value in numbers[0::2]:
	print(value)

1.8.3. Kopia listy

Często może zdarzyć się taka sytuacja, że chcemy zachować pierwotną wersję listy, ale musimy użyć jej wartości do jakiś operacji, które mogą naruszyć wartości jakiś jej elementów, w tym wypadku możemy stworzyć kopię listy.

cars = ['audi', 'bmw', 'renault', 'honda']
cars_bkp = cars[:]

1.9. Krotki

Może zdarzyć się potrzeba posiadania listy stałych, których wartość nie może zmienić się przez cały cykl życia programu. Z taką listą jest już lepiej w niż ze zwykłymi stałymi. Taką listę nazywa się krotką. Definiowanie krotki, przypomina definiowanie listy, jednak zamiast nawiasów kwadratowych, mamy zwykłe okrągłe. Definicja wygląda w następujący sposób:

rgb = ('red', 'green', 'blue')
print(rgb[0])
print(rgb[1])
print(rgb[2])

Dostęp do elementów krotki, jest identyczny jak do elementów listy, co zostało zobrazowane powyżej. Teraz dla eksperymentu możemy spróbować zmienić jeden z elementów krotki.

>>> rgb = ('red', 'green', 'blue')
>>> rgb
('red', 'green', 'blue')
>>> rgb[0]='yellow'
>>> Traceback (most recent call last):
	File "", line 1, in 
TypeError: 'tuple' object does not support item assignment

Iteracja przebiega tak samo jak na listach. Jedyną rzeczą jaką możemy zrobić przy krotce aby zmodyfikować jej zawartość, jest jej nadpisanie.

rgb = ('red', 'green', 'blue')
print(rgb[0])
print(rgb[1])
print(rgb[2])

rgb = ('cyan', 'yellow', 'magenta', 'black')

Krotka zawierająca jedną wartość musi zawierać przecinek na końcu, ponieważ dzięki niemu zmienna jest rozpoznawana jako krotka.

1.10. Wyrażenia warunkowe

Każdy bardziej rozbudowany program będzie potrzebował instrukcji decyzyjnej. Te instrukcje uruchamiają blok kodu na podstawie testów warunkowych. Najprostszym z nich jest sprawdzenie równości.

>>> car = 'Audi'
>>> car == 'audi'
False

W powyższym przykładzie wyszedł na jaw bardzo ważny szczegół, Python jest case-sensitive, rozróżnia wielkość liter. Dlatego warto przed jakimkolwiek testami warunkowymi przeprowadzić normalizacje.

>>> car = 'Audi'
>>> car.lower() == 'audi'
True

Kolejnym testem jaki możemy przeprowadzić na wszystkich typach danych jest sprawdzenie nierówności.

>>>  car = 'Audi'
>>>  car.lower() != 'audi'
False

Porównania mniejsze niż, mniejsze bądź równe, większe niż, większe bądź równe, są testami wykonywanymi na liczbach. Nawet jeśli zestawimy sobie w porównaniu dwa ciągi znaków, to Python porówna wartości z tablicy ASCII pierwszych znaków i na podstawie tego zostanie zwrócona wartość logiczna True lub False.

>>> age = 19
>>> age > 21
True
>>> age >= 21
True
>>> age > 21
False
>>> age >= 21
False
car1 = 'audi'
>>> car2 = 'bmw'
>>> car1 > car2
False
>>> car2 > car1
True
>>> car2='Kamaz'
>>> car1 > car2
True

Czasami może być tak, że takie najzwyklejsze testy jak te przeprowadzone powyżej nie wystarczą, aby instrukcja warunkowa uruchomiła blok kodu potrzebne będzie sprawdzenie kilku połączonych ze sobą warunków do łączenia ze sobą testów służą operatory logiczne.

age = 19
(age >= 13) and (age > 20)
True

Słowa kluczowego in nie wykorzystujemy tylko w pętli for, możemy również je wykorzystać do wyrażenia warunkowego, w którym to możemy określić czy dana wartość znajduje się na liście.

>>>  pojazdy = ['audi', 'bmw', 'Kamaz']
>>> 'audi' in pojazdy
True

Dodając słowo kluczowe not przed słowem in możemy sprawdzić czy danej wartości nie ma na liście.

pojazdy = ['audi', 'bmw', 'Kamaz']
'audi' not in pojazdy
False

1.11. Konstrukcja if

Oczywiście do sterowania wykonaniem programu za pomocą testów warunkowych służy instrukcja if, które jest częścią dużej konstrukcji warunkowej, składającej się z polecenia if, rozpoczynającego całą konstrukcje i definiującego pierwszy test warunkowy, jeśli test zwrócić wartość True zostanie wykonany blok kodu instrukcji if.

if test_warunkowy:
	#blok_kodu_instrukcji_if

A co jeśli test warunkowy zwróci wartość False, w tym wypadku możemy poddać wykonanie programu jeszcze jednemu warunkowi za pomocą instrukcji elif lub za pomocą ogólnego bloku przeznaczonego dla tego przypadku - else. Powiedzmy, że chcemy sprawdzić jeszcze jeden warunek.

if test_warunkowy:
	#blok_kodu_instrukcji_if
elif test_warunkowy2:
	#blok_kodu_instrukcji_elif

Co jeśli oba warunki zawiodą to powinniśmy zdefiniować kod blok kodu dla takiej sytuacji. Z racji tego iż można pominąć w Pythonie blok else, ja zachęcam do jego stosowania. Autor książki podaje że:

Blok else jest wykorzystywany w sytuacji, gdy nie został spełniony żaden warunek z warunków wcześniej zdefiniowanych za pomocą poleceń if lub elif. Brak spełnienia warunku może wynikać z podania nieprawidłowych danych lub danych o złośliwym działaniu. Jeżeli istnieje konkretny, ostateczny warunek do sprawdzenia, rozważ użycie bloku elif i całkowite pominięcie bloku else. W ten sposób zyskasz absolutną pewność, że kod będzie wykonywany jedynie po spełnieniu oczekiwanych warunków.

Moim zdaniem blok domyślny przy nie spełnieniu żadnego warunku, daje nam szanse na reakcje na nieprawidłowe dane wejściowe. Najprościej jeśli nie spełniony został żaden warunek to kończymy działanie programu.

if test_warunkowy:
	#blok_kodu_instrukcji_if
elif test_warunkowy2:
	#blok_kodu_instrukcji_elif
else:
	#blok_kodu_else

Przydatną rzeczą jaką możemy zrobić z poleceniem if jest sprawdzenie przed rozpoczęciem pracy z listą, czy lista aby nie jest pusta.

cars=[]

if cars:
	for value in cars:
		print(value)
else:
	print("Lista jest pusta")

Jak widać powyżej, aby sprawdzić czy lista jest pusta w Pythonie wystarczy podać jej nazwę w miejscu testu warunkowego.

1.12. Słowniki

Słowniki przypominają trochę format JSON zaimplementowany do języka programowania. Najprostszy słownik wygląda tak:

miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, }

Nie, przecinek na końcu nie jest błędem. Wskazuje tylko, że do słownika mogą zostać wprowadzone nowe pary klucz-wartość. Kluczem określamy wartość występującą w słowniku, zawsze ujętą w apostrofy lub cudzysłowie, z dwukropkiem na końcu, dzięki kluczowi będziemy odwoływać się do wartości pary. Dostęp do dowolnej wartości słownika:

miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, }
print(miasto1['powierzchnia'])

Kiedy chcemy uzyskać dostęp do konkretnej wartości to przy słowniku, klucz zapisujemy w nawiasie kwadratowym. Analogicznie do tablic asocjacyjnych w innych językach programowania.

Pusty słownik definiujemy podobnie jak pustą listę tylko inne są znaki.

empty_dict={}

Jeśli chcemy zmodyfikować wartość z jakieś pary, to tak samo jak w przypadku list, tylko że klucz zapisujemy w miejscu indeksu.

miasto1['kraj'] = 'Polska'

Kiedy jakieś dane w słowniku staną nam się zbędne, możemy je usunąć przy pomocy polecenia del.

del miasto1['populacja']

1.12.1. Dodawanie nowych wartości do słownika

Dodawanie nowych par klucz-wartość jest analogiczne do modyfikacji. Jeśli będzie modyfikować wartość pod kluczem, który nie istnieje w słowniku to zostanie ona dodana jako nowa para.

Co jeśli podczas pracy z słownikiem odwołamy się do klucza, który nie istnieje? Python zwróci błąd.

>>> print(miasto1['gestosc'])
Traceback (most recent call last):
	File "", line 1, in 
KeyError: 'gestosc'

Python daje możliwość użycia specjalnej metody get(), która jest wstanie zwrócić wartość podanego jako argument klucza, lub też zdefiniowanego jako kolejny argument komunikatu, w momencie kiedy dany klucz nie istnieje. Komunikat nie jest wymagany w przypadku jego braku nie zostanie nam nic zwrócone. Metoda get() nadaje się jako test czy dany klucz istnieje.

if miasto1.get('gestosc'):
	#Gęstość istnieje, zrób to i to
else:
	#Gęstość nie istnieje, zrób to i to

1.12.2. Iteracja przez słownik

Iteracji przez słownik w sumie są trzy rodzaje: przez pary, przez klucze i przez wartości. Przez pary, będzie prawdopodobnie najpopularniejszą z wszystkich rodzajów.

miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, }

for key,value in miasto1.items():
	print(f"Klucz: {key}")
	print(f"Wartość: {value}\n")

Metoda items() w dużym skrócie rozbije na dwie listy wszystkie klucze i wszystkie wartości, i to na nich pętla for w pamięci będzie przeprowadzać iteracje.

1.12.2.1. Iteracja przez klucze i wartości.

Iteracja przez klucze, zadziała na podobnej zasadzie co przez parę jednak do iteracji wykorzystuje się tylko jedną listę, listę z kluczami. Analogicznie z iteracją przez wartości tylko że listę z wartościami.

miasto1 = { 'kraj': 'Polska', 'powierzchnia': 517.14, 'populacja': 1_790_658, }

for key in miasto1.keys():
	print(f"Klucz: {key}\n")

for value in miasto1.values():
	print(f"Wartość: {value}\n")

Metody keys() i values() tworzą listy, dlatego też możemy przeprowadzić na nich iterację z wykorzystaniem pętli for oraz in.

W wydaniach Pythona począwszy od 3.7, słownik zachowuje kolejność, w której były dodawane pary. Podczas pracy z słownikiem jego domyślną uporządkowaniem jest kolejność dodawania par.

Listę kluczy możemy obudować funkcją sorted() aby je uporządkować.

1.13. Zbiory

Klucze muszą być unikatowe, aby wartości się na siebie nie nakładały. Ale wśród wartości już takich wymagań nie ma. Python udostępnia funkcję set(), która na podstawie przekazanej listy tworzy zbiór z unikatowych elementów. Zbiór jest składniowo bardzo podobny do słownika, jednak zawiera sam wartości, nie zawiera on żadnych danych porządkowych.

jezyki = {'python', 'ruby', 'c', 'python'}
>>> jezyki
{'python', 'ruby', 'c'}

Sposobem na to aby uzyskać dostęp do danych zapisanych na zbiorze jest iteracja lub konwersja na listę za pomocą funkcji sorted(). Należy pamiętać aby zapisać gdzieś listę zwróconą przez sorted()

1.14. Zagnieżdznie złożonych typów danych

Przedstawienie słowników umożliwia zaprezentowanie zagnieżdżania. Pozwala ono na umieszczenie jednego złożonego typu w drugim, możemy stworzyć między innymi listę słowników, listę w słowniku czy też słownik w słowniku. Listę słowników najlepiej rozpocząć od pustej listy, następnie czy to w skutek iteracji czy danych napływających do programu tworzyć nowy słownik i załadować go do listy za pomocą metody append(). Trochę eksperymentując zauważyłem ciekawą rzecz, mianowicie kiedy stworzyłem sobie model słownika powyżej iteracji, i chciałem odpowiednio według testów warunkowych modyfikować wartości modelu, to na listę trafiała tylko pierwsza aktualizacja przez całą iterację. Dostęp do danych zagnieżdżonych wygląda trochę jak tablice wielowymiarowe. W zależności od tego czy jest to lista słowników, to na początku podajemy indeks a później klucz, w przypadku zagnieżdżenia listy w słowniku, sytuacja jest odwrotna.

Ostatnim przypadkiem jest słownik w słowniku, tu podajemy klucz - klucz. Do operacji na zagnieżdżeniach warto wykorzystać metody stosowane do iteracji na słownikach. Ważną rzeczą stosowaną przy zagnieżdżeniach jest ich jak największe ograniczenie, jeśli algorytm zakłada większe zagnieżdżenie niż powyżej jednego poziomu (tj. każdy klucz zawiera pojedynczą listę lub każdy element listy zawiera słownik bez zagnieżdżeń), to oznacza że ma on poważne wady konstrukcyjne i na pewno jest jakiś lepszy sposób na rozwiązanie tego problemu.

1.15. Pobieranie danych od użytkownika.

Pobieranie danych od użytkowników jest realizowane dzięki funkcji input(). Funkcja ta jako argument przyjmuje, komunikat zachęcający użytkownika do wpisania danych tzw. znak zachęty. Dane wpisane przez użytkownika są zwracane w postaci ciągu tekstowego.

Jeśli musimy pobrać od użytkownika dane do obliczeń, to wartością zwracaną przez funkcje input() jest ciąg tekstowy. W tym wypadku musimy dokonać konwersji na typ całkowity. Odpowiada za to funkcja int(), której wartością zwracaną jest liczba całkowita, a argumentem dowolna wartość przypominająca liczbę, która ma zostać skonwertowana ja postać liczby całkowitej. Do obliczeń nie zawsze będziemy potrzebowali wartości całkowitej. Może zdarzyć się, że będziemy potrzebować wartości zmiennoprzecinkowej, aby uzyskać wartość w postaci liczby zmiennoprzecinkowej należy użyć funkcji float(). Te konwersje można odwrócić uzyskując ciąg tekstowy z liczb za pomocą funkcji str().

>>> age = input("Ile masz lat?")
Ile masz lat? 12
>>> age
'12'
>>> age = int(age)
>>> age
12
>>> age >= 18
False

Konwersje można skrócić do jednej linii, obudowując funkcję input() funkcją funkcją int().

age = int(input("Ile masz lat? "))
Ile masz lat? 18
>>> age
18
>>> age >= 18
True

1.16. Operator modulo (reszty z dzielenia).

Przydatną rzeczą jeśli chodzi o obliczenia liczbowe wykorzystywane w programowaniu napewno jest operator modulo (%). Przyjmuje dwa operandy (dzielną i dzielnik), zwraca resztę z dzielenia dzielnej przez dzielnik. Najprostszym przykładem użycia operatora modulo (%) jest sprawdzenie czy dana wartość jest parzysta czy też nie.

if number % 2 == 0:
	print(f"{number} jest liczbą parzystą.")
else:
	print(f"{number} nie jest liczbą parzystą.")

1.17. Pętla while.

Jeśli potrzebujemy pętli która działa do pewnego momentu, np. dopóki dana zmienna ma wartość taką a taką, to wtedy musimy skorzystać z pętli while. Pętla ta działa dopóty, dopóki warunek umieszczony obok słowa kluczowego while będzie zwracał wartość True. Pętla while nie posiada wbudowanego licznika, dlatego takowy licznik musimy sobie zbudować sami.

number=1
while number >= 10:
	if number % 2 == 0:
		print(f"{number} jest parzysta.")
  else:
		print(f"{number} nie jest parzysta.")
  number += 1

Wynik działania powyższego przykładu:

1 nie jest parzysta.
2 jest parzysta.
3 nie jest parzysta.
4 jest parzysta.
5 nie jest parzysta.
6 jest parzysta.
7 nie jest parzysta.
8 jest parzysta.
9 nie jest parzysta.
10 jest parzysta.

Zmienna number przybrała w tym przykładzie rolę danych, na których operujemy oraz licznika.

Czasami może zajść potrzeba aby kontrolować wykonanie programu za pomocą pętli while. Powiedzmy że mamy aplikacje, która wyświetla nam listę opcji, następnie prosi nas jej wybór, wybieramy jedną z opcji, następnie po wykonaniu czynności dedykowanych dla tych opcji wracamy do menu, gdzie znów możemy wybrać jedną z opcji lub zakończyć program. Konstrukcja takie aplikacji może opierać się na pętli while, której warunek zwraca wartość True, kiedy wybieramy opcje "Exit/Wyjście", no właśnie co się dzieje? Możliwości są trzy.

Istotną rzeczą dla każdej iteracji jest możliwość jej zrestartowania, kiedy gdzieś w kodzie pętli zostanie umieszczone słowo kluczowe continue, spowoduje powrót do sprawdzenia warunku a następnie wykonania bloku pętli od początku. To polecenie przydatne może być gdy chcemy pominąć jakieś elementy

Przy pracy z pętlami while warto pamiętać o prawidłowym warunku, zmianie licznika czy o zakończeniu pętli w odpowiednim momencie. Patrząc na pętle, o której mówiłem akapit wcześniej, odnośnie sterowania programem to są to pętle, które bez odpowiednich mechanizmów zatrzymujących będą wykonywać się w nieskończoność. Warunek nie zawiera licznika. Zmienna w teście warunkowym bez mechanizmów stopujących nie zmieni swojej wartości. Chociaż i w pętlach while opartych na licznikach też łatwo o nieskończoną pętle. Wystarczy zapomnieć zmienić licznik na końcu bloku.

Pętla while również nadaje się do pracy z listami czy słownikami, jedną taką ciekawą rzeczą jeśli chodzi o listy jest taki mechanizm, że jeśli umieścimy w warunku pętli nazwę listy, to pętla będzie wykonywać się dopóki na tej liście będą jakieś elementy, mechanizmem kontroli tutaj będzie pobieranie elementów listy za pomocą metody pop(). Jak w każdym innym teście warunkowym w warunku pętli while również istnieje możliwość użycia operatora in

1.18. Funkcje

Funkcje służą do przygotowania bloku kodu , który możemy później wykorzystać wielokrotnie w części głównej programu. Funkcje ułatwiają zarządzanie kodem, testowanie oraz ewentualne debugowanie z racji tego, iż jest to wydzielony fragment kodu, który możemy wykonywać niezależnie od pozostałej części programu.

1.18.1. Definiowanie funkcji

Definiowane funkcji rozpoczynamy od słowa kluczowego def następnie podajemy nazwę funkcji po nazwie umieszczamy bez spacji parę nawiasów okrągłych po nawiasach oczywiście występuje dwukropek. W nowej linii od pojedynczego tabulatora rozpoczynamy blok kodu funkcji.

1.18.1.1 Komentarze w funkcji

Komentarze w funkcji umieszcza się jako tzw. docstring, którego zadaniem jest opis działania funkcji. Wszystko co zostało ujęte pomiędzy parą potrójnych cudzysłowów ("""...""") jest traktowane jako docstring.

def hello_user():
	"""To jest funkcja, która wita użytkownika"""

Docstring wykorzystywany jest przez Python do tworzenia opisów funkcji podczas generowania dokumentacji dla poszczególnych projektów.

1.18.1.2. Parametry funkcji

Bardziej zaawansowane funkcje, które mają nieco więcej czynności niż wypisane "Witaj świecie!", zazwyczaj potrzebują jakiś danych z zewnątrz. Dane, parametry funkcji deklarujemy w nawiasie zaraz obok nazwy funkcji, w ten sposób zaznaczamy że nasza funkcją będzie wymagać jakiś informacji do działania.

def hello_user(username):
	"""To jest funkcja, która wita użytkownika"""
	print(f"Witaj, {username}!")

Prawdziwe dane przekazujemy w momencie wywołania funkcji w części głównej. Wywołanie funkcji jest realizowane poprzez podanie jej nazwy w raz parą nawiasów okrągłych, w których w zależności od definicji umieszczamy dane niezbędne do jej wykonania. Oczywiście jeśli funkcja nie potrzebuje żadnych danych, para nawiasów pozostaje pusta.

uname = 'xf0r3m'
hello_user(uname)

1.18.1.3. Argumenty pozycjne

Zmienne lub też dane umieszczone w wywołaniu funkcji nazywamy argumentami. Argumenty może przekazać do funkcji na dwa różne sposoby. Pierwszy sposób to sposób klasyczny, sposób argumentów pozycyjnych, opera się on kolejności. W jakiej kolejności parametry zostały podane w definicji funkcji, to w takiej samej kolejności należy przekazać do funkcji argumenty.

def func1(para1, para2, para3):
	print(f"{para1} {para2} {para3}")

func1('Ala', 'ma', 'kota')
# Ala ma kota

Python nie zwróci błędu jeśli pomylimy kolejność. Ale wynik działania funkcji może być nieoczekiwany.

def func1(para1, para2, para3):
	print(f"{para1} {para2} {para3}")
	#arg1=param3, arg2=param2 arg3=param1

func1('kota', 'ma', 'Ala')
# kota ma Ala

1.18.1.4. Argumenty w postaci słów kluczowych

Drugą metodą jest przekazywanie argumentów w postaci słów kluczowych. Opiera się ona zasadzie par "nazwa=wartość", w tym przypadku "parametr=argument". Tutaj kolejność nie ma znaczenia. Ponieważ już podczas wywołania przypisujemy parametrowi wartość argumentu.

def func1(para1, para2, para3):
	print(f"{para1} {para2} {para3}")

func1(para3='kota', para1='Ala', para2='ma')
# Ala ma kota

Może zdarzyć się że musimy przygotować funkcję dla typowych, ciągle powtarzających się danych, które mogli byśmy podać już podczas definicji funkcji. A jeśli zajdzie taka potrzeba będziemy mogli nadpisać wartość domyślną parametrów funkcji dla innych danych.

def func1(para3, para1='Ala', para2='ma'):
	print(f"{para1} {para2} {para3}"

func1('psa')
# Ala ma psa
func1('kota')
# Ala ma kota
func1('nierówno pod sufitem')
# Ala ma nierówno pod sufitem

func1('w LoL-a', 'Tola', 'gra')
# Tola gra w LoL-a

Jak można zauważyć parametry w definicji funkcji zmieniły kolejność, jest wymagane przez Python, aby parametry, które będą przekazywane jako argumenty pozycyjne zawsze były na pierwszym miejscu i tak też należy przekazywać argumenty.

1.18.1.5. Zwracanie wartości przez funkcje

Funkcje często są wykorzystywane do obliczeń. Niestety co nam po obliczonych wartościach kiedy pozostają one w bloku kodu funkcji. Możemy oczywiście zwrócić wartość uzyskaną przez funkcje. Służy do tego słowo kluczowe return.

def pitagoras_need_c_square(a,b):
	return (a**2 + b**2)

c_square = pitagoras_need_c_square(3,4)
print(c_square)
# 25

Jak widać przy zwrocie wartości wywołanie funkcji musimy przypisać do zmiennej.

1.18.2. Przekazywanie do funkcji złożonych typów dancyh

Do funkcji możemy przekazać nie tylko pojedyncze dane oraz zmienne, ale również całe listy i składniowo nie rożni się to przekazania pojedynczych danych.

def iteration_via_list(l):
	for list_element in l:
		print(f"{list_element}")

Warto zaznaczyć, że tak jak przekazujemy zmienne, to przekazujemy ich wartość. W przypadku listy prawdopodobnie zamiast wartość przekazywane jest miejsce w pamięci rozpoczęcia listy, przez to lista zdefiniowana w głównej części programu może zostać przez tą funkcję zmodyfikowana. Istnieje technika dzięki, której możemy się przed tym uchronić, możemy przekazać kopię listy (wycinek od pierwszego do ostatniego elementu).

received_cars = ['audi', 'bmw', 'toyota', 'subaru'];
repaired_cars = [];

def repair_car(rcev_cars, rep_cars):

	while rcev_cars:
		repaired_car = rcev_cars.pop()
		print(f"Naprawiam: {repaired_car}")
		rep_cars.append(repaired_car)

def print_raport(rcev_cars, rep_cars):
	print("Otrzymano następujące samochody: ")
	for rcev_car in rcev_cars:
		print(f"\t - {rcev_car}")

	print("\nNaprawiono następujące samochody: ")
	for rep_car in rep_cars:
		print(f"\t - {rep_car}")

repair_car(received_cars[:], repaired_cars)
repaired_cars.reverse()
print_raport(received_cars, repaired_cars)

O ile nie istnieje żaden ważny powód należy przekazywać funkcjom prawdziwą listę, użycie kopii może być mniej efektywne oraz zajmować więcej pamięci.

1.18.3. Przekazywanie nieokreślonej liczy argumentów

Podczas prac z kodem możemy spotkać się z takim problemem że nie będziemy potrafili przewidzieć ile może być potrzebnych argumentów. W Pythonie możliwe jest przekazanie dla parametru dowolnej liczby argumentów. Taki parametr definiuje się z gwiazdka (*) przed nazwą. Przekazanie kilku argumentów pod ten parametr powoduje utworzenie krotki z tymi argumentami, przez które możemy normalnie iterować.

def make_pizza(*toppings):
    print("Tworze pizzę: ")
    for toping in topings:
        print(f"\tDodaję: {topping}")

make_pizza('ser', 'szynka', 'pieczarki')

Nic nie stoi na przeszkodzie aby użyć np. argumenty pozycyjnego wraz z krotką argumentów. Należy oczywiście pamiętać o kolejności. Python zwróci błąd ponieważ za parametrem tego typu nie powinny znajdować się już żadne inne parametry.

Istnieje również metoda, która umożliwi przekazanie wielu argumentów jako słów kluczowych. Podobnie do powyższego przypadku wykorzystane są gwiazdki (**), dwie a nie jedna. Po przekazaniu par nazwa-wartość w funkcji zostanie utworzony słownik.

def make_car(make, model, **car):
    car2={}
    car2['make'] = make.title()
    car2['model'] = model.title()

    for k,v in car.items():
        car2[k]=v

    return car2

car = make_car('subaru', 'outback', color='blue', towPackage=True)

print(car)

Ten przykład jest nieco bardziej skomplikowany. Przy korzystaniu z przekazywania wielu argumentów za pomocą słów kluczowych należy pamiętać o tym że dane w słowniku są ułożone według kolejności dodawania danych do słownika (Ta zależność pojawiła się dopiero od Pythona >=3.7). W tym przypadku dane do słownika przekazane przez '**car' trafiają jeszcze przed przejściem do wykonywania kodu funkcji. Dlatego jeśli chcemy mieszać argumenty jak na powyższym przykładzie, należy o tym pamiętać. Poprzez utworzenie nowego słownika i na początku dodanie argumentów pozycyjnych następnie przepisanie danych ze słownika car do słownika car2 danych dodanych jako wielu argumentów słów kluczowych. W ten sposób uzyskaliśmy słownik o kolejności identycznej jak argumenty podane w funkcji.

1.19. Moduły

W Pythonie w prosty sposób możemy utworzyć tzw. moduły w innych językach możemy nazwać je np. plikami nagłówkowymi. Tworzymy je poprzez utworzenie oddzielnego pliku i wpisanie tam wszystkich funkcji. Kiedy mamy funkcje w oddzielnym pliku pozostaje je tylko zaimportować. Poniżej znajdują się metody importu, wraz z przykładem użycia funkcji z zaimportowanego modułu.

#import sandwich
#sandwich.make_sandwich('pszenne', 'szynka', 'ser', 'ketchup')

#from sandwich import make_sandwich
#make_sandwich('pszenne', 'szynka', 'ser', 'ketchup')

#from sandwich import make_sandwich as fn
#fn('pszenne', 'szynka', 'ser', 'ketchup')

#import sandwich as mn
#mn.make_sandwich('pszenne', 'szynka', 'ser', 'ketchup')

#from sandwich import *
#make_sandwich('pszenne', 'szynka', 'ser', 'ketchup')

Możemy używać słowa kluczowego import do zaimportowania całego modułu, lub zaimportować konkretną funkcję z danego modułu poprzez kombinacje słów kluczowych from oraz import. Gdzie po słowie from podajemy nazwę modułu, po słowie import podajemy nazwę funkcji. Ciekawą techniką jest tworzenie aliasów dla zaimportowanych modułów lub funkcji. Po nazwie modułu lub funkcji oraz po słowie kluczomym as podajemy alias czyli taką nazwę zastępczą lub pseudonim. Tworzenie aliasów przydaje się gdy np. mamy podejrzenie, że funkcja w module ma taką samą nazwę jak funkcja w głównej części programu. Linia przed ostatnia z powyższego przykładu mogą wydawać się równoznaczne, ale ostatnii przykład pokazuje nam w jaki sposób możemy zaimportować funkcje z modułu. Ta czynność pozwala na pominięcie prefixu nazwy modułu

1.20. Programowanie zorientowane obiektowo

Programowanie zorientowane obiektowo jest metodą, która pozwala programistom przedstawiać przy użyciu komputera rzeczy znane nam z realnego świata. Weźmy takiego człowieka. Człowiek jest obiektem zarówno w znanym nam świecie, jak i również może zostać przedstawiony jako obiekt w świecie wirtualnym. Jeśli człowiek staje się obiektem w ujęciu programistycznym, to jego cechy szczególne stają się właściwościami/atrybutami obiektu. Czynności jakie wykonuje mogą być metodami.

Człowiek jest przedstawicielem gatunku. Gatunku ludzkiego. Obiekty są również przedstawicielami gatunku jaki sobie zdefiniujemy. Tym gatunkiem są klasy.

Definicje klasy rozpoczynamy od słowa kluczowego class następnie podajemy nazwę tej klasy oraz zaraz obok nazwy bez spacji parę nawiasów okrągłych oraz dwukropek. W następnej linii po jednym znaku tabulacji możemy rozpoczynać definicję klasy. Klasa z poziomu kodu źródłowego składa się głównie z funkcji, które tutaj nazywają się metodami. Pierwszą definiowaną metodą jest metoda __init__. Uwaga, przy zapisie tej nazwy należy uważać na to, że jest ona obudowana dwoma znaki podkreślenia (__). Ta metoda jest metodą specjalną, ma za zadanie zainicjować właściwości zapisane w jej bloku. Te atrybuty będą atrybutami obiektu. Argumenty dla tej metody są przekazywane podczas tworzenia egzemplarza klasy, czyli obiektu.

class User():
    def __init__(self,firstname,lastname,age,sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex

Parametr self jest obiektem, który będzie tworzony na podstawie tej klasy. Słowo kluczowe self będzie oznaczać po prostu obiekt tej klasy. To atrybutowi obiektu tej klasy w metodzie __init__ nadajemy wartość parametru firstname. Z racji tego że obiekt jest egzemplarzem klasy stąd słowo self (samemu sobie).

1.20.1. Objekt

Egzemplarz klasy (obiekt) tworzymy nadając zmiennej wartości w postaci wywołania zdefiniowanej wcześniej klasy.

user1 = User('Jan', 'Nowak', '47', 'M')

Argumentami przekazywanymi klasie są tak naprawę argumenty dla metody __init__, ponieważ to ona jest uruchamiana podczas tworzenia obiektu. Argument self jest przekazywany automatycznie.

Powiedzmy że mamy utworzy obiekt 'user1'. Dostęp do jego właściwości uzyskujemy dzięki notacji kropki (.).

print(f"Witaj {user1.firstname} {user1.lastname}")
# >>> Witaj Jan Nowak

1.20.2. Metody

Mamy już cechy szczególne, to teraz pora na czynności jakie ten obiekt może wykonać. Do opisu czynności obiektu wykorzystuje się metody, czyli funkcje w kontekście obiektowym. Z pierwszą metodą mieliśmy już do czynienia przy okazji metody __init__, może jest ona nieco przezroczysta, jednak to nadal metoda. W poniższym przykładzie stworzymy sobie metodę, która opisze nam tego użytkownika, bo na razie właściwości to suche dane.

def describe_user(self):
    print(f"Imie i nazwisko: {self.firstname} {self.lastname}")
    print(f"\t-wiek: {self.age}\n\t-płeć: {self.sex}")

Metoda tym różni się od funkcji, że znajduje się wewnątrz klasy oraz tym, że zawsze ma parametr self, który pozwala jej na dostęp do właściwości obiektu. Metody wywołujemy prawie przez cały czas pod czas zabawy Pythonem, więc wywołanie metody naszej klasy nie powinno być problemem.

user1.describe_user()

1.20.3. Praca z atrybutami obiektów.

Właściwości/atrybuty obiektu nie muszą być zawsze przekazywane przez użytkownika podczas tworzenia obiektu mogą przyjmować wartości domyślne w zdefiniowane wewnątrz metody __init__.

class User():
    def __init__(self, firstname, lastname, age, sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex
        self.rank = 'Junior user'

Wraz z biegiem programu opartego na obiektach ich właściwości mogą, czasami wręcz muszą się zmienić. W Pythonie możemy to zrealizować na trzy sposoby.

Sposób 1:

user1.rank = 'Standard user'

Sposób 2:

class User():
    def __init__(self, firstname, lastname, age, sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex
        self.rank = 'Junior user'

    def promote_user(self, new_rank):
        self.rank = new_rank

user1 = User('Jan', 'Nowak', '47', 'M')
user1.promote_user('Standard user')

Sposób 3:

class User():
    def __init__(self, firstname, lastname, age, sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex
        self.rank = 'Junior user'

    def promote_user(self, new_rank):
        self.rank = new_rank

    def happy_birthday(self):
        print(f"Happy Birthday {self.firstname.title()}")
        self.age += 1

user1 = User('Jan', 'Nowak', '47', 'M')
user1.promote_user('Standard user')
user1.happy_birthday()

1.20.4. Dziedziczenie

Mamy sobie naszych użytkowników. Jednak potrzebujemy specjalnego użytkownika, który będzie tym wszystkim zarządzał - Administratora. Problem napotykamy wtedy kiedy chcemy zdefiniować jakieś ekstra metody dla admina, ale nie chcemy umieszczać ich w tej samej klasie do definiowania użytkowników, musimy również pamiętać, że admin to też użytkownik, i wypadało by mieć w bazie jego dane. Przekopiowanie kodu jest nieefektywne, ale z pomocą przechodzi na dziedziczenie, czyli tworzenie klas potomnych na podstawie istniejącej już klasy.

Przy tworzeniu klas potomnych warto pamiętać o jednej rzeczy. Klasa potomna jest to oddzielna klasa zawierająca połączenie z klasą nadrzędną. Klasy potomne mogą inicjować dla swoich obiektów właściwość klasy nadrzędnej oraz obiekty klas potomnych mogą korzystać z metod klasy nadrzędnej. Definicja takiej klasy wygląda następująco.

class User():
    def __init__(self, firstname, lastname, age, sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex
        self.rank = 'Junior user'
        self.quota = 100_000_000

    def promote_user(self, new_rank):
        self.rank = new_rank

    def happy_birthday(self):
        print(f"Happy Birthday {self.firstname.title()}")
        self.age += 1

    def check_quota_type(self):
        if self.quota < 0:
            print('Unlimited')
        else:
            print('100MB')

class Admin(User):
    def __init__(self, firstname, lastname, age, sex):
        super().__init__(firstname, lastname, age, sex)
        self.quota = -1
        self.rank = 'Admin'

    def ban_user(self, username):
        print(f'Użytkownik {username} został zbanowany')

Cechami po których można poznać że dana klasa jest klasą potomną, jest to, że w parametrach klasy znajduje się nazwa klasy nadrzędnej. W metodzie __init__ występuje metoda super(), której celem jest realizowanie połączenia pomiędzy klasą potomną a klasą nadrzędną. Użycie metody super() zależy od konstrukcji klasy nadrzędnej. Utworzenie takiego obiektu różni się tylko nazwą klasy.

root = Admin('Jan' 'Kowalski', 25, 'M')
root.happy_birthday()
# Happy Birthday Jan
root.check_quota_type()
# Unlimited

Atrybuty mogą przechowywać wiele rzeczy, od pojedynczych wartość po obiekty innych klas. Istnieje możliwość wewnątrz klasy przekazać atrybutowi jako wartość egzemplarz obiektu. Rozważmy to że musimy każdemu z naszych użytkowników zdefiniować uprawnienia, co im wolno, a co nie. Możemy spojrzeć na zestaw uprawnień jak na obiekt.

class User():
    def __init__(self, firstname, lastname, age, sex):
        self.firstname = firstname
        self.lastname = lastname
        self.age = age
        self.sex = sex
        self.rank = 'Junior user'
        self.quota = 100_000_000
        self.permissions = Permissions('User')

    def promote_user(self, new_rank):
        self.rank = new_rank

    def happy_birthday(self):
        print(f"Happy Birthday {self.firstname.title()}")
        self.age += 1

    def check_quota_type(self):
        if self.quota > 0:
            print('Unlimited')
        else:
            print('100MB')

class Permissions():
    def __init__(self, user_type):
        if user_type == 'Administrator':
           self.permissions = ['może dodać post',
               'może usunac post',
               'może zbanować użytkownika']
        else:
           self.permissions = ['może dodać post', 'może usunać post']

    def show_permissions(self):
        for permission in self.permissions:
            print(f"- {permission}")


class Admin(User):
    def __init__(self, firstname, lastname, age, sex):
        super().__init__(firstname, lastname, age, sex)
        self.quota = -1
        self.rank = 'Admin'
        self.permissions = Permissions('Administrator')

    def ban_user(self, username):
        print(f'Użytkownik {username} został zbanowany')

root = Admin('Jacek', 'xf0r3m', 25, 'M')
root.permissions.show_permissions()
print('\n')
user1 = User('Jan', 'Nowak', 47, 'M')
user1.permissions.show_permissions()

W powyższym przykładzie pokazano jak można użyć obiektu jako wartości atrybutu innego obiektu oraz jak uzyskać dostęp do metod takiego obiektu. root jest obiektem w klasie Admin, następnie po kropce podajemy nazwę atrybutu w tym przypadku jest permissions, aby po następnej kropce (z racji tego że wartość permissions to też obiekt) podajemy nazwę metody show_permissions() i w ten sposób uzyskaliśmy dostęp do zdefiniowanych w innym obiekcie uprawnień przypisanych do obiektów użytkowników jako atrybuty.

Klasy podobnie jak funkcje też można umieszczać w modułach, klasy umieszcza się w modułach w identyczny sposób jak funkcje. Identycznie też się je importuje. Kilka akapitów wyżej jest opis jak to zrobić.

1.21. Biblioteka standardowa

Wiele modułów zwierających pomocne klasy oraz funkcje jest dostarczanych wraz z Python jako standardowe biblioteki. Wśród nich możemy znaleźć taki moduł jak random, który zawiera funkcje służące do generowania liczb pseudolosowych - randint() oraz np. choice(), która zwraca nam losowo wybraną wartość z listy lub krotki.

>>> from random import choice
>>> cars = ['Shelby Cobra', 'Chevy', 'Pontiac', 'Dodge', 'Mustang']
>>> today_car = choice(cars)
>>> today_car
'Dodge'

1.22. Obsługa plików

Python tak wiele języków programowania umożliwia pracę z plikami. Dostęp do pliku uzyskujemy w dość dziwny, ale bezpieczny (dla pliku) oraz efektywny sposób.

with open('filename.txt') as file_object

Samemu otwarciu pliku służy funkcja open(), jednak jeśli skorzystalibyśmy z niej bez tej specyficznej obudowy, po zakończeniu wykonywania na nim jakiś operacji, musielibyśmy go zamknąć. Tylko pytanie, kiedy? Po ostatniej czynności na nim wykonanej? Nie wiadomo. Dlatego też w Pythonie pliki otwiera się za pomocą słowa kluczowego with, które to zamyka plik kiedy przestaje być potrzebny, czy to naszemu programowi, czy to Pythonowi podczas interpretacji kodu. Sekcja as file_object, będzie nam przedstawić odniesie do pliku jako obiekt file_object. Wielu przypadkach możemy spotkać się ze skróceniem file_object do f. Czynności wykonywane na pliku są umieszczane w bloku kodu pod linią otwarcia pliku z użyciem słowa kluczowego with.

W powyższym przykładzie, wewnątrz funkcji open() znajduje się argument o nazwie filename.txt, jest to ścieżka dostępowa do pliku, która może być względna - odnosić się od danego miejsca, lub też bezwzględna - pełna ścieżka od najwyższego katalogu na systemie plików w systemach uniksopodobnych (root, katalog głowny - /), w systemach MS Windows (Katalog dysku np. c:\ itp.). Argument filename.txt jest ścieżką względną, ponieważ odnosi się od CWD (obecnego katalogu roboczego - katalogu, w którym się znajdujemy, mamy otwarte IDE lub w katalogu lub w którym wykonuje się skrypt Pythona). W MS Windows stosujemy ten sam ukośnik (/) co w systemach uniksopodobnych, lub jeśli się już upieramy przy Windowsowych lewych ukośnikach (\), to musimy podać je podwójnie, ponieważ lewy ukośnik w Python oznacza rozpoczęcie białego znaku.

Dane z plików możemy odczytać na trzy różne sposoby.

Sposób 1:

with open('filename.txt') as f:
    plik = f.read()

Warto zwrócić uwagę na dodatkowy ostatni pusty wiersz. Metoda read() po napotkaniu końca pliku zwraca pusty wiesz. Z racji tego, że pusty wiersz (znak przejścia do nowej linii '\n') to biały znak, cały plik ściągnęliśmy do zmiennej w postaci pojedynczego ciągu tekstowego, to oznacza to, że znak znajduje się po prawej stronie ciągu, więc za pomocą metody operującej na ciągach tekstowych rstrip() możemy usunąć ten pusty wiersz.

Sposób 2:

with open('filename.txt') as f:
    for line in f:
        print(f"{line.replace('Python', 'C')}")

Metoda replace() zamienia wszystkie wystąpienia w ciągu pierwszego argumentu na drugi argument.

Sposób 3:

with open('filename.txt') as f:
    plik = f.readlines()

for linia in plik:
    print(f"{linia.replace('Python', 'C')}")

Metoda readlines() zwraca nam listę wierszy znajdujących się w pliku.

1.22.1. Funkcja open

Funkcja open() pozwala na wybranie atrybutu dla otwieranego pliku.

Zapis danych w pliku polega na otwarciu pliku do zapisu lub dopisywania używając wyżej wymienionych atrybutów jako drugiego argumenty funkcji open(). Plik nie musi istnieć na podanej ścieżce, zostanie utworzony w momencie zamknięcia odniesienia prze słowo kluczowe with

with open('filename.txt', 'w') as f:
    f.write('xf0r3m')

with open('filename.txt') as f:
    plik = f.read()

print(f"{plik}")
# xf0r3m

1.23. Obsługa błędów

Python posiada konstrukcję do obsługi błędów, to znaczy, że jeżeli w pewnym bloku umieścimy kod (technicznie, umieszcza się tam tylko jedną linię do której nie możemy być pewni, że uruchomienie jej nie spowoduje błędu), to jeśli spowoduje on błąd określony w drugiej części konstrukcji, zostanie wykonany kod z bloku tej drugiej części. Tą konstrukcją jest try-except. Koronnym przykładem jest chyba dzielenie przez zero.

try:
    print(8 / 0)
except ZeroDivisionError:
    print("Nie wolno dzielić przez 0")

Jeśli kod w bloku try wykona się bez błędu, to zostanie wyświetlony wynik z dzielenia. Jednak nigdy nie się to nie stanie. Ponieważ wykonanie linii print(8/0) spowoduje wygenerowanie błędu division by zero. Błędy w programowaniu możemy nazwać wyjątkami od wykonania. Każdy błąd jest częścią obiektu wyjątku w tym przypadku obiektem wyjątku dla błędu jest ZeroDivisionError. Skąd to wiadomo ? Otóż z komunikatów zwróconych przez sam Python jako stos wywołań. Ostatnia linia komunikatu o błędzie. Mówi nam: 'obiekt wyjątku: błąd'. Aby obsłużyć błąd, w linii except podajemy obiekt wyjątku, następnie w jej bloku definiujemy kod, który ma się wykonać kiedy Python napotka błąd tego typu. Jeśli potrzebujemy obiektu wyjątku, to spróbujmy zmusić tę niepewną linię do wygenerowania błędu bez jego obsługi. Python poda nam nazwę obiektu wyjątku.

W skład tej konstrukcji wchodzi jeszcze else, w tym bloku definiowany jest kod, który jest wykonywany, kiedy kod w bloku try nie spowoduje błędu.

a = input("Podaj pierwszą liczbę: ")
b = input("Podaj drugą liczbę: ")

try:
    result = int(a) + int(b)
except ValueError:
    print("Jedna z podanych wartości jest tekstem nie liczbą!")
else:
    print(f"{result}")

Często będzie tak, że nie będziemy chcieli wykonywać żadnego kodu, ani informować nikogo o tym, że wystąpił wyjątek. Wtedy możemy skorzystać ze słowa kluczowego pass, którego celem jest rezerwacja miejsca w bloku, na potrzeby późniejsze rozbudowy. Użycie instrukcji pass możemy tłumaczyć jako do nothing. Z racji tego, że instrukcja pass pozwala na ciche przyzwolenie na błędy. Musimy zdecydować kiedy użytkownik naszego programu powinien być informowany o błędzie. Na pewno wtedy gdy wyjątek powoduje jakaś informacja, która pochodzi od niego samego.

1.24. Wykorzystanie formatu JSON w Pythonie

Z racji tego iż mamy omówione pliki oraz wyjątki, czyli możemy definiować kod, który obsłuży sytuacje, gdy próbujemy odczytać plik który nie istnieje. Możemy zapisywać dane z naszych programów do pliku, a przy następnym uruchomieniu je wczytywać. Do tego celu wykorzystamy moduł json, który umożliwia użycie formatu JSON (JavaScript Object Notation) do przechowywania danych. A w szczególności dwie funkcje, dump() oraz load().

Funkcja dump() jak argumenty pobiera, dane (jako pierwszy argument pozycyjny) np. zmienną, równie dobrze może być to lista. Drugim argumentem jest odniesie do pliku.

import json

username = 'xf0r3m'

with open('filename.json', 'w') as f:
    json.dump(username,f)

Dane zostaną za pisane w pliku. Nie koniecznie w formacie JSON jaki znamy z JavaScript. Idealnie funkcja dump() na JSON przerabia słowniki, którym chyba najbliżej do tego formatu. Odczyt danych z takiego pliku jest wykonywany jest za pomocą funkcji load().

import json

with open('filename.json') as f:
    username = json.load(f)

print(f"{username})
# xf0r3m

1.25. Testy jednostkowe

Python za pomocą modułów biblioteki standardowej umożliwia nam przeprowadzenie testów jednostkowych dla stworzonych przez nas funkcji oraz klas. Jeśli chcemy tworzyć naprawdę dobry kod, i to nie zależnie od języka powinniśmy go testować. Nie mówię o tworzeniu testów jednostkowych dla hello world, ale kiedy nasz projekt dość pokaźnie urósł. Zawiera klasy lub/i funkcję, to powinien on zawierać testy tych konstrukcji. Na czym polega test jednostkowy? Najprościej rzecz biorąc na sprawdzeniu czy wartość zwracana przez funkcje lub atrybut modyfikowany przez metodę jest równy z predefiniowaną przez nas wartością, którą uznajemy za poprawną. Takie porównanie nazywamy asercją. Zakładamy, że wartość użyta do asercji jest równa zdefiniowanej wartości. Jeśli tak jest to test zostaje zaliczony, w przeciwnym wypadku wykonanie zostaje przerwane (w przypadku asercji jako funkcji). Asercja w Pythonie nie organiczna się tylko do sprawdzenia równości. Poniżej znajduje się tabelka z możliwymi funkcjami asercji zaimplementowanymi w module unittest. To ten moduł odpowiada za testy jednostkowe.

Metoda Opis
assertEqual(a, b) Sprawdza czy a == b
assertNotEqual(a, b) Sprawdza czy a != b
assertTrue(x) Sprawdza czy x przyjmuje wartość True
assertFalse(x) Sprawdza czy x przyjmuje wartość False
assertIn(element, lista) Sprawdza czy element jest na liście
asserNotIn(element, lista) Sprawdza czy element nie znajduje się na liście
Metody asercji oferowane przez moduł unittest

1.25.1. Testy jednostkowe funkcji

Na potrzeby przedstawienia testów zaczniemy od zdefiniowania sobie w module (city_functions.py) prostej funkcji 'city_countries', która pobiera od użytkownika nazwę miast oraz nazwę kraju, w którym to miasto leży następnie zwraca ciąg np. dla danych Warszawa Polska: Warszawa, Polska.

def city_countries(city, country):
  cc = f"{city.title()}, {country.title()}"
  return cc

Aby wywołać taką funkcje z racji tego, że jest plik modułu w nowym pliku musimy zaimportować moduł i wywołać funkcję:

from city_functions import city_countries
city_countries('Warszawa', 'Polska')
# Warszawa, Polska

Powiedzmy z racji tego, że jest to funkcja dużego projektu, musimy stworzyć dla niej test jednostkowy. Testy definiuje się w klasie potomnej klasy TestCase modułu unittest. Jedna metoda w wewnątrz klasy = jeden test. Taka klasa staje się zbiorem testów. Kiedy dla każdej funkcji w naszym projekcie zdefiniujemy chociaż jeden test, to możemy mówić wówczas o całkowitym pokryciu zbioru testów. Zatem dla testów jednostkowych utworzymy nowy plik, nazwiemy go test_cities.py

import unittest
from city_functions import city_countries

class TestCitiesFunctions(unittest.TestCase):

    def test_city_countries(self):
        cc = city_countries('Warszawa', 'Polska')
        self.assertEqual(cc, 'Warszawa, Polska')

unittest.main()

importujemy moduł unittest, następnie importujemy naszą funkcję. Tworzymy klasę potomną klasy unittest.TestCase, w tym przypadku o nazwie TestCitiesFunctions. Wewnątrz niej jako metodę definiujemy test. Funkcja main() modułu unittest uruchomi automatycznie każdą metodę w zbiorze, których nazwy zaczynają się od ciągu test_, zatem nie potrzeba jej wywoływać. Wewnątrz metody wywołujemy naszą funkcję zachowując jej wartość zwracaną w zmiennej cc następnie w jednej z metod asercji w tym przypadku assertEqual podstawiamy otrzymaną wartość następnie wartość, którą naszym zdaniem powinniśmy otrzymać od funkcji dla podanych w wywołaniu argumentów. Na końcu uruchamiamy zbiór testów. Wynik testu znajduje się poniżej.

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

W prawdzie nie wiele informacji, choć nas powinna zaciekawić pierwsza linijka, zwracany jest w niej rezultat testu, jeśli widzimy kropkę (.), oznacza to że test zakończył się sukcesem. Innymi wartościami zwrotnymi jest wielka litera F, oznaczająca nie powodzenie asercji, otrzymaliśmy inny wynik niż zakładaliśmy, oraz wielka litera E, oznaczająca błąd gdzieś w kodzie. Np. jeśli rozbudowujemy funkcje o kolejne parametry, ale już zapomnimy w teście tych parametrach.

Rozbudujemy naszą funkcję o kolejny parametr pozwalający przedstawić populacje.

def city_country(city, country, population):
    cc = f"{city.title()}, {country.title()} - populacja {population}"
    return cc

Próbując wykonać test dla tej funkcji dostaniemy o taką odpowiedź:

E
======================================================================
ERROR: test_city_country (__main__.TestCitiesFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/xf0r3m/python/test_cities.py", line 8, in test_city_country
    cc = city_country('Warszawa', 'Polska')
TypeError: city_country() missing 1 required positional argument: 'population'

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (errors=1)

Błąd polega na tym, że zapomnieliśmy jednym argumencie, jeśli zmienimy ten argument na opcjonalny, wtedy test zostanie pomyślnie wykonany. Poniżej zamieszczam również dane zwracane, kiedy wartość rzeczywista różni się od przez nas założonej, pojawia się wtedy błąd asercji.

F
======================================================================
FAIL: test_city_country (__main__.TestCitiesFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/xf0r3m/python/test_cities.py", line 9, in test_city_country
    self.assertEqual(cc, 'Warszawa, Polska')
AssertionError: 'Warszawa, Polska - populacja ' != 'Warszawa, Polska'
- Warszawa, Polska - populacja
+ Warszawa, Polska


----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

Oczywiście kiedy zdefiniujemy kolejne testy, to będą pojawiać się w pierwszej linijce kropki lub litery E czy F, wszystko w zależności od wyniku testu.

1.25.2. Testy jednostkowe metod

Testy jednostkowe możemy tworzyć nie tylko dla funkcji ale i dla klas (bardziej metod). Przygotowałem poniżej klasę modelującą pracownika w swoich atrybutach zawiera imię, nazwisko oraz roczne zarobki. W klasie znajduje się również metoda odpowiedzialna za przyznawanie podwyżki, domyślną wartością podwyżki jest 5000. Naszym zadaniem jest stworzenie dwóch testów sprawdzających ową metodę. Jak w przykładzie z funkcją, klasę umieszczamy w odrębnym pliku modułu.

Plik: pracownik.py

class Employee():

    def __init__(self, imie, nazwisko, y_salary):
        self.imie = imie
        self.nazwisko = nazwisko
        self.y_salary = y_salary

    def give_raise(self, amount_raise=5000):
        self.y_salary += amount_raise

Poniżej znajduje się plik ze zbiorem testów. Dla klas tworzymy go w identyczny sposób jak dla funkcji, z jedną różnicą. Dla poprawności testu, każdy test powinien otrzymać oddzielny egzemplarz klasy, aby nie powtarzać kodu tworzenia egzemplarza, z pomocą przychodzi nam metoda unittest o nazwie setUp. Kiedy definiujemy taką metodę w wewnątrz zbioru testów, powinna zawierać kod przeznaczony do utworzenia egzemplarza danej klasy. Ta metoda będzie wywoływana przez Python przed każdym wywołaniem testu.

Plik: test_employee.py

import unittest
from pracownik import Employee

class EmployeeTest(unittest.TestCase):

    def setUp(self):

        imie = 'Jan'
        nazwisko = 'Nowak'
        y_salary = 140_000

        self.new_employee = Employee(imie, nazwisko, y_salary)

    def test_give_default_raise(self):

        self.new_employee.give_raise()
        self.assertEqual(self.new_employee.y_salary, 145_000)

    def test_give_custom_raise(self):
        self.new_employee.give_raise(25_000)
        self.assertEqual(self.new_employee.y_salary, 165_000)

unittest.main()

Naszymi testami było sprawdzenie metody odpowiedzialnej za przyznawanie podwyżek. Pierwszy test to podwyżka o wartość domyślnej a drugi to podwyżka zdefiniowana przez użytkownika. Danymi zwracanymi przez Python dla uruchomienia tych testów są:

..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

1.26. Koncepcje stylu kodu źródłowego

Poniżej przedstawiam koncepcje stylu nadawanego zapisom kodu źródłowego, ponieważ Now is better than never oraz Beautiful is better than ugly.

Kilka reguł, których należy stosować pod czas używania zmiennych w Pythonie.

Zmienne w Pythonie zapisujemy z małych liter.

Odnośnie komentarzy, to warto sobie opisywać jakieś bardziej złożone fragmenty naszego programu, żeby później, kiedy powrócimy do kodu po jakimś czasie, nie tracić czasu na próbę zrozumienia co autor miał na myśli.

Generalnie konwencje stylu, będą stosowane nie po to aby kod był łatwy do zapisu, ale do odczytu właśnie. Jest dokument w sieci nazywa się PEP 8 (Python Enhancement Proposal), który opisuje sposoby nakładania stylu na tworzony kod. Pierwszą rzeczą którą jest podczas zapisu kodu łatwego do odczytu, są wcięcia. W Pythonie sprawa jest nieco paradoksalna, ponieważ nie możemy sobie szastać wcięciami na prawo i lewo, ponieważ wcięcia często oznaczają blok kodu, ale jednocześnie przez to Python sam nakazuje nam stosowanie wcięć przy definiowaniu bloku kodu. Specyfikacja PEP 8 zaleca użycia czterech spacji jako jednego poziomu wcięcia. Najnowsze wersje edytorów ustawiają jeden znak tabulacji na wielkość czterech spacji. Kolejną rzeczą jest długość wiersza, która nie powinna przekraczać 80 znaków takie zalecenie później ułatwi nam pracę kiedy na jednym ekranie będziemy musieli mieć podzielone okno edytora na kilka plików naraz. W oknie edytora możemy zobaczyć taką pionową kreskę, właśnie ona stoi na miejscu 80-tego znaku. Taka linia powinna być już włączona, jeśli nie jest to zawsze można ją włączyć. Do organizacji kodu w schludny plik tekstowy mogą pomóc nam puste wiersze. Nie należy ich jednak nadużywać. Metodę jaką ja stosuje jest rozdzielanie kodu ze względu na jego kontekst. Np. mam sobie wywołanie funkcji, która zwraca mi wartość do zmiennej - jest jedna linia. Teraz chcę to wypisać - to jest druga linia operująca na tych samych danych, czyli umieszczam ją jedna pod drugą. Kiedy muszę wywołać podobnie inną funkcję muszę robię linię odstępu przed kolejnymi instrukcjami.

PEP 8 opisuje również styl dla testów warunkowych. W tego zalecenia powinniśmy rozdzielać operandy od operatora spacją, zarówno w test prostych jak i złożonych.

Podczas nadawania stylu funkcjom powinniśmy pamiętać o kilku kwestiach. Nazwa funkcji powinna sugerować jej przeznaczenie i składać się z małych liter i znaków podkreślenia. W kodzie każdej funkcji powinien znajdować się komentarz zaraz na początku bloku kodu funkcji, dla komentarza należy użyć formatu docstring. W przypadku definiowania parametrów domyślnych lub też opcjonalnych po obu stronach znaku równości nie należy umieszczać żadnych spacji. Jeżeli parametry w definicji nam się nie mieszczą 80 znakach, to możemy bez trudu złamać tą linię za przecinkiem, dać dwa znaki tabulatora aby nie zostało to potraktowane jako blok funkcji i kontynuować zapisywanie parametrów na końcu zamykając nawias, podając znak dwukropka. Jeśli przechowujemy naszą funkcję w module i ta funkcja nie jest jedyna w tym module, to możemy poszczególne funkcje w module rozdzielić dwoma pustymi wierszami.

Nazwa klasy powinna stosować styl CamelCase: każde słowo nazwy pisane wielką literą, brak znaków podkreślenia. Z kolei nazwy egzemplarzy klas powinny być zapisane małymi literami, a wykorzystanie w tych nazwach znaków podkreślenia między słowami jest dopuszczalne. Na początku każdej klasy należy umieścić komentarz typu docstring wyjaśniający przeznaczenie klas znajdujących się w danym module. W kodzie klasy umieszczamy pusty wiersz pomiędzy metodami. Klasy w module rozdzielamy dwoma pustymi wierszami.

Importując moduły z biblioteki standardowej oraz swoje, w pierwszej kolejności należy podać polecenia import modułów biblioteki a dopiero później swoje moduły.

Ostatnim pojęciem ze stylów będzie refaktoryzacja. Jest to podział dużego kodu na funkcje wykonujące zazwyczaj konkretne zadanie.

Tutaj kończy się część pierwsza, czyli poznawanie Pythona, jego składni, poleceń i konstrukcji. Teraz przejdziemy do konkretnego zagadnienia, gdzie raczej będziemy analizować studium przypadku. Przypadku tworzenia aplikacji internetowej, podobnej do tej jaką tworzy się za pomocą takich języków jak np. PHP.

Koniec części pierwszej.

Część 2: Aplikacje internetowe

Django to zestaw narzędzi (czyt. framework) pozwalający na tworzenie aplikacji internetowych/interaktywnych witryn za pomocą Pythona.

Tworząc witrynę czy też aplikacje w Django, tworzymy projekt. Każdy projekt składa się z wielu mniejszych części zwanych aplikacjami stworzonych za pomocą Pythona według zasad Django. Każdy porządny projekt powinien mieć jasno określone założenia, na których skupiają się programiści podczas pracy, taki określony zbiór cech i funkcjonalności nazywamy specyfikacją.

Środowisko wirtualne

Rozpoczynając pracę z Django musimy skonfigurować środowisko wirtualne, w którym będziemy mogli dowolnie instalować rzeczy, nie powodując bałaganu w modułach Pythona na naszym systemie. Każde środowisko można bez problemu usunąć wydając odpowiednie dla systemu polecenie powłoki służące do usuwania plików. W tej części będę przedstawiał polecenia z systemu Linux, dystrybucji opartych o Debian, takich jak np. Ubuntu. Przepraszam za pominięcie innych systemów. Konfiguracje środowiska wirtualnego zaczynamy od zainstalowania modułu w systemie.

$ sudo apt install python3-venv

Po zainstalowaniu pakietu, dla większego porządku tworzymy sobie katalog, w którym będziemy przechowywać nasz projekt. Wewnątrz katalogu wydajemy poniższe polecenie.

$ python3 -m venv project_env

Za project_env możemy podstawić swoją nazwę. Po wydaniu tego polecenia Python utworzy katalog project_env, w którym będą przechowywane wszystkie pliki utworzonego środowiska wirtualnego. Na tym konfiguracja się kończy. Kiedy chcemy uruchomić nasze środowisko wydajemy poniższe polecenie.

$ source project_env/bin/activate

Polecenie powinno zakończyć działanie zaraz po uruchomieniu nie zwracając żadnych danych. Jedyną rzeczą, która się zmieni jest prompt (znak zachęty), przednim będzie znajdował się nawias, zawierający nazwę naszego katalogu ze środowiskiem wirtualnym.

(project_env) xf0r3m@macbook:/python/django$

Środowisko możemy wyłączyć wydając polecenie:

(project_env) xf0r3m@macbook:/python/django$ deactivate

lub po prostu zamykając powłokę, w której je aktywowaliśmy. Po uruchomieniu środowiska będziemy używać polecenia python, to polecenie będzie odnosić się do wersji Pythona jakiej użyliśmy to stworzenia środowiska, w tym przypadku nie będziemy używać polecenia python3. Teraz możemy przejść do instalacji Django. Wydajemy polecenie w aktywowanym środowisku wirtualnym:

Podstawy pracy z frameworkiem Django

2.2.1. Instalacja django

(project_env) xf0r3m@macbook:python/django$ pip install django

Kiedy polecenie skończy działanie, przechodzimy do utworzenia naszego projektu.

2.2.2. Stworzenie nowego projektu w Django

Powiedzmy że stworzymy stronę pizzerii, gdzie będzie można zamówić pizzę i przejrzeć menu dla zamówień telefonicznych. Aby utworzyć projekt wydajemy poniższe polecenie.

(project_env) xf0r3m@macbook:python/django$ django-admin startproject pizzeria .

W tym poleceniu najważniejsza jest kropka na końcu powodująca, że projekt zostanie utworzony w katalogu, w którym się znajdujemy. Brak kropki spowoduje utworzenie projektu w jeszcze jednym wewnętrznym katalogu. Spoglądając na pliki umieszczone w naszym folderze powinniśmy dostrzec plik manage.py, folder środowiska wirtualnego zakończony sufiksem _env oraz folder o nazwie pizzeria zawierający pliki naszego projektu.

2.2.3. Tworzenie bazy danych dla projektu

Jak wszyscy wiemy, aplikacje internetowe często przetwarzają jakieś dane. Te dane należy gdzieś składować, najpopularniejszą metodą magazynowania danych przez aplikacje sieci WWW są bazy danych. Django również korzysta z baz danych, a my wykorzystamy najprostszą z nich, w postaci jednego pliku - bazy SQLite3. W przypadku innych systemów bazodanowych niezbędna będzie instalacja dodatkowych modułów Pythona. Na tym etapie po prostu stworzymy bazę danych dla naszego projektu aby wszystko funkcjonowało poprawnie. Wydajemy polecenie:

(project_env) xf0r3m@macbook:python/django$ python manage.py migrate

W Django, czynności na bazie wykonujemy za pomocą opcji migrate skryptu manage.py. Teraz kiedy nasza baza nie istnieje, polecenie migrate spowoduje jej utworzenie.

2.2.4. Uruchomienie serwera projektu

Teraz możemy uruchomić nasz serwer aplikacji. Wydajemy polecenie:

(project_env) xf0r3m@macbook:python/django$ python manage.py runserver

Po uruchomieniu tego polecenia, z danych przeznie zwracanych powinniśmy dowiedzieć się, że nasz serwer nasłuchuje na pętli zwrotnej (localhost/127.0.0.1) na porcie 8000. Możemy przejść pod ten adres za pomocą przeglądarki, a naszym oczom ukaże się strona domyślna projektu, predefiniowana przez Django.

2.2.5. Tworzenie aplikacji Django

Jak wiemy z początkowych akapitów, na projekt w Django składają się na mniejsze części nazywane aplikacjami, często odpowiadają one za konkretne rzeczy. Teraz stworzymy aplikacje odpowiedzialną za nasze pizze i ich dodatki.

(project_env) xf0r3m@macbook:python/django$ python manage.py startapp pizzas

Po wykonaniu tego polecenia na chwilę się zatrzymamy. Na początku wyświetlimy sobie zawartość utworzonego katalogu aplikacji.

(project_env) xf0r3m@macbook:python/django$ ls pizzas
admin.py  __init__.py  models.py    templates  urls.py
apps.py   migrations   __pycache__  tests.py   views.py

2.2.6. Modele

Katalogiem templates zajmiemy się później, natomiast katalog __pycache__ nie jest w ogóle istotny, jest on tworzony przez Python podczas uruchamiania skryptu. Każda aplikacja w Django reprezentuje wzorzec model-view-template, na modele składają się reprezentacje danych w postaci klas. Weźmy sobie zatem taką pizzę, co może ją opisać? Na przykład nazwa. Każda pizza ma swoją nazwę, mamy zatem jakąś właściwość, która składa się na ten model, modele posiadają także metodę, która pozwala na reprezentowanie danego modelu. Kiedy wyciągniemy egzemplarz modelu z bazy, to odwołując się do niego uzyskamy, jakąś wartość. Może być to po prostu nazwa. Z technicznego punktu widzenia modele to klasy potomne, klasy Model z modułu django.db.models. Wewnątrz tej klasy znajdują się atrybuty, oraz metoda __str__. Modele przypominają tabele relacyjnych baz danych, nawet są do nich konwertowane podczas migracji, każda właściwość takiego modelu staje się kolumną w bazie. Modele definiujemy w pliku models.py w folderze aplikacji, a tak przedstawia się model naszej pizzy.

class Pizza(models.Model):
    name = models.CharField(max_length=30)

    def __str__(self):
        return self.name

W powyższym przykładzie mamy jeden atrybut znakowy (CharField) o długość 30 znaków. Odpowiada to SQL-owemu VARCHAR(30). Mamy również w naszej klasie metodę odpowiadającą za reprezentacje danego modelu. W tym wypadku zwracamy po prostu nazwę. Modele mogą być ze sobą połączone. Rozważmy przykład, posiadamy model pizzy i mamy model dodatku na pizzę, teraz chcieli byśmy dowiedzieć się jakie są dodatki dla konkretnej pizzy, aby metaforycznie położyć dodatki na pizzy skorzystamy z tak zwanego klucz obcego lub klucza zewnętrznego - odniesienia się do innego modelu.

class Toping(models.Model):
    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
    name = models.CharField(max_length=200)

    def __str__(self):
        return self.name

Za nasze połączenie opowiada metoda ForeignKey, która jako argumenty pobiera nazwę modelu, do którego będzie odnosić się właściwość pizza, drugi argument jest zachowaniem, w przypadku usunięcia egzemplarza pizzy, do którego odnoszą się egzemplarze dodatków, kiedy jest ustawiony na models.CASCADE, to gdy usuniemy pizzę to również powiązane z nią dodatki zostaną usunięte.

2.2.7. Instalacja aplikacji w projekcie

Kiedy mamy już zdefiniowane modele, to możemy je teraz wstawić do naszej bazy. Przed przeprowadzeniem migracji, trzeba ją najpierw przygotować. Jednak przed przygotowaniem migracji, należy na początku zainstalować naszą aplikacje, aby Python wiedział, że pizzas należy do projektu pizzeria i mógł w bazie danych projektu utworzyć tabele. Instalacja przebiega w następujący sposób, otóż w katalogu projektu odszukujemy plik settings.py. W nim szukamy zmiennej INSTALLED_APPS, a następnie umieszczamy nazwę naszej aplikacje w pojedynczych apostrofach na samym początku listy. Musimy pamiętać o przecinku po nazwie aplikacji.

INSTALLED_APPS = [
    'pizzas',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Dlaczego umieściliśmy naszą aplikacje, na samym początku listy? Lista zawiera aplikacje domyślnie instalowane przez Django w naszym projekcie aby zapewnić mu podstawową funkcjonalność, oraz zaoszczędzić czas na tworzenie aplikacji. Aplikacje te realizują identyczne zadania dla każdego projektu. Może jednak zdarzyć się tak że będzie trzeba nadpisać funkcje domyślnej aplikacji, dlatego też swoje aplikacje dopisujemy powyżej aplikacji domyślnych.

2.2.8. Zatwierdzanie modeli w bazie danych.

Aby przygotować migrację wydajemy poniższe polecenie:

(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas

Jeśli modele są poprawne pod względem składni, to przez skrypt zostaniemy poinformowani mniej więcej takim komunikatem.

Migrations for 'pizzas':
  pizzas/migrations/0001_initial.py
    - Create model Pizza
    - Create model Topping

Ten komunikat oznacza również wygenerowanie plik migracji. Wydajemy poniższe polecenie aby zatwierdzić nasze zmiany w bazie:

(project_env) xf0r3m@macbook:python/django$ python manage.py migrate

Wywołanie tego polecenia spowoduje utworzenie modeli. Warto zaznaczyć w tym momencie, że każda poważniejsza zmiana w modelu musi zostać zatwierdzona w bazie. Każdą zmianę na modelach możemy próbować migrować, najwyżej Python nie wygeneruje pliku migracji, ponieważ uzna, że zmiana, którą wprowadziliśmy nie była istotna dla modelu w bazie. Najlepiej kiedy jednak tworzymy model postarać się przemyśleć każdą właściwość, ponieważ zmiany wprowadzane w trakcie jego działania są nieco bardziej skomplikowane. Zostały one (dodawnie kolumny) opisane pod koniec strony podczas zabezpieczania naszego projektu. Przypuszczam że zmiany w bloku metody __str__ są takim zmianami, których nie trzeba zatwierdzać w bazie.

2.2.9. Zarządzanie modelami za pomocą wirtyny administracyjnej Django

Po dokonaniu migracji, możemy wprowadzić dane początkowe do naszych modeli, do wprowadzania danych początkowych, możemy posłużyć się witryną administracyjną dostarczaną nam przez Django. Jednak przed samym rozpoczęciem musimy wykonać dwie czynności. Pierwszą rzeczą jest utworzenie konta superużytkownika - użytkownika witryny administracyjnej. Wydajemy poniższe polecenie:

(project_env) xf0r3m@macbook:python/django$ python manage.py createsuperuser

Skrypt zapyta nas o kilka rzeczy. Mianowicie o nazwę użytkownika, która może zostać domyślna - nazwa użytkownika systemowego, który wydał polecenie utworzenia superużytkownika. Adres skrzynki mailowej, który jest opcjonalny. Ostatnią rzeczą jaką będziemy musieli podać to hasło. Dwukrotnie aby je potwierdzić. Po utworzeniu superużytkownika, możemy przejść do drugiej czynności.

Aby móc zarządzać danym z modeli za pomocą witryny administracyjnej, należy je najpierw zarejestrować. Modele rejestrujemy w pliku admin.py. Na początku importujemy nasz moduł z modelami za pomocą linii:

from .models import Pizza,Toping

Kropka przed models oznacza, że Python ma wziąć moduł z tego samego katalogu, w którym znajduje się interpretowany plik admin.py. Następnie przy pomocy metody register() obiektu admin.site rejestrujemy każdy z zaimportowanych modeli.

admin.site.register(Pizza)
admin.site.register(Toping)

Po zarejestrowaniu modeli możemy uruchomić nasz serwer deweloperski.

(project_env) xf0r3m@macbook:python/django$ python manage.py runserver

Otwieramy w przeglądarce następujący adres http://127.0.0.1:8000/admin . Następnie logujemy się na konto superużytkownika. Po zalogowaniu zobaczymy dwie sekcje w jednej są nasze modele, a w drugie modele użytkowników i grup. Na co chciałbym zwrócić uwagę, to fakt że nazwy modeli są zapisane w liczbie mnogiej. My tych zapisów nie generowaliśmy, Python sam sobie wywnioskował (dodał literę 's' i tyle). Jednak wiemy, że tworzenie odmian nazw języków ludzi w świecie komputerów od zawsze było problemem. Tak jest i w tym przypadku. Załóżmy że mamy (tylko tak dla przykładu), model o nazwie 'Entry'. Liczba mnoga od 'Entry' to 'Entries'. Python domyślnie stworzy 'Entrys'. Oczywiście zostało to przewidziane. W celu zdefiniowania prawidłowej nazwy dla liczby mnogiej możemy użyć podklasy (klasy w klasie, nie mylić z klasą potomną) Meta, która definiuje tak jakby atrybuty opisujące samą klasę, w niej definiujemy atrybut verbose_name_plural. Pamiętajmy o tym że klasa Meta musi być zawarta w innej klasie.

Wracając do dodawania danych do modeli za pomocą witryny administracyjnej. Dane dodajemy przy użyciu plusika - zostaniemy przeniesieni bezpośrednio do formularza wprowadzającego dane do bazy, albo klikając na nazwę modelu - zostaniemy przeniesieni do strony zawierającej egzemplarze tego modelu, gdzie klikamy przycisk "Add <nazwa_modelu> +". Kiedy dodamy jakiś egzemplarz (wypełniając formularz i klikając przycisk save), to podglądając listę egzemplarzy modelu to dane odnośnie egzemplarza jakie widzimy są właśnie wynikiem działania metody __str__. Jak widać panuje pełna dowolność, co zostanie nam przestawione jako reprezentacja danego modelu.

2.2.10. Powłoka Django

Po dodaniu danych, możemy przejść do powłoki Django. Tę powłokę możemy wykorzystać (a nawet powinniśmy) testowania zapytań do bazy. Powłokę uruchamiamy poleceniem.

(project_env) xf0r3m@macbook:python/django$ python manage.py shell

Przed przystąpieniem do wydawania zapytań, należy zaimportować do powłoki niezbędne nam modele. Najprostsze zapytanie wygląda następująco.

# Import modeli
from pizzas.models import Pizza, Toping

pizzas = Pizza.objects.all()

W poleceniu from, pizzas jest nazwą aplikacji. Zmienna pizzas będzie przechowywać listę (tzw. query set) wszystkich rekordów z tabeli (egzemplarzy modelu). Aby pobrać konkretny rekord użyjemy innej metody. Metoda get() pobiera jako argument warunek, na podstawie którego ma wybrać danych rekord, jeśli nie podamy warunku, to zostanie zgłoszony błąd że funkcja zwróciła więcej niż jeden egzemplarz.

pepperoni = Pizza.objects.get(name='Pepperoni')

Teraz mając jeden rekord w zmiennej, mamy możliwość sprawdzenia jakie są dodatki na naszej pizzy, odwołując się do innego modelu za pomocą naszego klucza zewnętrznego/obcego.

pepperoni_tops = pepperoni.toping_set.all()

Aby odwołać się do innego modelu za pomocą klucza obcego, używamy atrybutu naszej wydobytej już pizzy (bo to jej dodatki chcemy obejrzeć), który jest obiektem o nazwie składającej się z nazwy modelu do, którego się odwołujemy, napisanego za pomocą małych liter (Uwaga, zapis nazwy modelu małymi literami jest istotny!), zakończonego sufiksem (przyrostkiem) _set oraz metody, która wydobędzie nam rekordy. Tak wydobyte dane możemy sobie posortować, do sortowania służy metoda order_by() jako argument przyjmuje nazwę pola według jakiego ma sortować zwracane dane. Nazwę pola umieszczamy jako ciąg tekstowy, czy to w apostrofach, czy w cudzysłowie. Wynik sortowania możemy odwrócić, stawiając myślnik (-) przed nazwą pola, nadal w ciągu tekstowym.

pepperoni_tops = pepperoni.toping_set.order_by('-name')

Powłoka w naszej pracy z Django będzie odgrywać ważną rolę, dzięki niej możemy testować nasze zapytania zanim umieścimy je w widokach, co jest znacznie lepszym rozwiązaniem a niżeli używanie niepewnych zapytań.

2.3. Tworzenie aplikacji

Kiedy mamy omówione modele oraz zapytania to teraz możemy przejść do tworzenia naszych stron aplikacji. Tworzenie stron aplikacji możemy podzielić na trzy czynności: mapowanie adresów URL, utworzenie widoku oraz utworzenie szablonu strony. Skupimy się w tym momencie na stronie głównej. Strona główna powinna zostać wyświetlona, kiedy użytkownika poda adres strony np. https://terminallog.morketsmerke.net. Kiedy my podamy teraz adres naszego serwera dostaniem stronę z informacją o tym, że nasz projekt Django jest gotowy do pracy.

2.3.1. Mapowanie stron aplikacji

Aby moc mapować nasze adresy na konkretne strony, musimy przekierować adres serwera w pliku urls.py w katalogu naszego projektu na plik mapowania URL (urls.py) w katalogu naszej aplikacji. Jakby jeszcze tego było mało, Django mapując URL-e, całkowicie ignoruje adres serwera.

Na początku w katalogu naszej aplikacji pizzas tworzymy plik o nazwie urls.py. Wewnątrz pliku importujemy funkcje path z modułu django.urls oraz importujemy widoki (o widokach za chwilę). Następnie definiujemy zmienną app_name z nazwą aplikacji, pozwoli nam to na łatwe tworzenie odnośników, do innych podstron tej aplikacji, następnie wewnątrz listy urlpatterns deklarujemy nasze mapowania, zawartość pliku powinna wyglądać mniej więcej tak.

"""Definiuje wzorce adresów URL dla pizzas."""

from django.urls import path

from . import views

app_name='pizzas'
urlpatterns = [
    path('', views.index, name='index'),
]

Funkcja path przyjmuje jak widzimy powyżej trzy argumenty. Pierwszy z nich, jest ciąg, który zostanie przekazany przez przeglądarkę w momencie żądania danej strony. Teraz ten ciąg jest pusty, ponieważ po przez adres serwera będziemy żądać strony głównej naszej aplikacji. Drugim argumentem jest funkcja widoku, której zadaniem jest przetworzenie i przygotowanie danych do użycia w szablonie. Jeśli mówimy o czynnościach jakie wykonuje dana aplikacja internetowa w Django, to mówimy o czynnościach wykonywanych właśnie przez kod zdefiniowany ciele funkcji widoku. Przeważnie jedna podstrona to jedna funkcja widoku. Funkcja widoku jest uruchamiana w momencie gdy żądanie zostanie dopasowane do pierwszego argumentu funkcji path. Ostatnim argumentem jest nazwa zmiennej używana przy tworzeniu odnośników w szablonach, Python podstawi wartość pierwszego parametru jeśli tworząc odnośnik odwołamy się do tej zmiennej.

2.3.2. Tworzenie widoku

Po zdefiniowaniu mapowania, czas na drugą czynność. Na zdefiniowanie kodu funkcji widoku. Przechodzimy do pliku views.py następnie tworzymy nową funkcję o nazwie index przyjmującej parametr o nazwie request. Z racji tego że jest to strona główna, to będzie się ona opierać głównie na szablonie. Więc tutaj nasza funkcja widoku, będzie mieć tylko jedną linę, kiedy otworzymy plik views.py pierwsza linia zawiera import funkcji render, odpowiada ona za wygenerowanie gotowego pliku HTML z szablonu, który zrozumie każda przeglądarka. Funkcja render przyjmuje w tym przypadku dwa argumenty. Żądanie przechowywane w parametrze request oraz ścieżkę dostępu do szablonu. Plik views.py powinien wyglądać następująco.

from django.shortcuts import render

# Create your views here.

def index(request):
    """Strona główna aplikacji Pizzas"""
    return render(request, 'pizzas/index.html')

2.3.3. Tworzenie szablonu

Ze ścieżką dostępu do szablonu jest o tyle zabawnie że, aby szablon był widziany przez Django musi być umieszczony w katalogu aplikacji następnie w podkatalogu templates, w którym znajduje się kolejny podkatalog o nazwie aplikacji, a w nim dopiero szablon. Pliki szablonów to tak naprawdę pliki HTML, które zawierają znaczniki szablonu, dzięki którym możemy wprowadzać nasze dane na strony witryny, ale również w prosty sposób nią zarządzać. Szablon strony głównej nie zawiera wiele informacji, nie zawiera nawet żadnego znacznika szablonu.

<p>Pizzeria</p>

<p>Aplikacja ta pozwala na przymowanie zamówień oraz wyświetlanie menu pizzerii</p>

2.3.4. Mapowanie aplikacji

Chcąc uruchomić do co zrobiliśmy do tej pory, musimy przed uruchomieniem serwera wskazać w pliku mapowania URL-i projektu konkretną aplikację. Tę czynność należy wykonać w pliku <nazwa_projektu>/urls.py w tym konkrentym przypadku jest to pizzeria/urls.py. Wykonanie czynności rozpoczynamy od zaimportowanie funkcji include. Funkcją ta jest dostępna w module django.urls więc po path na górze pliku do pisujemy , include.

from django.urls import path, include

Następnie podobnie do mapowań stron aplikacji, jako kolejny element list podajemy wywłołanie funkcji path wraz z pustym żądaniem oraz zamiast podawać funkcję widoku oraz nazwę zmiennej wywołujemy funkcję include wraz ze wskazaniem pliku mapowania stron aplikacji w postaci 'nazwa_aplikacji.urls'

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('pizzas.urls')),
]

2.3.5. Rozszerzanie przykładu

Z racji tego że mamy trochę za mało informacji, aby zaprezentować duże możliwości szablonów w Django, rozbudujemy trochę naszą aplikacje o jedną podstronę zawierającą menu naszej pizzerii.

Naszą rozbudowę zaczniemy od stworzenia szablonu, w którym to będą znajdować się same odnośniki do naszych podstron, Inne szablony będą dziedziczyć te elementy od niego. W naszym katalogu z szablonami tworzymy plik o nazwie base.html. Plik zawiera w sumie dwie linijki

<a href="{% url 'pizzas:index' %}">Pizzeria>/a<

{% block content %}{% endblock content %}

Znacznik szablonu url w atrybucie href wstawi bezwzględny adres URL zawierający wzorzec przypisany do nazwy index w funkcji path w pliku urls.py. Po przeniesieniu tytułu jako odnośnika do szablonu nadrzędnego. Możemy teraz wpisać linię ustawiającą dziedziczenie w szablonie index.html. Linię wstawiamy na samej górze.

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

Kiedy mamy już odpowiednie miejsce gdzie będziemy umieszczać odnośniki do naszych podstron. Możemy przejść teraz do utworzenia widoku naszej podstrony, naszą stronę nazwiemy klasycznie menu. W pliku views.py naszej aplikacji pizzas, umieszczamy poniższy kod.

def menu(request):

    from pizzas.models import Pizza,Toping

    pizzas = Pizza.objects.all()

    pizza_dict={}
    pizzas_list=[]

    class PizzaClass():
        def __init__(self, name, topings):
            self.name = name
            self.topings = topings

    for pizza in pizzas:
        pizza_name = f"{pizza}"
        topings = pizza.toping_set.all()
        t=""
        for toping in topings:
            if len(t) == 0:
                t=f"{toping}, "
            else:
                t=f"{t}{toping}, "
        t=t.rstrip(', ')
        some_pizza = PizzaClass(pizza_name, t)
        pizzas_list.append(some_pizza)

    pizza_dict['pizzas']=pizzas_list

    return render(request, 'pizzas/menu.html', pizza_dict)

Przed przystąpieniem do objaśniania chciałbym przestawić model wyświetlania danych, otóż chciałem uzyskać taki sam lub zbliżony ulotkom z pizzerii wydruk menu, czyli:

nazwa_pizzy - składniki/dodatki

A więc problem nie był kod widoku, ponieważ tu mogliśmy zbudować kod realizujący to samo zadanie w znacznie prostszy sposób, ale kod szablonu. Znaczniki szablonu mają duże możliwości, jednak nie są pełnym odwzorowaniem tego co można zrobić w Pythonie. Rozwiązanie było banalne. Mamy listę z nazwami pizz, uruchamiamy pętle, wewnątrz tej iteracji, odwołujemy się nazwą pizzy do listy składników i problem rozwiązany. Jednak w szablonach Django, jedyne co możemy zrobić z zmiennymi to nadać im wartość i wypisać. Nie możemy użyć zmiennej szablonu jako klucza aby odwołać się do kolejnej listy w zagnieżdżonej pętli for. Używanie zmiennej context w szablonie zakończyło się fiaskiem, mogliśmy pisać kod szablonu, który niczego nie generował. Zajrzałem więc do oficjalnej dokumentacji Django, rozwiązanie od razu nie przypadło mi do gustu, słownik + lista + obiekty, jednak jak możemy się o tym przekonać rozwiązanie działa. Słownik w tym przypadku jest strukturą danych przekazywaną do szablonu za sprawą funkcji render, lista jest strukturą dzięki, której może odbyć się iteracja przez nasze pizze, obiekty pizzy został użyty w celu spięcia nazwy pizzy oraz jej dodatków w jednej strukturze, do której będziemy mogli się odwołać w szablonie. Zapewne istnieje inne rozwiązanie, jednak jeśli drogi czytelniku w życiu stworzyłeś może jeden program w Pythonie, przeczytałeś całą pierwszą część Crash Course (uwaga wykonując większość ćwiczeń, wszystkich nie trzeba ponieważ się powtarzają, ewentualnie trzeba zmienić jedną rzecz jednak logika pozostaje ta sama) i dotarłeś aż tutaj to moja umiejętność programowania w Pythonie jest mniej więcej na Twoim poziomie.

Tak więc co się dzieje w widoku, już tłumaczę. Na początku importujemy nasze modele, żeby było skąd dane pobrać. Wydane zostaje zapytanie do modelu Pizza o wszystkie (egzemplarze modelu) rekordy, tworząc listę naszych pizz. Tworzymy klasę naszej pizzy, klasa będzie zawierać tylko metodę __init__ a oraz dwie właściwości nazwę oraz dodatki jako ciągi tekstowe. Przeprowadzając iteracje z elementu naszej listy pizz tworzymy nazwę pizzy po prostu przechowując ją w zmiennej jako ciąg tekstowy, pobieramy listę naszych dodatków odwołując się kluczem obcym pizzy do modelu Toping. Przerabiamy listę dodatków na ciąg tekstowy. Po utworzeniu listy dodatków jako ciągu możemy, zacząć tworzyć obiekty. Nowo utworzone obiekty trafiają do listy i tak kończy się iteracja. Na samym końcu dodajmy naszą listę obiektów do słownika, który w ostatniej linii naszego widoku jest przekazywany do szablonu za pomocą funkcji render. Poniżej znajduje się kod naszego szablonu.

{% extends 'pizzas/index.html' %}

{% block content %}

<p>Menu</p>

<ul>
    {% for pizza in pizzas %}
        <li>
            {{ pizza.name }}
            <small>- {{ pizza.topings }}</small>
        </li>
    {% endfor %}
</ul>


{% endblock content %}

W tym momencie powinniśmy mieć gotowe menu. Teraz przygotujemy stronę, która wyświetli nam listę dodatków każdej z pizzy, abyś mogli zobaczyć jak działa przekazywanie danych do aplikacji za pomocą adresu URL.

Naszą rozbudowę rozpoczęliśmy od utworzenia widoku dla nowej strony. Widok jak widać poniżej jest bardzo prosty.

def pizza_site(request, pizza_id):

    from pizzas.models import Pizza,Toping

    context = {}

    name = Pizza.objects.get(id=pizza_id)
    topings = name.toping_set.all()

    context['name'] = name
    context['topings'] = topings

    return render(request, 'pizzas/pizza_site.html', context)

Zwrócić uwagę należy na dodatkowy parametr przekazywany do widoku. Parametr o nazwie pizza_id będzie przekazywali za pomocą adresu URL, taki parametr definiuje się we wzorcu adresu URL. Wzorcem zajmiemy się pod koniec naszych prac. Szablon dla tej strony został przedstawiony poniżej. Za pomocą słownika context została przekazana nazwa pizzy oraz lista z jej składnikami, nową rzeczą wprowadzoną tutaj jest znacznik szablonu {% empty %} oznacza on mniej więcej to że jeśli interpreter natrafi na pustą listę to możemy zdefiniować w bardzo prosty sposób komunikat o tym że składniki na tej pizzy są na życzenie.

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

{% block content %}

<p>{{ name }}</p>
  <ul>
    {% for toping in topings %}
        <li>{{ toping }}</li>
    {% empty %}
        <li>Dodatki na życzenie</li>
    {% endfor %}
</ul>

{% endblock content %}

Mając już szablon możemy przejść do mapowania URL-i. W aplikacji pizzas w pliku urls.py za pomocą funkcji path dopisujemy do listy urlspattern kolejny wzorzec adresu.

path('pizza/<int:pizza_id>', views.pizza_site, name='pizza_site')

Stąd właśnie bierze się parametr widoku pizza_site, jeśli po pizza/ podamy liczbę całkowitą to wywołamy widok pizza_site przekazując tę liczbę jako identyfikator rekordu pizzy. Wszystko świetnie, jednak nie znamy id pizz. Należało by stworzyć odnośnik do konkretnej pizzy w menu. W obecnej formie naszego menu próżno szukać id. Dlatego też musimy zmodyfikować widok menu dodając do naszej klasy atrybut id, który można pobrać bezpośrednio z egzemplarza modelu za pomocą notacji kropki i właściwości o nazwie id. Poniżej wstawiam zmieniony kod widoku menu.

def menu(request):

    from pizzas.models import Pizza,Toping

    pizzas = Pizza.objects.all()

    pizza_dict={}
    pizzas_list=[]

    class PizzaClass():
        def __init__(self, id, name, topings):
            self.id = id
            self.name = name
            self.topings = topings

    for pizza in pizzas:
        pizza_name = f"{pizza}"
        topings = pizza.toping_set.all()
        t=""
        for toping in topings:
            if len(t) == 0:
                t=f"{toping}, "
            else:
                t=f"{t}{toping}, "
        t=t.rstrip(', ')
        some_pizza = PizzaClass(pizza.id, pizza_name, t)
        pizzas_list.append(some_pizza)

    pizza_dict['pizzas']=pizza_list

    return render(request, 'pizzas/menu.html', pizza_dict)

Teraz musimy utworzyć nasz odnośnik w pętli w pliku szablonu menu, aby pojawił się dla każdej pizzy.

{% extends 'pizzas/index.html' %}

{% block content %}

<p>Menu</p>

<ul>
    {% for pizza in pizzas %}
        <li>
            {{ pizza.name }}
            <small>- {{ pizza.topings }}</small>
            <a href="{% url 'pizzas:pizza_site' pizza.id %}">Pokaż</a>
        </li>
    {% endfor %}
</ul>


{% endblock content %}

Podczas tworzenia adresu odnośnika za pomocą znacznika szablonu url odwołujemy się do wzorca zdefiniowanego za pomocą wartości nadanej parametrowi name funkcji pathw pliku urls.py, tym razem wartość pizza_id podajemy obok odwołania się do wzorca. Teraz przy wywołaniu strony menu przy każdej z pizz powinien pokazać się odnośnik "Pokaż" kierujący nas do konkretnej strony pizzy.

2.4. Aplikacja users

Skoro mamy już stronę główną, mamy menu i jest z czego wybierać możemy teraz przejść do składania zamówień. Złożenie zamówienia w naszej pizzerii będzie wymagało utworzenia konta. Obsługą kont zajmie się odrębna aplikacja users, którą teraz utworzymy. Przechodzimy do katalogu naszego projektu i wydajemy następujące polecenie:

(project_env) xf0r3m@macbook:python/django$ python manage.py startapp users

Po wydaniu tego polecenia zostanie utworzona struktura plików identyczna jak dla aplikacji pizzas. Teraz instalujemy aplikacje users w identyczny sposób jak instalowaliśmy aplikacje pizzas.

INSTALLED_APPS = [
    'pizzas',
    'users',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

2.4.1. Logowanie użytkowników

Naszą przygodę z kontami użytkowników zaczniemy od dania możliwości logowania potencjalnym klientom naszej pizzerii. Samo logowanie w Django jest banalne ponieważ korzystać będziemy z gotowego, przygotowanego przez Django widoku dla logowania. Jedynego czego będziemy potrzebować to mapowania URL oraz szablonu. Zaczniemy od mapowania. W pliku urls.py projektu pizzeria na liście urlpatterns dokonujemy takiego samego przekierowania jak w przypadku przekierowania w aplikacji pizzas jednak naszym wzorcem będzie users/ a pliku urls.py będzie znajdował się w katalogu aplikacji users (jako argument funkcji include podajemy users.urls).

urlpatterns = [
    path('admin/', admin.site.urls),
    path('users/', include('users.urls')),
    path('', include('pizzas.urls'))
]

W aplikacji users tworzymy plik urls.py. W nim importujemy funkcje path oraz include z modułu django.urls, definiujemy app_name następnie na liście urlpatterns dodajemy jedno wywołanie funkcji path kierujące puste domyślne zapytanie do pliku zdefiniowanego przez Django django.contrib.auth.urls.

from django.urls import path,include

app_name="users"
urlpatterns = [
    path('', include('django.contrib.auth.urls'))
]

Teraz kiedy mapowanie jest gotowe, możemy przejść do utworzenia szablonu. Domyślna funkcja widoku stworzona do logowania generuje stronę na podstawie szablonu login.html umieszczonego w katalogu templates/registration. Te nazwy muszą zostać zachowane. Utwórzmy te katalogi wewnątrz katalogu aplikacji users. Następnie utworzymy plik z szablonem.

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

{% block content %}

    <form action="{% url 'users:login' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit">Zaloguj się</button>
       <input type="hidden" name="next" value="{% url 'pizzas:index' %}" />
    </form>

{% endblock content %}

Tak prezentuje się nasz szablon logowania. Warto zwrócić uwagę na to jak łatwy jest dostęp do zasobów innej aplikacji wystarczy odpowiednia ścieżka (patrz znacznik extends), w atrybucie action zdefiniowana została strona, która ma obsłużyć dany formularz. Znacznik szablonu csrf_token służy zabezpieczeniu formularza przez atakiem CSRF. Następnie wyświetlany jest sam formularz, który przechowywany jest w zmiennej form. Wyświetlenie jej wraz z as.p spowoduje, że etykiety oraz pola wprowadzania danych zostaną wyświetlone jako jeden akapit i tak dla każdego z elementów formularza. Formularze w Django nie zawierają przycisków, trzeba je zdefiniować samemu. Ostatnią rzeczą jest przekazanie wraz z formularzem do widoku adres strony, do której ma zostać przekierowany użytkownik po poprawnym zalogowaniu. Jako ciekawostkę możemy sobie dodać warunek w formularzu, który wyświetli nam komunikat z informacją o tym że formularz nie został poprawnie wypełniony. W ten czas nasz formularz wyglądać będzie tak:

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

{% block content %}

    {% if form.errors %}
        <p>
            Nazwa użytkownika i hasło są niepoprawne. Spróbuj ponownie.
        </p>
    {% endif %}

    <form action="{% url 'users:login' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit">Zaloguj się</button>
        <input type="hidden" name="next" value="{% url 'pizzas:index' %}" />
   </form>

{% endblock content %}

Dlaczego jednak jako ciekawostkę? Otóż Django sam dodaje takową informacje jeśli formularz nie przejdzie walidacji. Teraz dodamy warunek w pliku w szablonie bazowym aplikacji pizzas, po to abyśmy wiedzieli, że zostaliśmy zalogowani oraz w przeciwnym wypadku wyświetli odnośnik prowadzący do strony logowania. Poniżej znajduje się już uzupełniony kod szablonu bazowego aplikacji pizzas.

<p>
    <a href="{% url 'pizzas:index' %}">Pizzeria</a>-
    <a href="{% url 'pizzas:menu' %}">Menu</a>
</p>

{% if user.is_authenticated %}
    <p>
        Witaj, {{ user.username }}
    </p>
{% else %}
    <p>
        <a href="{% url 'users:login' %}">Zaloguj się</a>
    </p>
{% endif %}

{% block content %} {% endblock content%}

Jednego użytkownika już mamy. Możemy się zalogować się jako superużytkownik. Po poprawnym zalogowaniu zostaniemy przeniesieni z powrotem na stronę główną, jednak teraz zamiast Zaloguj się zobaczymy Witaj, pizza_admin. Teraz pod powitaniem umieścimy przycisk Wyloguj, który będzie kończył sesję użytkownika w aplikacji. Wylogowanie również jest funkcją, która jest dostarczana wraz z Django. Mapowanie URL-u wylogowania jest również dostarczane przez Django, tak jak mapowanie URL-u logowania. Szablon bazowy wygląda następująco po dodaniu nowego odnośnika.

<p>
    <a href="{% url 'pizzas:index' %}">Pizzeria</a>-
    <a href="{% url 'pizzas:menu' %}">Menu</a>
</p>

{% if user.is_authenticated %}
    <p>
        Witaj, {{ user.username }}
    </p>
    <p>
        <a href="{% url 'users:logout' %}">Wyloguj</a>
    </p>
{% else %}
    <p>
        <a href="{% url 'users:login' %}">Zaloguj się</a>
    </p>
    <p>
        <a href="{% url 'users:register' %}">Zarejestruj się</a>
    </p>
{% endif %}

{% block content %} {% endblock content%}

W ramach eksperymentu, kliknijmy w ten odnośnik. Zostaniemy poinformowani o wylogowaniu z naszej aplikacji przez witrynę administracyjną Django. Nie zbyt fajnie, co? Oczywiście to zachowanie możemy zmienić, tworząc nowy szablon w katalogu templates/registration. Uwaga, Plik musi nazywać się: logged_out.html. Po rozszerzeniu pliku o szablon bazowy, między znacznikami szablonu oznaczającymi zawartość strony w tym szablonie możemy umieścić wszystko co nam się podoba.

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

{% block content %}

    <p>
        Dziękujemy za skorzystanie z naszej aplikacji.
   </p>

{% endblock content %}

2.4.2. Rejestracja użytkowników

Teraz możemy dać możliwość samodzielnego utworzenia konta na stronie naszej pizzerii. Tutaj nie jest już tak kolorowo. Jedyne co nam Django oferuje w tym przypadku jest formularz. Widok, szablon oraz mapowanie URL-u musimy utworzyć samodzielnie. Zaczynamy od widoku dla formularza rejestracji.

from django.shortcuts import render, redirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth import login

def register(request):

    if request.method != 'POST':
        form = UserCreationForm()
    else:
        form = UserCreationForm(data=request.POST)
        if form.is_valid():
            new_user = form.save()
            login(request, new_user)
            return redirect('pizzas:index')
   content = {'form': form}
   return render(request, 'registration/register.html", context)

W tym przypadku aby funkcja widoku mogła poprawnie działać oraz wyświetlać nam formularz, potrzebujemy modułów. Pierwszą funkcją jaką importujemy jest redirect, powoduje ona przekierowanie HTTP, na stronę podaną jako argument. Adresy stron podane jako argumenty funkcji redirect czy też w znacznikach szablonu url podajem w formacie nazwa_aplikacji:nazwa_mapowania - wartość argumentu name funkcji path w pliku urls.py. Następnie importujemy nasz formularz oraz funkcję login. Za pomocą tej funkcji będzie możliwe automatyczne zalogowanie nowo utworzonego użytkownika. Funkcje widoku obsługujące formularz posiadają pewien schemat. Taka funkcja rozpoczyna się od sprawdzenia użytej metody przesłania danych do serwera, protokół HTTP wyróżnia dwie GET oraz POST. Metoda GET służy głównie do obsługi żądań osób odwiedzających witrynę. Poniżej żądanie wyświetlenia strony konkretnej pizzy.

http://localhost:8000/pizza/1

Żądanie wyświetlenia konkretnej pizzy zostało przesłane do serwera za pomocą żądania GET. Najprościej można to ująć w ten sposób, że żądania GET są przesyłanie w adresie strony. Natomiast żądania POST są przesyłane oddzielnym pakietem transmisji. W żądaniach POST najczęsciej są przesyłane dane formularzy.

Jeśli nasz widok, ktoś wywołał za pomocą żądania GET to wtedy zmiennej form zostanie nadany pusty formularz. Następnie definiowana jest zmienna context zawierająca wszelkie dane dla szablonu w tym nasz formularz. Na sam koniec funkcja widoku zwraca wygenerowaną przed funkcję render() stronę, na której znajduje się nasz formularz rejestracji. W przeciwnym wypadku jeśli nasza funkcja została wywołana za pomocą żądania POST (np. kiedy użytkownik przesłał formularz), to tworzona jest zmienna z formularzem gdzie jego dane są pobierane z właściwości POST obiektu request. Za pomocą instrukcji warunkowej if oraz metody formularza is_valid() sprawdza jest poprawność wypełnienia pól formularza, jeśli któraś z wartości pół nie przechodzi walidacji (sprawdzenia poprawności), to następną czynnością jest wpisanie formularza do zmiennej context i wygenerowanej strony z formularzem oraz komunikatem błędu. Zaś kiedy formularz został wypełniony bezbłędnie to wykonywana jest metoda formularza save(), która powoduje zapisanie danych z formularza w bazie danych na podstawie przypisanego do niego modelu. Zwróćmy uwagę na to, że wartość zwraca przez metodę save() została zapisana do zmiennej new_user. Dzięki tej zmiennej będzie możliwe automatyczne zalogowanie zarejestrowanego użytkownika, kiedy mamy tą zmienną możemy zalogować użytkownika za pomocą funkcji login() ta funkcja pobiera dwa argumenty obiekt żądania (request) oraz odniesienie do użytkownika ( new_user). Kiedy użytkownik został zalogowany, możemy teraz przekierować go na stronę główną, która zostanie wyświetlona ze spersonalizowanym powitaniem oraz odnośnikiem do wylogowania się z konta na naszej stronie. Teraz potrzebujemy jeszcze szablonu, który poza odnośnikiem nie różni się niczym do szablonu logowania.

Plik szablonu formularza rejestracji znajduje się w folderze templates/registration pod nazwą register.html:

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

{% block content %}

    {% if form.errors %}
        <p>Rejestracja nie powiodła się. Spróbuj ponownie</p>
    {% endif %}

    <form action="{% url 'users:register' %}" method="post"<
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit">Zarejestruj się</button>
        <input type="hidden" name="next" value="{% url 'pizzas:index' %}" />
    </form>

{% endblock content %}

Funkcja mapowania w pliku users/urls.py wygląda następująco:

path('register/', views.register, name="register"),

Teraz możemy do szablonu bazowego aplikacji pizzas dodać odnośnik dla niezalogowanych, prowadzący do naszej strony z rejestracją.

<p>
    <a href="{% url 'pizzas:index' %}">Pizzeria</a>-
    <a href="{% url 'pizzas:menu' %}">Menu</a>
</p>

{% if user.is_authenticated %}
    <p>
        Witaj, {{ user.username }}
    </p>
    <p>
        <a href="{% url 'users:logout' %}">Wyloguj</a>
    </p>
{% else %}
    <p>
        <a href="{% url 'users:login' %}">Zaloguj się</a>
    </p>
    <p>
        <a href="{% url 'users:register' %}">Zarejestruj się</a>
    </p<
{% endif %}

{% block content %} {% endblock content%}

2.5. Rozbudowa aplikacji 'pizzas'.

Teraz kiedy mamy logowanie oraz rejestracje. To powinniśmy mieć możliwość złożenia zamówienia w naszej pizzerii (aplikacja pizzas). Sama operacja jest dość złożona bo do realizacji tego zadania potrzebujemy aż 5 elementów: modelu zamówienia, widoku wyświetlającego zamówienia, widoku dodającego zamówienia, formularza, szablonu i mapowania URL. Zaczniemy od modelu zamówienia. Na nasze zamówienie będzie składać się pizza, wielkość, sos pierwszy oraz sos drugi, dowóz czy odbiór własny oraz datę zamówienia. Model prezentuje się poniżej. Stosować będziemy skróty, które nasz kucharz będzie stanie odszyfrować dlatego pola są takie małe.

class Order(models.Model):

    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
    size = models.CharField(max_length=10)
    sauce1 = models.CharField(max_length=10)
    sauce2 = models.CharField(max_length=10)
    deliver = models.CharField(max_length=10)
    date_order = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return f"{self.pizza.name} - {self.size},{self.sauce1},\
        {self.sauce2},{self.deliver} - {self.date_order}"

Po dopisaniu naszego modelu zamówienia musimy wykonać migrację.

(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas
(project_env) xf0r3m@macbook:python/django$ python manage.py migrate

Po pomyślnej migracji zarejestrujemy nowy model na witrynie administracyjnej, po to aby dodać kilka testowych wartość do sprawdzenia widoku wyświetlania złożonych zamówień. Widok złożonych zamówień prezentuje się następująco:

def orders_view(request):

    from pizzas.models import Order

    orders = Order.objects.order_by('-date_order')
    context = {'orders': orders}
    return render(request, 'pizzas/orders_view.html', context)

W szablonie zamiast używać listy użyjemy tabeli. Poniżej znajduje się kod szablonu.

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

{% block content %}

<table>
    <thead>
        <tr>
            <td>Numer zamówienia>/td>
            <td>Pizza<td>
            <td>Wielkość</td>
            <td>Sos_1>/td>
            <td>Sos_2<td>
            <td>Dostawa</td>
        </tr>
    </thead>
    <body>
    {% for order in orders %}
        <tr>
            <td>{{ order.id }}</td>
            <d>{{ order.pizza.name }}</td>
            <td>{{ order.size }}</td>
            <td>{{ order.sauce1 }}</td>
            <td>{{ order.sauce2 }}</td>
            <td>{{ order.deliver }}</td>
        </tr>
    {% empty %}
        <tr><td colspan="6">Brak zamówień</td></tr>
    {% endfor %}
    </tbody>
</table>

{% endblock content %}

Teraz możemy przejść do składania zamówień na naszą pizzę, zaczniemy od formularza. Nie jest on może trudny w interpretacji ale jest nieco bardziej złożony niż te przedstawione w książce.

from django import forms

from .models import Order

class NewOrder(forms.ModelForm):
    class Meta:
        model = Order
        fields = ['pizza', 'size', 'sauce1', 'sauce2', 'deliver']
        labels = {
            'pizza': 'Pizza',
            'size': 'Wielkość',
            'sauce1': 'Pierwszy sos',
            'sauce2': 'Drugi sos',
            'deliver': 'Dostawa'
        }
        size_choice = [
            (None, '-----------'),
            ('S', 'Mała'),
            ('M', 'Duża'),
            ('L', 'Familijna'),
            ('XL', 'XXL 55cm')
        ]
        sauce_choice = [
            ('br', '-----------'),
            ('k', 'Ketchup'),
            ('m', 'Majonez'),
            ('cz', 'Czosnkowy'),
            ('pik', 'Pikantny')
        ]
        deliver_choice = [
            (None, '-----------'),
            ('odb', 'Odbiór osobisty'),
            ('dwz', 'Dowóz')
        ]
        widgets = {
            'size': forms.Select(choices=size_choice),
            'sauce1': forms.Select(choices=sauce_choice),
            'sauce2': forms.Select(choices=sauce_choice),
            'deliver': forms.Select(choices=deliver_choice)
        }

Formularze w Django to nic innego jak klasy potomne, klasy ModelForms modułu django.forms, jednak kodu określającego formularz nie tworzymy w samej klasie potomnej tylko w pod klasie Meta. Z racji tego, że klasa ModelForm bazuje na modelach (zresztą jak sama nazwa wskazuje) musimy pamiętać o ich imporcie jeszcze przed zdefiniowaniem samej klasy. W klasie Meta podajemy takie informacje jak model na jakim bazuje formularz (zmienna model), pola formularza (lista fields) oraz etykiety (słownik labels), z racji tego że informacje przekazywane przez zamawiającego mogą być predefiniowane, to cały formularz oprzemy na jednym elemencie, na liście wyboru select. Jeśli nasz model zawiera klucz obcy, to pole z automatu jest ustawiane przez Django jako element select. Resztę naszych pól musimy ustawić samodzielnie. Zmianie elementów przydzielanych przez Django, służy słownik widgets, w którym każdemu polu przypisujemy wyświetlany element, w tym przypadku jest to forms.Select czyli element HTML select jako argument choices podajemy listę z dwuelementowych krotek, w naszym przypadku zdefiniowaną powyżej słownika widgets.

Jako ciekawostkę podam fakt, że jeśli w jakieś krotce w pierwszym elemencie podamy jako pierwszy argument, czyli wartość elementu option, wartość None to pole automatycznie stanie się polem required, czyli jego wartość musi być ustawiona inaczej przeglądarka nie pozwoli wysłać formularza. Poniżej znajduje się kod widoku obsługującego ten formularz.

def new_order(request):

    if request.method != 'POST':
        form = NewOrder()
    else:
        form = NewOrder(data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('pizzas:thank_you')
    context = {'form': form}
    return render(request, 'pizzas/new_order.html', context)

Ten przypadek obsługi jest chyba najprostszy. Nie ma tu nic złożonego, po przejściu walidacji zamówienie jest zapisywane w bazie. Pod mapowaniem thank_you kryje się jeden akapit z podziękowanie za złożenie zamówienia. Poniżej znajduje kod szablonu.

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

{% block content %}

    {% if form.errors %}
        <p>
            Złożenie zamówienia nie powiodło się. Spróbuj ponownie
       </p>
    {% endif %}

    <form action="{% url 'pizzas:new_order' %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
	<button name="submit">Zamów</button>
    </form>

{% endblock content %}

Powiedzmy że zamówiliśmy pizzę, ale w pizzerii skończył się jeden jej składnik. Kelnerka po kontakcie telefonicznym z klientem doszła do porozumienia i klient wybrał inną pizzę. Jednak w systemie nie ma możliwości zmiany zamówienia. Utworzymy ją. Do tego celu potrzebujemy dodatkowego widoku, mapowania, szablonu oraz odnośnika do edycji zamówienia w tabeli z zamówieniami. Edycje danych wpisywanych za pomocą formularza tworzy się bardzo szybko ponieważ wystarczy skopiować widok oraz szablon. W widoku dodać kilka rzeczy, w szablonie zmieć dwie rzecz oraz dodać jedną. Poniżej znajduje się kod widoku, najważniejszy w tym przedsięwzięciu.

def edit_order(request, order_id):

    from .models import Order

    order = Order.objects.get(id=order_id)

    if request.method != 'POST':
        form = NewOrder(instance=order)
    else:
        form = NewOrder(instance=order, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('pizzas:edit_ok')
    context = {'form': form, 'order': order}
    return render(request, 'pizzas/edit_order.html', context)

Zatem po kolei żeby było wiadomo jakie zamówienie mamy zmienić potrzebujemy jego identyfikatora, id z tabeli w bazie danych. Potrzebne nam id będzie przekazywane w żądaniu GET, przez kliknięcie w łącze na stronie z zamówieniami. Na początku naszego widoku importujemy model Order, aby uzyskać dostęp do egzemplarzy tego modelu w bazie. Za pomocą zapytania Order.objects.get(id=order_id), pobraliśmy rekord zamówienia do zmiennej order. Następnie wykonuje się klasyczna obsługa formularza z tą jednak różnicą, że kiedy tworzone są egzemplarze formularza to przekazywany jest argument instance, któremu nadawana jest wartość naszego zapytania, ustawienie argumentu instance powoduje wypełnienie pół formularza danymi z zapytania, strona edit_ok zawiera jeden akapit z informacją że zapisano zmiany w zamówieniu. W zmiennej context została dodana wartość obiektu order pod kluczem order. Jest to niezbędne kiedy zmieniamy rekordy, ponieważ w atrybucie action formularza musimy podać id zmienianego rekordu, ponieważ ten widok będzie wykonywany po raz drugi gdy użytkownik prześle dane. Dlatego też po słowie kluczowym else gdzie deklarowany jest formularz, podano argument instance, aby było wiadomo jaki rekord należy zmienić. Poniżej znajduje się kod szablonu.

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

{% block content %}

    {% if form.errors %}
        <p>
            Zmiana zamówienia nie powiodła się. Spróbuj ponownie
        </p>
    {% endif %}

    <form action="{% url 'pizzas:edit_order' order.id %}" method="post">
        {% csrf_token %}
        {{ form.as_p }}
        <button name="submit"&t;Zapisz zmiany</button>
    </form>

{% endblock content %}

Tutaj zmieniono atrybut action na edit_order, dodano order.id oraz zmieniono treść przycisku. Poniżej znajduje się zawartość pliku pizzas/urls.py z uwzględnieniem nowego mapowania oraz zmieniony kod szablonu pizzas/orders_view.html.

Plik pizzas/urls.py

from django.urls import path

from . import views

app_name='pizzas'
urlpatterns = [
    path('', views.index, name='index'),
    path('menu', views.menu, name='menu'),
    path('pizza/<int:pizza_id>', views.pizza_site, name='pizza_site'),
    path('orders/', views.orders_view, name="orders"),
    path('new_order/', views.new_order, name="new_order"),
    path('edit_order/<int:order_id>', views.edit_order, name='edit_order')
]

Plik pizzas/templates/pizzas/orders_view.html

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

{% block content %}

<table border="1">
    <thead>
        <tr>
            <th>Numer zamówienia</th>
            <th>Pizza</th>
            <th>Wielkość</th>
            <th>Sos_1</th>
            <th>Sos_2</th>
            <th>Dostawa</th>
            <th>>/th>
        </tr>
    </thead>
    <tbody>
    {% for order in orders %}
        <tr>
            <td>{{ order.id }}</td>
            <td>{{ order.pizza.name }}>/td>
            <td>{{ order.size }}</td>
            <td>{{ order.sauce1 }}</td>
            <td>{{ order.sauce2 }}</td>
            <td>{{ order.deliver }}</td>
            <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td>
        </tr>
    {% empty %}
        <tr><td colspan="7">Brak zamówień</td></tr>
    {% endfor %}
    <tbody>
</table>

{% endblock content %}

2.6. Zabezpieczenie aplikacji

Kolejną rzeczą jak należy zrobić to zabezpieczyć pewne strony przed niepowołanym dostępem. W zależności od tego jakie restrykcje chcemy wprowadzić możemy użyć konkretnych metod. Pierwszą z nich jest wymaganie logowania, jeśli nie zalogowani przejedziemy pod adres http://localhost:8000/new_order. Zostanie nam przedstawiona strona do składania zamówień. W ten sposób osoba złośliwa może po nabijać zamówień, zostawiając jedynie ślad w logach o ile uruchomiliśmy serwer wraz z opcją zapisywania dziennika. Django daje nam banalny sposób na zabezpieczenie się przed tego typu sytuacjami. W pliku views.py w aplikacji pizzas importujemy dekorator (dodatek/modyfikator do funkcji) o nazwie login_required.

from django.contrib.auth.decorators import login_required

Dekorator login_required sprawdzi czy jesteśmy zalogowani, kiedy będziemy wywoływać funkcje widoku. Dekoratora używamy w pomocą znaku @ oraz nazwy dekoratora tuż nad definicją widoku, tak jak przedstawiono to poniżej:

@login_required
def orders_view(request):
    ...

@login_required
def new_order(request):
    ...

@login_required
def edit_order(request, order_id):

Aby działało to poprawnie w ustawieniach projektu pizzeria/settings.py musimy zdefiniować zmienną LOGIN_URL na samym dole pliku, jeśli tego nie zrobimy niezalogowani użytkownicy, przechodzący pod zabezpieczoną przez dekorator stronę otrzymają stronę błędu, że nie znaleziono strony. Wartość zmiennej LOGIN_URL zapisujemy w klasycznym odniesieniu do strony, tak jak w przypadku znaczników szablonu URL, czy funkcji redirect (nazwa_aplikacji:nazwa_mapowania_URL).

LOGIN_URL = 'users:login'

Teraz kiedy nie zalogowani, spróbujemy na przykład podejrzeć zamówienia. Zostaniemy przekierowani na stronę logowania, identycznie będzie z wywołaniem widoku new_order, czyli złożeniem nowego zamówienia oraz z jego edycją.

Drugi sposób w jaki możemy zabezpieczyć naszą aplikacje, jest wyświetlenie użytkownikom rzeczy stricte tyczących się ich. Powiedzmy że użytkownik powinien mieć wgląd w swoje zamówienia. Ta zmiana będzie wymagać trochę pracy, ale myślę że jest ciekawy do opisania przypadek, ponieważ musimy zmienić już wdrożony model danych. Dodać jedną kolumnę do tabeli, to nie jest problem jeśli tabela była by pusta, ale co jeśli dane do tej tabeli już zostały wprowadzone dane. Zaczniemy od zaimportowania modelu User w pliku pizzas/models.py.

from django.contrib.auth.models import User

Następnie dodamy do naszego modelu Order nowe kolumnę o nazwie owner, która będzie kluczem obcym w modelu User dostarczanym przez Django.

class Order(models.Model):

    pizza = models.ForeignKey(Pizza, on_delete=models.CASCADE)
    size = models.CharField(max_length=10)
    sauce1 = models.CharField(max_length=10)
    sauce2 = models.CharField(max_length=10)
    deliver = models.CharField(max_length=10)
    date_order = models.DateTimeField(auto_now_add=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f"{self.pizza.name} - {self.size},{self.sauce1},\
        {self.sauce2},{self.deliver} - {self.date_order}"

Aby wdrożyć nasze zmiany musimy wykonać migrację.

(project_env) xf0r3m@macbook:python/django$ python manage.py makemigrations pizzas

Podczas wykonywania tego polecenia Python wyświetli informacje o tym, że próbujemy dodać nie-zerowe pole do modelu Order bez wartości domyślnej i nie jest to możliwe, ponieważ zmiany muszą być również uwzględnione na istniejących już rekordach. Python daje nam do wyboru dwie opcje, albo podanie teraz wartość jaka zostanie przypisana w polu owner dla istniejących już rekordów, lub porzucenie migracji i dodanie wartości domyślnej w deklaracji pola w klasie modelu.

You are trying to add a non-nullable field 'owner' to order without a default; 
we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null
    value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 2

Teraz wybierzemy opcje nr. 2. Jeśli podczas testowania rejestracji stworzyliśmy użytkownika to możemy mu nadać wszystkie zamówienia, models.ForeignKey() zawiera identyfikatory (id) z zewnętrznej tabeli. Aby dowiedzieć się jakie id mają nasi użytkownicy skorzystamy z pomocy powłoki Django.

(project_env) xf0r3m@macbook:python/django$ python manage.py shell
>>> from django.contrib.auth.models import User
>>> users = User.objects.all()
>>> for user in users:
...     print(f"{user.id} {user.username}")
...
1 pizza_admin
2 xf0r3m

Na początku importujemy nasz model, następnie pobieramy wszystkie egzemplarze tego modelu aby poprzez iteracje wpisać id oraz nazwę użytkownika. Teraz wiemy, że nasz stworzony w celach testowych użytkownik ma id = 2. Oczywiście moglibyśmy to pominąć i domyślić się że musi mieć id = 2. Jednak chciałem przedstawić tutaj sposób w jaki możemy to sprawdzić aby mieć pewność. Również możemy wyszukać użytkownika za pomocą lekko zmodyfikowanego zapytania.

>>> user = User.objects.get(username='xf0r3m')
>>> user.id
2

Teraz kiedy znamy już id możemy ponownie wydać polecenie migracji. Python znów nas zapyta o to samo, jednak teraz wybieramy opcje nr. 1 a następnie po znaku zachęty >>> wpiszemy id naszego użytkownika.

You are trying to add a non-nullable field 'owner' to order without a default;
we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows
 	with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option: 1
Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available,
so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>> 2

Teraz zmodyfikujemy widok order_views, tak aby wyświetlał zamówienia wszystkich użytkowników, jeśli zalogowanym użytkownikiem jest pizza_admin, czyli nasz superużytkownik. A jeśli zalogowany jest kto inny, to widzi on tylko swoje zamówienia.

@login_required
def orders_view(request):

    from pizzas.models import Order

    if request.user.username != 'pizza_admin':
        orders = Order.objects.filter(owner=request.user).order_by('-date_order')
    else:
        orders = Order.objects.order_by('-date_order')


    context = {'orders': orders}
    return render(request, 'pizzas/orders_view.html', context)

Ważną rzeczą użytą w tym przypadku jest to, iż egzemplarz klasy User zalogowanego użytkownika jest przechowywany w właściwości user obiektu request. Jednak chcąc przyrównać nazwy użytkowników musimy użyć właściwości username. Ze względu na iż zmodyfikowaliśmy model Order teraz nie możliwe jest dodanie nowego zamówienia bez zmian uwzględniających właściciela. Poniżej znajduje się kod widoku new_order uwzględniający właściciela.

@login_required
def new_order(request):

    if request.method != 'POST':
        form = NewOrder()
    else:
        form = NewOrder(data=request.POST)
        if form.is_valid():
            new_order = form.save(commit=False)
            new_order.owner = request.user
            new_order.save()
            return redirect('pizzas:index')
    context = {'form': form}
    return render(request, 'pizzas/new_order.html', context)

Z założeń wynika iż użytkownik raz przypisany do zamówienia jest nietykalny, nie musimy w ogóle zmieniać kodu widoku edit_order. Wprowadziłem lekką zmianę do szablonu orders_view.html aby wyświetlał datę, według narzuconego formatu.

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

{% block content %}

<table border="1">
    <thead>
        <tr>
            <th>Numer zamówienia</th>
            <th>Pizza</th>
            <th>Wielkość</th>
            <th>Sos_1</th>
            <th>Sos_2</th&t;
            <th>Dostawa</th>
            <th>Data zamówienia</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
    {% for order in orders %}
        <tr>
            <td>{{ order.id }}</td>
	    <td>{{ order.pizza.name }}</td>
            <td>{{ order.size }}</td>
	    <td>{{ order.sauce1 }}</td>
            <td>{{ order.sauce2 }}</td>
            <td>{{ order.deliver }}</td>
            <td>{{ order.date_order|date:'d M Y H:i'  }}</td>
	    <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td>
        </tr>
    {% empty %}
        <tr><td colspan="7">Brak zamówień</td></tr>
    {% endfor %}
    </tbody>
</table>

{% endblock content %}

Pionowa kreska | oraz date:'d M Y H:i' są to filtry znaczników szablonu, w tym przypadku jest filtr daty, który wyświetla nam date w formacje 01 Jan 1970 00:00.

Aby upewnić się ze wszystko działa utwórzmy trzeciego użytkownika i złóżmy zamówienie. Użytkownicy powinni widzieć tylko swoje zamówienia, z kolei superużytkownik powinien widzieć je wszystkie. Ostatnią rzeczą jaka pozostała do zabezpieczenia, jest wyłączność na edycje zamówień przez superużytkownika. Zwykli użytkownicy nie powinni mieć dostępu do zmiany zamówienia. Zmian musimy dokonać w dwóch miejscach: w szablonie oraz w widoku edit_order. Najpierw rozpoczniemy od szablonu. Poniżej zmieniony kod szablonu

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

{% block content %}

<table border="1">
    <thead>
        <tr>
            <th>Numer zamówienia</th>
            <th>Pizza</th>
            <th>Wielkość</th>
            <th>Sos_1</th>
            <th>Sos_2</th>
            <th>Dostawa</th>
            <th>Data zamówienia</th>
        {% if user.username == 'pizza_admin' %}
            <th></th>
        {% endif %}
        </tr>
    </thead>
    <tbody>
    {% for order in orders %}
        <tr>
            <td>{{ order.id }}</td<
            <td>{{ order.pizza.name }}</td>
            <td>{{ order.size }}</td>
            <td>{{ order.sauce1 }}</td<
            <td>{{ order.sauce2 }}</td>
            <td>{{ order.deliver }}</td>
            <td>{{ order.date_order|date:'d M Y H:i'  }}</td>
            {% if user.username == 'pizza_admin' %}
                <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td>
            {% endif %}
        </tr>
    {% empty %}
        <tr><td colspan="7">Brak zamówień</td></tr>
    {% endfor %}
    </tbody>
</table>

{% endblock content %}

Dodaliśmy dwa znaczniki warunkowe szablonu, sprawdzające czy uwierzytelniony użytkownik to pizza_admin. Teraz zajmiemy się kodem widoku, na początek importujemy dwie rzeczy z modułu django.http.

from django.http import HttpResponseRedirect, Http404

Następnie w kodzie widoku dopisujemy warunek, który przyrówna request.user.username do ciągu pizza_admin jeśli się te wartości nie będą się zgadzać wtedy zostanie zgłoszony wyjątek Http404. Poniżej znajduje się kod zmodyfikowanej funkcji widoku.

@login_required
def edit_order(request, order_id):

    if request.user.username != 'pizza_admin':
        raise Http404

    from .models import Order

    order = Order.objects.get(id=order_id)

    if request.method != 'POST':
        form = NewOrder(instance=order)
    else:
        form = NewOrder(instance=order, data=request.POST)
        if form.is_valid():
            form.save()
            return redirect('pizzas:index')
    context = {'form': form, 'order': order}
    return render(request, 'pizzas/edit_order.html', context)

2.7. Nadawanie stylu aplikacji - Bootstrap 4

Przed nami ostatni rozdział podróży przez Pythona oraz przez Django. Zostały jeszcze dwa zagadnienia, które należało by omówić w kontekście Django. Nadawanie stylu oraz oraz wdrożenie naszej aplikacji na platformie sieciowej Heroku. Zaczniemy od nadawania stylu, aby nasza aplikacja zaczęła wyglądać bardziej profesjonalnie za pomocą znanego zestawu narzędzi jakim jest Bootstrap 4. Dla naszej aplikacji skorzystamy z prostego motywu. Na początku instalujemy dodatek zawierający Bootstrap przygotowany do współpracy z Django. W aktywnym środowisku wirtualnym wydajemy polecenie:

(project_env) xf0r3m@macbook:python/django$ pip install django-bootstrap4

Po zainstalowaniu Bootstrap, dopisujemy go zainstalowanych aplikacji w naszym projekcie. Plik pizzeria/settings.py, zmienna INSTALLED_APPS.

INSTALLED_APPS = [
    'users',
    'pizzas',
    'bootstrap4',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Teraz kiedy nasz Bootstrap jest już zainstalowany. Możemy podmienić kod z szablonu base.html na ten poniżej.

Najprościej rzecz ujmując, ten szablony stworzy bazową stronę naszej aplikacji. Na górze strony zostanie umieszczony responsywny pasek nawigacyjny, zawierający wszystkie nasze dotychczasowe odnośniki oraz nazwę aplikacji. Responsywność wygląda w ten sposób że jeśli strona zostanie wyświetlona na wąskim ekranie, odnośniki zostaną schowane w wysuwanym na dół menu. A na pasku zostanie wyświetlona nazwa aplikacji czy już w tym przypadku projektu oraz przycisk aktywujący wysunięcie menu. Poniżej znajduje się kod szablonu bazowego.

{% load bootstrap4 %}

<!doctype html>
<html lang="pl">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
        <title>Pizzas>/title<
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}
    </head>
    <body<
        <nav class="navbar navbar-expand-md navbar-light bg-light mb-4 border">
            <a class="navbar-brand" href="{% url 'pizzas:index' %}"<
                Dolce&Gusto
            </a>
            <button class="navbar-toggler" type="button" data-toggle="collapse" 
            data-target="#navbarCollapse" aria-controls="navbarCollapse" 
            aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav ml-auto">
                    {% if user.is_authenticated %}
                        <li class="nav-item"<
                            <span class="navbar-text">
                                Witaj, {{ user.username }}
                            </span>
                        </li>
                        {% if user.username == 'pizza_admin' %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'pizzas:orders' %}">
                                    Zamówienia
                                </a>
                            </li>
                        {% else %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'pizzas:new_order' %}">
                                    Złóż zamówienie
                                </a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'pizzas:orders' %}">
                                    Moje zamówienia
                                </a>
                            </li>
                        {% endif %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'users:logout' %}">
                                Wyloguj
                            </a>
                        <li>
                    {% else %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'users:register' %}">
                                Rejestruj
                            </a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'users:login' %}">
                                Zaloguj
                            </a<
                        </li>
                    {% endif %}
                </ul>
            </div>
        </nav>
        <main role="main" class="container">
            <div class="pb-2 mb-2 border-bottom"<
                {% block page_header %} {% endblock page_header %}
            </div>
            <div>
                {% block content %}{% endblock content %}
            </div>
        </main>
    </body>
</html>

W tym szablonie na samy początku ładujemy Bootstrap 4 aby mieć możliwość skorzystania ze znaczników szablonu przygotowanych przez tę aplikację. Następnie tworzona jest podstawowa struktura strony HTML, z zaznaczeniem użycia języka polskiego w atrybucie lang w znaczniku otwierającym HTML, w nagłówku strony - pomiędzy znacznikami head, przekazywane są informacje odpowiedzialne za kodowanie strony <meta charset="utf-8">, responsywność <meta name="viewport" ...>, ustawienie tekstu wyświetlanego w tytule karty lub/i okna przeglądarki <title></title>. Następnie mamy dwa znaczniku szablonu Django, w których to ładowane są pliki arkusza stylów CSS oraz biblioteki JavaScript ze wskazaniem o użyciu pełnej biblioteki, a nie wersji odchudzonej (nie zawierającej obsługi AJAX-a oraz efektów - takich rzeczy jak np. slideToggle).

W ciele strony - między znacznikami <body> umieszczamy nasz pasek nawigacyjny <nav ...>. Na pasku nawigacyjnym umieszczamy odnośnik do strony głównej zatytułowany nazwą projektu w Django, przycisk widoczny jedynie na małych ekranach, zwężonych oknach przeglądarki lub dużych zoomach, powodujący wyjechanie menu ukrytego w wyżej wymienionych sytuacjach zaraz pod przyciskiem znajduje się znajduje się sekcja <div> z menu przedstawionym tutaj za pomocą listy nieuporządkowanej. Wewnątrz listy znajdują się znaczniki szablonu oraz odnośniki znane nam z poprzedniej wersji pliku base.html. Pod paskiem nawigacji znajduje się element główny naszej strony, zwierający dwa znaczniki sekcji.

Pierwszy z nich zawiera, parę znaczników szablonu block page_header, dzięki temu zdefiniowany na innej stronie znaczniki block page_header zostanie wstawiony w to miejesce, nieco enigmatyczne wydają się użyte w tej sekcji klasy, pierwszy z nich pb-2 oznacza wewnętrzny odstęp dolny treści elementu krawędzi elementu (padding-bottom) o wielkości $spacer * 0,5, gdzie $spacer (zmienna SASS - język pre procesora kompilowany do CSS) przyjmuje wartość 16px. klasa pb istnieje w 5 wariantach. od * 0,25 od * 3. Identycznie jest z drugą klasą, tylko że zamiast dopełnienia (odstęp pomiędzy treścią elementu a jego krawędzią), mamy margines czyli odstęp między krawędzią elementu a krawędzią następnego elementu, w naszym przypadku mamy margines dolny, ponieważ po literce m występuje literka b następnie po myślniku mamy warjant, klasa border-bottom powoduje nałożenie stylów zdefiniowanych w tej klasie na dolną krawędź elementów. Nie ma co rozwodzić się na wszystkimi użytymi tym przykładzie klasami. Ponieważ ten kod w 90% został zaczerpnięty z przykładów znajdujących się po tym adresem https://getbootstrap.com/docs/4.5/examples/ jedne co zostało zmienione to podział elementu main na dwie sekcje div i nadanie większego odstępu pierwszej sekcji od drugie wyżej wymienionymi stylami. Teraz nadamy styl stronie głównej.

Poniżej prezentuje się kod szablonu strony głównej, w przeciwieństwie do poprzedniego nie jest tak obszerny.

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

{% block page_header %}
<div class="jumbotron">
    <div class="container">
      <h1 class="display-3">Dolce&Gusto</h1>
      <p>Pyszna pizza tylko na dowóz. Zamów już dziś.</p>
      <p>>a class="btn btn-primary btn-lg" href="{% url 'users:register' %}" role="button"<Zarejestruj się &raquo;</a></p>
    </div>
</div>
{% endblock page_header %}

Tutaj zostaje wykorzystany blok page_header aby umieścić element nazwany przez Bootstap Jumbotronem. Jest to wyróżniający się duży prostokąt, przeważnie z dużym tytułem, drobnym opisem oraz łączem w postaci przycisku. "&raquo;" to encja HTML (encje zawierają znaki specjalnej, cięzko jest wpisać za pomocą klawiatury) przedstawiąca dwa połączone znaki większości lub jak kto woli dwa prawe ostre nawiasty "»" . Przycisk kieruje na formularz rejestracji, teraz sobie obstylujemy formularze. Styl dla formularzy rejestracji będzie taki podstawowy, a styl dla formlarza logowania weźmiemy z przykładów na stronie Bootstrap.

Poniżej znajduje się kod formularza rejestracji, jest to klasyczny książkowy przykład nadawania formularzom w Django stylów Bootstrap 4.

{% extends 'pizzas/base.html' %}
{% load bootstrap4 %}

{% block page_header %}
    <h2>Rejestracja</h2>
{% endblock page_header %}

{% block content %}

    <form action="{% url 'users:register' %}" method="post">

        {% csrf_token %}
        {% bootstrap_form form %}
        {% buttons %}
            <button name="submit" class="btn btn-primary">Zarejestruj się</button>
        {% endbuttons %}

        <input type="hidden" name="next" value="{% url 'pizzas:index' %}" />
    </form<

{% endblock content %}

Ten formularz będzie nieco inny, dodamy obraz w bloku page_header. Musimy dodać również dostosowane dlatego tego formularza style. W tym przykładzie warto zwrócić uwagę na to że nie wykorzystaliśmy formularza przygotowanego przez Django, skorzystaliśmy jedynie z nazw pól odpowiednich dla formularza i przesłaliśmy nasz spersonalizowany formularz. Kod jest nieco obszerny.

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

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}

{% block content %}
<style>
.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}
</style>

    {% if form.errors %}
    <div class="alert alert-danger alert-dismissible fade show" role="alert">
        Niepoprawny login lub hasło. Spróbuj ponownie.
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button<
      </div>
    {% endif %}

    <form class="form-signin" action="{% url 'users:login' %}" method="post">
        {% csrf_token %}
        <h1 class="h3 mb-3 font-weight-normal text-center">Zaloguj się</h1>
        <label for="username" class="sr-only">Nazwa użytkownika</label>
        <input type="text" id="username" name="{{form.username.html_name}}" 
        class="form-control" placeholder="Nazwa użytkownika" required autofocus>
        <label for="form.password" class="sr-only">Hasło</label>
        <input type="password" id="password" name="{{form.password.html_name}}" 
        class="form-control" placeholder="Hasło" required>
        <button class="btn btn-lg btn-primary btn-block" 
        type="submit">Zaloguj</button>
        <p class="mt-5 mb-3 text-muted text-center">&copy; 2020</p>
        <input type="hidden" name="next" value="{% url 'pizzas:index' %}" />
    </form>

{% endblock content %}

Z racji tego iż nasz formularz jest dostosowywany. Musieliśmy też dodać również własny komunikat o tym że logowanie się nie powiodło. Alert z możliwością zamknięcia znajduje się w pomiędzy warunkowymi znacznikami szablonu. Do obstylowania pozostała jeszcze tabelka z zamówieniami, formularz zamówienia, edycja zamówienia, menu, strona pizzy oraz strona wylogowania. Poniżej znajdują sią szablony obstylowanych już stron.

Strona podglądu zamówień:

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

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" /<
{% endblock page_header %}

{% block content %}

<table class="table">
    <thead>
        <tr>
            <th scope="col">Nr zamówienia</th>
            <th scope="col">Pizza</th>
            <th scope="col">Wielkość</th;>
            <th scope="col">Sos_1</th>
            <th scope="col">Sos_2</th>
            <th scope="col">Dostawa</th>
            <th scope="col">Data zamówienia</th>
        {% if user.username == 'pizza_admin' %}
            <th></th>
        {% endif %}
        </tr>
    </thead>
    <tbody>
    {% for order in orders %}
        <tr>
            <th scope="row">{{ order.id }}</th>
            <td>{{ order.pizza.name }}>/td<
            <td>{{ order.size }}</td>
            <td>{{ order.sauce1 }}</td>
            <td>{{ order.sauce2 }}</td>
            <td>{{ order.deliver }}</td>
            <td<{{ order.date_order|date:'d M Y H:i'  }}</td>
            {% if user.username == 'pizza_admin' %}
                <td><a href="{% url 'pizzas:edit_order' order.id %}">Zmień</a></td>
            {% endif %}
        </tr>
    {% empty %}
        <tr><td colspan="7">Brak zamówień</td></tr>
    {% endfor %}
    </tbody>
</table>

{% endblock content %}

Strona formularza zamówień:

{% extends 'pizzas/base.html' %}
{% load bootstrap4 %}

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}

{% block content %}

    <form action="{% url 'pizzas:new_order' %}" method="post">
        {% csrf_token %}
        {% bootstrap_form form %}
        {% buttons %}
            <button name="submit" class="btn btn-primary">Zamów</button>
        {% endbuttons %}
    </form>

{% endblock content %}

Warto zaznaczyć że usunięto znacznik warunkowy, który wyświetlał informacje, o ile zapisanie zmian się nie powiodło, otóż jeśli wyświetlenie formularza pozostawiamy Bootstrapowi, to on sam wyświetli komunikat jeśli w formularzu pojawią się błędy.

Strona edycji zamówień:

{% extends 'pizzas/base.html' %}
{% load bootstrap4 %}

{% block page_header %}
    >img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}


{% block content %}

    <form action="{% url 'pizzas:edit_order' order.id %}" method="post">
        {% csrf_token %}
        {% bootstrap_form form %}
        {% buttons %}
        <button name="submit" class="btn btn-primary">Zapisz zmiany</button>
        {% endbuttons %}
    </form>

{% endblock content %}

Strona menu:

{% extends 'pizzas/index.html' %}

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}

{% block content %}

<table class="table">
    <thead<
        <th scope="col">Pizza</th>
        <th scope="col">Składniki</th>
        <th scope="col"></th>
    </thead>
    <tbody>
        {% for pizza in pizzas %}
        <tr<
            <th scope="row">{{ pizza.name }}</td>
            <td>{{ pizza.topings }}</td>
            <td><a href="{% url 'pizzas:pizza_site' pizza.id %}">Wybierz</a></td>
        </tr>
    {% empty %}
        <tr>Nie ustalono jeszcze menu</tr>
    {% endfor %}
    </tbody>
</table>


{% endblock content %}

Strona pizzy:

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

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}

{% block content %}

<h2>{{ name }}</h2>

<ul class="list-group list-group-flush">
    {% for toping in topings %}
        <li class="list-group-item">{{ toping }}<li>
    {% empty %}
        <li class="list-group-item">Dodatki na życzenie>/li>
    {% endfor %}
</ul>

{% endblock content %}

Strona wylogowania:

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

{% block page_header %}
    <img src="https://i.imgur.com/olxvhk5.jpg" style="width: 100%;" />
{% endblock page_header %}


{% block content %}

<div class="alert alert-primary" role="alert">
    Dziękujemy za skorzystanie z naszej aplikacji.
  </div>



{% endblock content %}

2.8. Platforma Heroku

Teraz kiedy mamy obstylowaną naszą aplikacje, możemy wrzucić ją na platformę sieciową Heroku. Wdrożenie danych na platformę Heroku, będzie wymagało trochę zachodu. Na początku sprawdzimy czy w naszym systemie są dostępne pakiety, które umożliwią kompilacje instalowanego przez pip oprogramowania. Poniższa procedurę testowano na Ubuntu LTS Desktop 20.04, w terminalu wydajemy następujące polecenie.

$ sudo apt install python3-psycopg2 python3-dev libpq-dev postgresql-server-dev-all build-essential

Powyższe polecenie zainstaluje wybrane pakiety jeśli, któregoś brakuje w systemie. Aby móc w ogóle pracować z Heroku potrzebujemy założyć sobie na platformie konto, aby to zrobić przechodzimy pod ten adres: https://signup.heroku.com/.

Po założeniu konta możemy zainstalować heroku w naszym systemie.

$ sudo snap install --classic heroku

Kiedy nasze oprogramowanie zostanie zainstalowane, możemy przejść do katalogu naszą aplikacją i uruchomić środowisko wirtualne.

$ cd python/django3
python/django3 $ source blog_env/bin/activate

W środowisku wirtualnym instalujemy wymagane poniższe pakiety.

(blog_env) python/django3 $ pip install wheel
(blog_env) python/django3 $ pip install psycopg2
(blog_env) python/django3 $ pip install django-heroku
(blog_env) python/django3 $ pip install gunicorn

Aby nasza aplikacja mogła działać na platformie Heroku w sposób identyczny jak na komputerze lokalnym, musi mieć zainstalowane takie samo oprogramowanie jakie instalowaliśmy przez pip do tej pory. Nie było tego dużo, django oraz django-bootstrap4. Jednak Heroku nie rozwiąże sobie zależności samodzielnie, dlatego na liście trzeba uwzględnić wszystko razem z zależnościami, to zadanie możemy wykonać jednym poleceniem.

(blog_env) python/django3 $ pip freeze > requirements.txt

Plik musi nazywać requirements.txt. Tak wygląda jego zawrtość:

asgiref==3.2.10
beautifulsoup4==4.9.1
dj-database-url==0.5.0
Django==3.1
django-bootstrap4==2.2.0
django-heroku==0.3.1
gunicorn==20.0.4
psycopg2==2.8.5
pytz==2020.1
soupsieve==2.0.1
sqlparse==0.3.1
whitenoise==5.2.0

Uwaga, może się zdarzyć że w tym pliku znajdzie się taka linia: pkg-resources==0.0.0. Należy ją usunąć, gdyż nasz projekt nie zostanie zbudowany na Heroku, bo nie będzie mógł odnaleźć takiej zależności. Ta linia nie jest istotna, została wpisana do pliku przez błąd pip.

Po utworzeniu listy zależności należy wskazać Heroku, jakiej wersji Pythona ma używać dla naszego projektu, wersje sprawdzimy poniższym poleceniem.

(blog_env) python/django3 $ python --version
Python 3.8.2

Natomiast zawartość pliku runtime.txt (nazwa pliku musi być identyczna) musi wyglądać następująco:

python-3.8.2

Python z małej litery i myślnik zamiast spacji. Nastepnym a zarazem ostatnim potrzebnym nam plikiem, będzie plik stricte Heroku Procfile, w nim zostanie zdefiniowany proces maszyny wirtualnej odpowiedzialny za hostowanie naszego projektu.

Zawrtość pliku Procfile

web: gunicorn blog.wsgi --log-file -

Teraz kiedy mamy wszystkie pliki możemy dopisać ustawienia dla Heroku w naszym projekcie. W głównym katalogu projektu w pliku settings.py na samym końcu pliku dopisujemy poniższe linie.

import django_heroku
django_heroku.settings(locals())

Aby móc synchronizować nasz kod źródłowy z platformą Heroku skorzystamy z systemu kontroli wersji Git. Git w dużym skrócie tworzy zatwierdzenie (commit) zmian w kodzie, które są tak jakby oddzielną jego wersją. Projekty tworzą repozytoria, w których przechowywane są zatwierdzenia, każda nowa funkcjonalność może być (nawet dobrze, gdyby była) kolejnym zatwierdzeniem. Daje nam to możliwość powrotu do poprzedniej wersji w razie problemów. Git jest obecnie standardem zarządania kodem źródłowym, w nie długim czasie od publikacji tego materiału pojawi się kolejny tym razem dużo mniejszy posty odnośnie podstaw Git-a.

Git powinien być już zainstalowany w systemie. Jednak warto się upewnić wydając polecenie:

$ git --version
git version 2.25.1

Jeśli dostaniemy taki wynik polecenie, oznacza to że Git jest już zainstalowany w systemie. Instalacja na Ubuntu wymaga jednego polecenia:

$ sudo apt install git

Teraz możemy przejść do konfiguracji, musimy podać nazwę użytkownika oraz adres e-mail, który dokonuje zatwierdzeń, ponieważ Git to monitoruje, jeśli zapomnimy o tym kroku, Git będzie wymagał podania tych danych przy pierwszym przesłaniu kodu do zdalnego repozytorium na platformie Heroku.

$ git config --global user.name "xf0r3m"
$ git config --global user.email "morketsmerke@gmail.com"

Po skonfigurowaniu gita możemy utworzyć puste repozytorium w katalogu z danymi projektu wydając bardzo proste polecenie

python/django3 $ git init

Powinniśmy dostać taki wyniki działania dla tego polecenia:

Initialized empty Git repository in /home/xf0r3m/python/django3/.git/

Teraz dodamy plik, w którym zdefiniujemy wszystkie pliki/katalogi, które mają być ignorowane przez Git podczas dodawania zmian do repozytorium. Tworzymy plik .gitignore, plik ten będzie ukryty z powodu kropki poprzedzającej nazwę pliku. W tym pliku wpisujemy katalog środowiska wirtualnego blog_env/, katalog __pycache__/ oraz wszystkie pliki baz danych *.sqlite3. Dlaczego? Otóż monitowanie plików lokalnej bazy danych jest generalnie złą praktyką i może doprowadzić do niezłych tarapatów, kiedy np. nadpiszemy bazę produkcyjną tą na platformie Heroku, bazą testową z Git-a. Dlatego pomijamy ten plik. Poniżej znajduje się zawartość pliku .gitignore

	blog_env/
__pycache__/
*.sqlite3

Teraz kiedy mamy już zdefiniowany plik .gitignore, możemy przejść do pierwszego zatwierdzenia. W katalogu projektu wydajemy polecenie:

$ git add .

To polecenie doda do repozytorium wszystkie pliki projektu poza plikami i katalogami z pliku .gitignore.

$ git commit -am "Projekt gotowy do wdrożenia na platformie Heroku"

Powyższe polecenie powoduje właśnie zatwierdzenie. Opcja -a powoduje uzwględnienie wszystkich zmodyfikowanych plików do zatwierdzenia, natomiast opcja -m powoduje dodanie komunikatu do zatwierdzenia.

Po wydaniu powyższego polecenia warto wydać polecenie:

$ git status

Aby sprawdzić stan wykonanego zatwierdzania, wynik działania polecenia powinien wyglądać podobnie jak poniżej.

$ git status
On branch master
nothing to commit, working tree clean

Po wykonaniu pierwszego zatwierdzenia przyszedł czas na przekazanie projektu do Heroku. Teraz musimy przypomnieć sobie adres e-mail, na jaki zakładaliśmy konto Heroku oraz jakie było do niego hasło. Jeśli mamy już te dane w pobliżu wydajemy polecenie

python/django3 $ heroku login
heroku: Press any key to open up the browser to login or q to exit:
Logging in... done
Logged in as user@example.com

Po wydaniu powyższego polecenia, zgodnie z instrukcją naciskamy dowolny klawisz poza literą q, aby otworzyła nam się przeglądarka. Tam podajemy dane dostępowe do Heroku, po poprawny zalogowaniu, zostanie wyświetlony komunikat o poprawyn zalogowaniu oraz o tym że możemy już zamknąć okno przeglądarki. W terminalu pod instrukcją wyświetloną przez polecenie, zostaną dopisane pozostałe dwie linie takie jak przedstawione powyżej.

Po zalogowaniu możemy już utworzyć nową aplikację wydając w terminalu poniższe polecenie.

python/django3 $ heroku create
Creating app... done, ⬢ dry-atoll-67152
https://dry-atoll-67152.herokuapp.com/ | https://git.heroku.com/dry-atoll-67152.git

W odpowiedzi na to polecenie otrzymamy nazwę nowo utworzonej aplikacji na Heroku, potem zmienimy ją na bardziej przystępną, oraz dwa adresy. Jeden pod którym aplikacja jest dostępna w internecie, natomiast drugi to adres zdalnego repozytorium Git, na który będziemy wpychać zatwierdzenia kodu naszej aplikacji. Adres Git dla naszej aplikacji Heroku został już automatycznie zdefiniowany jako adres zdalnego repozytorium pod nazwą heroku. Dokonajmy teraz pierwszego wepchnięcia oraz zbudujmy naszą aplikacje na platformie Heroku. Budowa następuje automatycznie po wepchnięciu.

python/django3 $ git push heroku master

Po wydaniu tego polecenia na ekranie zostanie wyświetlony cały proces budowania aplikacji w Heroku.

Enumerating objects: 46, done.
Counting objects: 100% (46/46), done.
Delta compression using up to 4 threads
Compressing objects: 100% (39/39), done.
Writing objects: 100% (46/46), 9.12 KiB | 1.14 MiB/s, done.
Total 46 (delta 6), reused 0 (delta 0)
remote: Compressing source files... done.
remote: Building source:
remote:
remote: -----> Python app detected
remote:  !     Python has released a security update! Please consider upgrading to python-3.8.5
remote:        Learn More: https://devcenter.heroku.com/articles/python-runtimes
remote: -----> Installing python-3.8.2
remote: -----> Installing pip 20.1.1, setuptools 47.1.1 and wheel 0.34.2
remote: -----> Installing SQLite3
remote: -----> Installing requirements with pip
remote:        Collecting asgiref==3.2.10
remote:          Downloading asgiref-3.2.10-py3-none-any.whl (19 kB)
remote:        Collecting beautifulsoup4==4.9.1
remote:          Downloading beautifulsoup4-4.9.1-py3-none-any.whl (115 kB)
remote:        Collecting dj-database-url==0.5.0
remote:          Downloading dj_database_url-0.5.0-py2.py3-none-any.whl (5.5 kB)
remote:        Collecting Django==3.1
remote:          Downloading Django-3.1-py3-none-any.whl (7.8 MB)
remote:        Collecting django-bootstrap4==2.2.0
remote:          Downloading django_bootstrap4-2.2.0-py3-none-any.whl (24 kB)
remote:        Collecting django-heroku==0.3.1
remote:          Downloading django_heroku-0.3.1-py2.py3-none-any.whl (6.2 kB)
remote:        Collecting gunicorn==20.0.4
remote:          Downloading gunicorn-20.0.4-py2.py3-none-any.whl (77 kB)
remote:        Collecting psycopg2==2.8.5
remote:          Downloading psycopg2-2.8.5.tar.gz (380 kB)
remote:        Collecting pytz==2020.1
remote:          Downloading pytz-2020.1-py2.py3-none-any.whl (510 kB)
remote:        Collecting soupsieve==2.0.1
remote:          Downloading soupsieve-2.0.1-py3-none-any.whl (32 kB)
remote:        Collecting sqlparse==0.3.1
remote:          Downloading sqlparse-0.3.1-py2.py3-none-any.whl (40 kB)
remote:        Collecting whitenoise==5.2.0
remote:          Downloading whitenoise-5.2.0-py2.py3-none-any.whl (19 kB)
remote:        Building wheels for collected packages: psycopg2
remote:          Building wheel for psycopg2 (setup.py): started
remote:          Building wheel for psycopg2 (setup.py): finished with status 'done'
remote:          Created wheel for psycopg2: 
filename=psycopg2-2.8.5-cp38-cp38-linux_x86_64.whl size=483298 
sha256=7daad67c78674aaee0b224f104bccb4ab6a0dbc40682c37b80f6f6e5572f5f7e
remote:          Stored in directory: /tmp/pip-ephem-wheel-cache-105idbzs/
wheels/35/64/21/9c9e2c1bb9cd6bca3c1b97b955615e37fd309f8e8b0b9fdf1a
remote:        Successfully built psycopg2
remote:        Installing collected packages: asgiref, soupsieve, beautifulsoup4, 
dj-database-url, pytz, sqlparse, Django, django-bootstrap4, whitenoise, psycopg2,
django-heroku, gunicorn
remote:        Successfully installed Django-3.1 asgiref-3.2.10 
beautifulsoup4-4.9.1 dj-database-url-0.5.0 django-bootstrap4-2.2.0 
django-heroku-0.3.1 gunicorn-20.0.4 psycopg2-2.8.5 pytz-2020.1
soupsieve-2.0.1 sqlparse-0.3.1 whitenoise-5.2.0
remote: -----> $ python manage.py collectstatic --noinput
remote:        132 static files copied to '/tmp/build_f49f7b9a/staticfiles', 
418 post-processed.
remote:
remote: -----> Discovering process types
remote:        Procfile declares types -> web
remote:
remote: -----> Compressing...
remote:        Done: 56.1M
remote: -----> Launching...
remote:        Released v5
remote:        https://dry-atoll-67152.herokuapp.com/ deployed to Heroku
remote:
remote: Verifying deploy... done.
To https://git.heroku.com/dry-atoll-67152.git
 *[new branch]      master -> master

Jeśli otrzymaliśmy podobny input, to znaczy że cały proces przebiegł prawidłowo. Teraz jeśli wydamy polecenie:

python/django3 $ heroku open

Zostanie otworzona przeglądarka z nasza aplikacją w Heroku. Jednak, jeśli klikniemy w odnośnik 'Menu', zostanie nam zrócona strona błedu ProgrammingError, oznacza to mniej więcej tyle i aż tyle że nie ma bazy danych. Utworzenie bazy danych oraz dodanie superużytkownika, to dwie kolejne czynności, które możemy wykonać dwa różne sposoby. Przedstawie je poniżej, aby byśmy zwrócili uwagę na to że z aplikacjami na Heroku można wchodzić w interakcje w dwojaki sposób. Pierwszy z nich to wydawanie konkretnych poleceń poprzedzając nasze polecenie słowami heroku run. Uruchamiamy daną czynność tak samo jak w środowisku wirtualnym na naszym lokalnym komputerze. Tak też stworzymy bazę danych.

python/django3 $ heroku run python manage.py migrate
Running python manage.py migrate on ⬢ dry-atoll-67152... up, run.7671 (Free)
Operations to perform:
  Apply all migrations: admin, auth, blogs, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying blogs.0001_initial... OK
  Applying blogs.0002_blogpost_owner... OK
  Applying sessions.0001_initial... OK

Jak można zauważyć na początku trwa nawiązywanie połączenia z naszym kontenerem (naszą aplikacją), a następnie wykonywane jest polecenie. Drugim sposobem jest uruchomienie powłoki w środowisku kontenera Heroku, za pomocą tej metody utworzymy superużytkownika. Aby uruchomić powłokę należy wydać następujące polecenie.

python/django3 $ heroku run bash
Running bash on ⬢ dry-atoll-67152... up, run.8503 (Free)
~ $

Po połączeniu zostanie na zrócony taki charakterystyczny, krótki znak zachęty. Zaraz zanim wydajemy polecenie, takie jakie wydawaliśmy podczas tworzenie superużytkownika na naszym komputerze.

~ $ python manage.py createsuperuser
Użytkownik (leave blank to use 'u17122'): blog_admin
Adres email:
Password:
Password (again):
Superuser created successfully.

Teraz możemy przjeść do naszej aplikacji oraz móc sprawdzić moduł logowania oraz zalogować się do witryny administracyjnej aby zdefiniować trochę danych.

Teraz kiedy nasza aplikacja jest gotowa, możemy przjeść do nadania jej nieco przyjaźniejszej nazwy. Zmianę nazwy realizujemy za pomocą polecenia:

python/django3 $ heroku apps:rename blogs-morkme-v01

Pamiętać jednak należy o tym że podczas nadawania nazw w Heroku możemy korzystać jedynie z liter, cyfr oraz myślników.

Teraz przejdziemy do zabepieczenia naszej aplikacji. Zwróćmy uwagę na to, że gdy w plikach naszego projektu na Heroku zabrakło bazy danych, została na zwrócona strona z opisem błędu ProgrammingError, ta strona została przygotowana przez Debuger. W środowisku testowym jest powszechne i często spotykane zjawisko, że w razie błędów zostanie wyświetlona strona z informacjami o tym gdzie szukać danego błędu, jednak w środowisku produkcyjnym takim jak Heroku jest to niedopuszczalne, takie strony zazwyczaj zwracają za dużo newralgicznych danych i mogą być wykorzystane do ataków przeciwko nam. Na podstawie zmiennej środowiskowej będziemy regulować wyświetlanie stron debugera. Z racji tego iż w Django zmienna Debug przyjmuje wartość boolowską, a z kolei zmienne środowiskowe przechowują ciągi tekstowe, będzie nam potrzebna konstrukcja if, którą zaimplementujemy w pliku settings.py w głównym katalogu projektu.

Naszą implementacje zaczynamy od zaimportowania obiektu environ modułu os na górze pliku. Następnie na samym dole pliku wprowadzamy poniższy kod.

if environ.get('DEBUG') == 'TRUE':
    DEBUG = True
elif environ.get('DEBUG') == 'FALSE':
    DEBUG = False

Za pomocą metody get obiektu environ, pobieramy wartość zmiennej środowiskowej, której nazwa jest pobierana jako ciąg tekstowy w pierwszym argumencie. W zależności od wyniku przyrównania wartości w zmiennej DEBUG nadawana jest wartość boolowska True or False. Z racji tego iż przesłaliśmy plik settings.py, wraz ustawioną opcją DEBUG na True. To teraz musimy ją zmienić w naszej aplikacji na platformie Heroku, możemy tego dokonać za pomocą jednego polecenia.

python/django3 $ heroku config:set DEBUG=FALSE

To polecenie spowoduje ustawienie podanej zmiennej środowiskowej na podaną wartość oraz restart aplikacji. Teraz otworzymy naszą aplikację za pomocą polecenia heroku open i przejdziemy pod stronę, której nie zaimplementowano w naszej aplikacji. Powinniśmy otrzymać typowy komunikat Not Found. Ten komunikat nie pasuje do szaty graficznej naszej aplikacji, my jednak możemy dodać własne szablony dla typowych błędów. Szablony będą banalne, ale będą miały jedną ważną cechę. Będą ładować podstawowy wygląd naszej aplikacji. Zaczynamy od utworzenia katalogu templates w głównym katalogu projektu. W tym katalogu tworzymy dwa pliki: 404.html oraz 500.html. Poniżej zamieszczam zawartość każdego szablonu.

Plik 404.html

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

{% block page_header %}
    <h2<Żądana strona nie zostałą odnaleziona (404).</h2>
{% endblock page_header %}

Plik 500.html

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

{% block page_header %}
    <h2>Wystąpił wewnętrzny błąd serwera (500).</h2>
{% endblock page_header %}

Teraz wracamy do naszego pliku settings.py w nim ustawiamy jedną rzecz. Na początku importujemy obiekt path z modułu os. Następnie odszukujemy klucz DIRS w słowniku wenątrz listy TEMPLATES. Następnie wewnątrz listy DIRS wpisujemy jedną wartość zwracaną przez metodę join obiektu path, która jako argumenty przyjmuje ścieżkę bazową BASE_DIR oraz ścieżkę dostępu do szablonów względem ścieżki bazowej. Trochę to zagmatwane, ale jak spojrzymy w górę kodu dostrzerzemy fakt iż BASE_DIR to zmienna, którą możemy sobie odtworzyć w powłoce. Jako__file__ podstawimy ścieżke do dostępową do pliku settings.py względem naszego katalogu głównego z plikami, gdzie znajdują katalog projektu, katalogi aplikacji oraz katalog ze środowiskiem wirtualnym. Poniżej zamieściłem listing z konsoli jak możemy sprawdzić naszą ścieżkę.

>>>from pathlib import Path
>>> base_dir = Path('blog/settings.py').resolve(strict=True).parent.parent
>>> base_dir
PosixPath('/home/xf0r3m/python/django3')

Teraz znając już wartość naszej zmiennej BASE_DIR możemy perfekcyjnie ustalić ścieżke dostępową do naszych szablonów.

Warto pamiętać o tym, że te szablony będą wyświetlane tylko wtedy, gdy opcja DEBUG jest ustawiona na False. Możemy je przetestować żądając strony, której nie ma, otrzymamy stronę błędu 404. Natomiast jeśli spróbujemy edytować zamówienie, którego niema to w ten czas otrzymamy stronę błędu 500. Skupmy się przez chwilę na tym drugim przypadku, kiedy chcemy edytować zamowienie, którego nie ma. Przezkazujemy za pomocą metody GET identyfikator zamówienia, kiedy widok próbuje wydobyć informacje odnośnie rekordu o takim identyfikatorze otzymuje wartość None oznacza to tyle, że nie ma takiego rekordu, jeśli więc nie ma takiego rekordu to żądana strona edycji zamówienia nie może istnieć, więc lepiej użyć strony 404 niż 500. Można to zrealizować wykorzystując funkcję get_object_or_404 z tego samego modułu django.shortcuts, z którego importowaliśmy funkcje render oraz redirect, następnie możemy jej użyć w każdym widoku wymagającym identyfikatora np. w zapytaniach do bazy.

# order = Order.objects.get(id=order_id)
order = get_object_or_404(Order, id=order_id)

Zbliżamy się do końca tego materiału, jednak zanim się rozejdziemy dalej poznawać świat Pythona, posprzątamy po sobie. Jednak jeśli chcesz możesz w ramach ćwiczenia rozwinąć projekt Pizzeria do stanu użytczeności.

Jednak jeśli ta aplikacja, która była z nami przez dłuższą część nauki frameworka Django przestała być potrzebna, to możemy ją usunąć z platformy wydajac polecenie:

python/django2 $ heroku apps:destroy pizzeria-morkme-1a
 ▸    WARNING: This will delete ⬢ pizzeria-morkme-1a including
 ▸    all add-ons.
 ▸    To proceed, type pizzeria-morkme-1a or re-run this
 ▸    command with --confirm pizzeria-morkme-1a
 >

Heroku zarząda od nas potwierdzenia, w postaci ponownego wprowadzenia nazwy aplikacji. Po jej podaniu, otrzymamy informacje poprawnym zniszczeniu (usunięciu) aplikacji na platformie Heroku.

> pizzeria-morkme-1a
Destroying ⬢ pizzeria-morkme-1a (including all add-ons)... done

Jeśli chodzi o podstawy Pythona oraz Django to wydaje mi się, że mamy je już za sobą.