Daj Się Poznać 2017, Python, TaskList

Co będzie z konkursowym projektem?

Konkurs Daj Się Poznać 2017 minął, Gala Finałowa zakończyła się, teraz czas na to, co można jeszcze z konkursowym projektem zrobić. Niektórzy finaliści z edycji 2016 wykruszyli się, niektórzy rozwinęli projekty.

Teraz wymienię rzeczy do wprowadzenia na przyszłość:

  • Testy jednostkowe dla logiki aplikacji wraz z jej weryfikacją;
  • Oddzielenie front-end od back-end przez 2 osobne serwery;
  • Front-end w Angular 4+
  • Back-end w Python 3+ wystawiający RESTowe web serwisy;
  • Testy integracyjne;
  • Testy interfejsu użytkownika.

Co się uda zaimplementować? To wyjdzie w praniu. Na razie nie narzucam sobie terminu.

Daj Się Poznać 2017, Podsumowanie, Specjalne

Refleksje po konkursie Daj Się Poznać 2017

Etap konkursowy już minął. Zostałem finalistą Daj Się Poznać 2017. Zdarzało się

Na ścisłego finalistę głosów miałem za mało, ale nie przejmuję się tym. Najważniejsze, że udało się wytrwać do końca. Może w następnej edycji się uda dojść do tego etapu ;).

Parę śledzonych przeze mnie blogów nie mogło zakwalifikować się do finału, chociaż mieli ciekawe pomysły. Dwa, które najbardziej zapamiętałem to JAVOVANIE (https://javovanie.wordpress.com/) i blog Justyny Wojtczak. Ten pierwszy to był projekt gry na platformę Android. Drugi – stroik do skrzypiec na watchOS pisany w Swift 3.

Teraz moje zdanie o konkursie.

Bardzo mi się podoba formuła konkursu i założenie, że najważniejsza jest wytrwałość. Rok temu śledziłem rozwój konkursu, ale nie miałem odwagi w nim wystartować. Bałem się, że nie dam rady łączyć pracy zawodowej, końcówki studiów i pisania pracy magisterskiej, z którą miałem trochę przygód. Ale postanowiłem, że w następnej edycji wystartuję. Jak tylko się dowiedziałem, że rusza rejestracja, zastanawiałem się nad wyborem projektu do opracowania przez ten czas i wziąłem na tapetę projekt zaliczeniowy ze studiów.

Dlaczego ten projekt? Żeby na własnej skórze przekonać się, co to znaczy pracować ze swoim starym kodem. Sprawdzić, czego wtedy nie uwzględniłem. Zastanowić się, jak można ulepszyć stary projekt.

Mimo wszystko gratulacje wszystkim, którzy wytrwali do końca! I tym, którym się nie udało, ale założyli bloga. Teraz odliczam czas do Gali Finałowej w Warszawie po zwariowanym poprzednim tygodniu.

 

Daj Się Poznać 2017, TaskList

Co jeszcze można rozwinąć?

Projekt już działa na Heroku, większość (3 na 4) cele na dalszy rozwój wykonane, koniec maja się zbliża, więc można zbliżyć się do podsumowań.

Pierwsza rzecz: odzyskiwanie hasła użytkownika. Opcja, która nieraz się przyda.

Druga rzecz: rejestracja za pomocą e-maila, zamiast loginu. A jak rejestracja, to jeszcze dołóżmy możliwość rejestracji za pomocą danych z Facebooka, Twittera i innych sieci społecznościowych.

Trzecia rzecz: możliwość usunięcia konta użytkownika.

Następnym razem będzie o tym, co się zmieniło w stosunku do początków.

Daj Się Poznać 2017, Programowanie, Python, TaskList

Strony błędów

Projekt się rozwija, przechodzi refaktoryzacje, ale jeszcze nie ma obsługi błędów HTTP. Czas to zmienić.

Konwencja jest taka: jeśli jest plik HTML z kodem błędu, ten szablon zostanie wyświetlony w odpowiedzi na błąd HTTP.

Więc stworzyłem pliki dla błędów 400, 403, 404 i 500.

Szablon układu już jest, więc z niego korzystam. Zawarość tych stronek wygląda tak:

<pre>{% extends 'layout.html' %}
{% block title %} - jakiś tytuł{% endblock %}

{% block content %}
Tutaj treść
{% endblock %}</pre>

Jeśli jest w settings.py Debug = True, nie zobaczymy tych stron błędów. Dlatego trzeba zmienić wartość na False. Wtedy zadziałają.

Przykład dla błędu 404:

Screenshot_20170512_161611.png

Daj Się Poznać 2017, Python, TaskList

Wdrożenie projektu na Heroku

Na portalu Heroku.com można udostępnić całemu światu różne aplikacje, w tym te napisane w Pythonie, jak ten projekt. Wcześniej projekt był testowany na różnych środowiskach testowych: Fedora 25, Arch Linux, Windows 10. Teraz pora na Heroku.

Aby ułatwić sobie życie z rozwijaniem i wdrażaniem, utworzyłem nową gałąź „heroku”. W tej gałęzi wrzucam wszelkie zmiany w konfiguracji.

Wymagania

Przed wdrożeniem na Heroku projektu trzeba założyć konto. Założenie konta jest proste: przechodzisz przez formularz rejestracyjny, aktywujesz konto i możesz wrzucać aplikacje. Możesz też instalować dodatki, takie jak bazy danych.

Mając założone konto, trzeba było zaktualizować plik requirements.txt, utworzyć plik runtime.txt i Procfile. Plik runtime.txt zawiera nazwę i wersję środowiska Python, które będzie używane. W Procfile zawarłem polecenia do wykonania podczas wdrażania.

W pliku requirements.txt dołożyłem następujące moduły: gunicorn, whitenoise, dj-database-url, psycopg2. Wykorzystywaną bazą danych jest baza PostgreSQL, zamiast bazy SQLite, którą wykorzystuję do testów.

Wdrażanie

Wdrażanie realizuję przez integrację z Githubem. Po połączeniu z Githubem wybieram gałąź heroku i wciskam Deploy Branch. Zaczyna się proces wdrażania.

Są trzy etapy: pierwszy – pobranie kodu z gałęzi na Githubie. Drugi etap – budowa projektu (pobranie zależności, wykonanie działań konfiguracyjnych, utworzenie kontenera z aplikacją) i trzeci – udostępnienie aplikacji. Jeżeli etap drugi zakończy się powodzeniem, aplikacja będzie dostępna pod wskazanym adresem.

Co jeszcze zostaje po wdrożeniu?

Żeby aplikacja działała, potrzebne są jeszcze: utworzenie odpowiednich tabel w bazie danych. Wykorzystałem dobrze znane polecenie python manage.py migrate, ale okazało się, że to za mało. Nie było tabeli TaskList_task. Musiałem ją ręcznie utworzyć.

W SQLite polecenie do tworzenia tabeli jest takie:


CREATE TABLE IF NOT EXISTS "TaskList_task" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(256) NOT NULL, "description" text NOT NULL, "end" datetime NOT NULL, "done" bool NOT NULL, "start" datetime NOT NULL, "user_id" integer NOT NULL default 0);

Aby przystosować to polecenie do specyfiki PostgreSQL trzeba dokonać pewnych zmian: nie ma kolumny AUTOINCREMENT – trzeba zastosować SERIAL lub tak jak
w Oracle utworzyć sekwencję i pobierać kolejną wartość z sekwencji jako identyfikator zadania.
Druga rzecz: zamiana datetime na timestamp. I trzecia: zamienić bool na boolean.

Efekty zmian są takie:


CREATE TABLE IF NOT EXISTS "TaskList_task" ("id" SERIAL UNIQUE, "name" varchar(256) NOT NULL, "description" text NOT NULL, "end" TIMESTAMP NOT NULL, "done" boolean NOT NULL, "start" TIMESTAMP NOT NULL, "user_id" integer NOT NULL default 0);

Jeszcze doszły problemy z nakładaniem się ograniczenia na tabeli auth_user (tabela z użytkownikami). Polegały na rzucaniu wyjątkami o złamaniu ograniczenia na nowo tworzonym użytkowniku. Weryfikacja, czy użytkownik istnieje, już jest w aplikacji i kończy się komunikatem walidacyjnym, że taki użytkownik już istnieje.

Wystarczyło usunąć to ograniczenie. Przy okazji mała refaktoryzacja – zamiana obsługi wyjątku DoesNotExist na metodę get_or_create oraz zmiana klasy bazowej RegisterForm z CreateView na FormView.

Do czego są dodatkowe pakiety?

Pakiet gunicorn jest serwerem WWW, który można wykorzystać produkcyjnie.
Pakiet dj-database-url pozwala wyciągnąć pasującą Django konfigurację połączenia z bazą danych.
Pakiet whitenoise pozwala na udostępnianie plików statycznych (HTML, CSS, JavaScript) protokołem HTTP, jak serwer WWW, np. przez adres /static.
Pakiet psycopg2 pozwala obsłużyć PostgreSQL. Innymi słowy adapter PostgreSQL dla Pythona.

Efekt końcowy

Aplikacja działa pod adresem https://django-tasklist.herokuapp.com/.

Możecie testować ile wlezie.

Daj Się Poznać 2017, Python, TaskList

Uruchamiamy w trybie produkcyjnym

Na razie działaliśmy na zapewnianym przez Django testowym serwerze. Teraz będziemy próbowali uruchomić aplikację na serwerze WWW.

Testy będę prowadzić na maszynie wirtualnej z Arch Linux.

Opis instalacji Arch Linux od podstaw pominę, żeby nie zanudzać czytelników. Jeżeli chcecie dowiedzieć się więcej, radzę zapoznać się z Installation Guide i wypróbować samemu na wirtualnej maszynie lub zbędnym komputerze.

Przejdźmy do konkretów.

Jakie programy potrzebujemy? Serwer WWW, WSGI, Python 3.4 lub nowszy, PIP oraz Django 1.9 lub nowsze (najlepiej 1.11 wzwyż). Zainstalujmy je.

Projekt umieściłem w katalogu /home/server/tasklist. Jak? Gitem. Przez sklonowanie repozytorium https://github.com/szymonsiecinski/tasklist.

Po instalacji niezbędnego oprogramowania, czas na instalację dodatkowych pakietów przez PIP poleceniem
pip install -r requirements.txt

Po instalacji czas na małą zmianę w pliku settings.py, dzięki której ułatwimy sobie życie. Chodzi o dopisanie tej linijki:

STATIC_ROOT = os.path.join(BASE_DIR, 'static')

W tym pliku BASE_DIR jest zdefiniowane jako ścieżka, w której jest projekt, więc czemu z tego nie skorzystać zamiast wklepywać z palca?

Teraz wykonujemy polecenie
python manage.py collectstatic

aby wszystkie statyczne pliki (css, js, fonty) wylądowały w jednym miejscu, dzięki czemu łatwiej będzie udostępniać je.

W przypadku użytej maszyny wirtualnej zdecydowałem się na serwer Apache oraz moduł WSGI. Przejdźmy zatem do ustawień serwera.

Jaki użytkownik oraz grupa będzie mieć dostęp do pliku – kwestia gustu. Domyślnie jest http, które pozostawiłem.

Włączmy też moduł WSGI oraz aliasu:

Teraz czas na to, co tygryski lubią najbardziej: konfiguracja (wirtualnego) serwera z naszym projektem.

<VirtualHost *:80>
Alias /static /home/server/tasklist/static/

WSGIDaemonProcess tasklist python-path=/home/server/tasklist/TaskList/wsgi.py:/home/server/tasklist
WSGIProcessGroup tasklist

<Directory /home/server/tasklist/static>
Order allow,deny
Allow from all
Require all granted
</Directory>

WSGIScriptAlias / /home/server/tasklist/TaskList/wsgi.py process-group=tasklist

<Directory /home/server/tasklist/TaskList>
<Files wsgi.py>
Order deny,allow
Allow from all
Require all granted
</Files>
</Directory>

</VirtualHost>

I parę wyjaśnień: Alias oznacza ustawienie nowego adresu dla serwera z zawartością wskazanego katalogu. Do tego, aby aplikacja działała, trzeba ustawić demona (usługę) WSGI z projektem. Projekt można dowolnie nazwać, ale bardzo ważne jest python-path. Tam podajemy ścieżkę do naszej aplikacji. Jeżeli potrzebujemy ustawić więcej niż jedną ścieżkę, wykorzystujemy dwukropek (:) do rozdzielenia ścieżek w jednej linii.

Potem ustawiamy WSGIProcessGroup (do ustawienia procesu) oraz alias WSGIScriptAlias. Pierwszy argument to ścieżka aplikacji, drugi to ścieżka do pliku wsgi.py z ustawieniami aplikacji.

W znacznikach Directory zawarłem ustawienia dotyczące dostępu do wskazanych katalogów.

Jeszcze jedna bardzo ważna rzecz: bez przyznania praw do odczytu i dostępu dla katalogu z aplikacją będą błędy 403 (dostęp zabroniony). A plik z bazą danych musi być dostępny do zapisu dla serwera WWW.

Plik httpd.conf z konfiguracją zapisuję i uruchamiam serwer.

I na dowód parę zrzutów ekranowych:

Ten pokaz slajdów wymaga włączonego JavaScript.

Daj Się Poznać 2017, Programowanie, Python, TaskList

Przyjęte konwencje w projekcie

Jeżeli ktoś śledził mój kod w repozytorium, pewnie zauważył, że są jakieś konwencje. W tym wpisie spiszę konwencje, które przyjąłem.

Nie więcej niż 100 znaków w linii

Dzięki takiemu ograniczeniu łatwiej czytać tekst na węższym ekranie lub jeżeli otwieram obok siebie dwa pliki tekstowe.

Moduł zawiera nie więcej niż 100 linii

Dzięki temu każdy moduł nie jest wielkim blokiem tekstu, który trudno się czyta. Jeżeli moduł się rozrasta, do nowego wydzielane są dotychczas występujące w nim klasy.

Konwencje nazewnicze

Przyjąłem CoRobiModuł<Typ>, np. LoginView – klasa obsługująca widok logowania użytkownika, LoginForm – klasa obsługująca formularz logowania.

Klasy obsługujące cykl życia danych związanych z zadaniami mają konwencję Task<Operacja>, np. TaskCreate, TaskDelete, TaskList.

Jeśli chodzi o nazwy klas, zapisywane są CamelCase, np. RegisterView. Nazwy zmiennych i pól są zapisywane snake_case, np. template_name.

Nazwy plików są zapisywanie snake_case, jeżeli zawierają więcej niż jedną klasę, przewiduje się istnienie w nich więcej niż jednej klasy lub nie zawierają klas. W przypadku plików zawierających jedną klasę zastosowałem CamelCase (w jednym przypadku: AuthenticationBackend).

Wcięcia

W Pythonie nie stosuje się nawiasów klamrowych do oznaczania bloków kodu. Stosuje się wcięcia. Za wcięcie przyjęte są cztery spacje.

Daj Się Poznać 2017, Programowanie, Python, TaskList

Refaktoryzacja strony głównej

Skoro strona zmiany hasła użytkownika oraz rejestracji użytkownika przeszły zmiany, to czas na odświeżenie strony głównej. Zmiany będą podobne to tych dotyczących strony zmiany hasła użytkownika oraz rejestracji użytkownika, co oznacza taki scenariusz:

  1. utworzenie klasy obsługującej formularz;
  2. zmiana klasy bazowej klasy widoku na FormView;
  3. dodanie atrybutu form_class oraz success_url;
  4. nadpisanie metod get_context_data oraz form_valid;
  5. usunięcie starych metod get i post;
  6. usunięcie ręcznie wpisanych pól i zastąpienie ich automatycznie generowanym formularzem zgodnym z Bootstrap 3;

Postępując według tego scenariusza utworzyłem klasę LoginForm

<pre>class LoginForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(LoginForm, self).__init__(*args, **kwargs)

    class Meta:
        model = User
        fields = ['username', 'password']
        widgets = {
            'password': forms.PasswordInput()
        }
        help_texts = {
            'username': ""
        }

    def clean(self):
        return self.cleaned_data</pre>

Następnie zmieniłem klasę bazową na FormView i zacząłem dodawać nowe metody i pola, aby potem przenieść ważny kod ze starych metod do nowych, co poskutkuje takim kodem:

<pre>class LoginView(FormView):
    """
    odpowiada za obsługę widoku logowania użytkownika
    """
    template_name = "TaskList/login.html"
    form_class = LoginForm
    success_url = reverse_lazy("task_list")

    def get_context_data(self, **kwargs):
        context = super(LoginView, self).get_context_data(**kwargs)
        context['user'] = None
        context['active'] = "home"
        return context

    def form_valid(self, form):
        username = form.cleaned_data['username']
        password = form.cleaned_data['password']
        user = authenticate(username=username, password=password)

        if user is not None:
            if user.is_active:
                login(self.request, user)
                return super(LoginView, self).form_valid(form)
        else:
            form.add_error(None, _("Podano niepoprawną nazwę użytkownika lub hasło."))
            return self.form_invalid(form)</pre>

I jeszcze zmieniony szablon

<pre>{% extends "layout.html" %}

{% block title %}
{% endblock %}

{% block content %}
{% if response is not None %}

<div class="alert alert-danger">{{response}}</div>

{% endif %}

{% load bootstrap %}
<form method="POST" action="/" class="form-signin">
    {% csrf_token %}

<h2 class="form-signin-heading">Wejście do listy zadań</h2>


    {{ form | bootstrap }}
    

<div class="form-group">
        <button type="submit" class="btn btn-primary">Zaloguj się</button>
        <a class="btn btn-default" href="/register">Załóż konto</a>
</div>

</form>
{% endblock %}</pre>

Po zmianach sprawdziłem, czy projekt działa. Skoro zadziałał, to wrzucę screenshota po refactorze.

screenshot-localhost 8000-2017-05-08-00-52-54.png

Do następnego wpisu!

Daj Się Poznać 2017, Python, TaskList

Czas na requirements.txt

Jak zasugerowała mi w komentarzu do wpisu Testy na Windows 10 Paulina Kaczmarek (tu znajdziecie jej blog), spróbuję zastosować plik requirements.txt do zapisywania i instalowania zależności do uruchomienia projektu.

Co będzie potrzebne do poprawnego działania projektu? Przypomnijmy sobie, co umieściłem w readme.md:

  • Python 3.4 +
  • Django 1.8 +
  • django-bootstrap
  • django-bootstrap-form
  • django-datetime-widget
  • SQLite 3+ lub jakakolwiek zgodna z Django baza danych

Przełóżmy to na listę pakietów Pythona, które zawrzemy w requirements.txt.

Jakie pakiety umieścimy? Na pewno django, django-bootstrap-form i django-datetime-widget.

Więc zawartość requirements.txt jest taka:

django>=1.8
django-bootstrap-form
django-datetime-widget

Podczas tworzenia tego pliku w IntelliJ IDEA (ja używam Community Edition z wtyczką do Pythona) dostaję podpowiedzi, czy pakiety, które zapisuję, są zainstalowane, jak poniżej:

Screenshot_20170507_222217.png

No i testujemy na wirtualnej maszynie z Fedorą.

Najpierw tworzymy izolowane środowisko z Pythonem poleceniem python3.5 -m venv requirements-test.

Potem uruchamiamy środowisko . requirements-test/bin/activate i wykonujemy polecenie pip3.5 install -r requirements.txt, dzięki czemu zainstalujemy sobie zależności dla projektu. Teraz możemy na próbę uruchomić.

VirtualBox_Fedora TaskList_07_05_2017_23_27_33.png

Jak widać, zadziałało.