Przejdźmy do najlepszej części tutoriala, czyli do momentu kiedy nasza aplikacja zaczyna działać!

Założeniem jest, aby aplikacja pobierała z GitHubJobs API, oferty pracy, następnie zapisywała je w pliku JSON, na koniec wyświetlała je na froncie.

Zacznijmy od stworzenia logiki aplikacji

W pliku scripts.py, będziemy definiować funkcje odpowiadające za wysyłanie zapytania do API, odbieranie danych i zapisywanie ich w pliku JSON.

Podstawowe query pobierające dane do API wygląda następująco:

https://jobs.github.com/positions.json?description=python&location=new+york

Nasze zapytanie zaczyna się po znaku „?”, określamy w nim description, czyli rodzaj technologii, oraz location, czyli lokalizację. Napiszemy funkcję, która obsłuży zapytanie, przyjmując jako swoje parametry, właśnie description oraz location

Na samym początku zainstalujmy moduły Pythona, które obsłużą naszą funkcję: moduł requests oraz moduł json, który jest standardową biblioteką Pythona. Aby to zrobić musimy ponownie uruchomić środowisko wirtualne:

env\Scripts\activate

pip install requests

deactivate

Teraz możemy spokojnie używać naszych modułów

models.py

import requests
import json

Zdefiniujmy funkcję, odpowiedzialną za pobieranie danych.

Funkcja będzie przyjmować 2 parametry, odpowiadające wcześniejszemu description oraz location, dzięki czemu po wywołaniu funkcji i przekazaniu parametrów, zostaną one użyte w ciele funkcji i obsłużą zapytanie.

scripts.py

def getResponseFromApi(description, location):

W funkcji umieśćmy globalną zmienną response_result oraz metodę requests, która obsłuży zapytanie do serwera GitHub Jobs. Definiujemy zmienną basic_url, która przechowa podstawowe query, oraz params_to_pass, w której znajdują się zapytania o w/w description i location.

Na sam koniec zmienna response_result, która posiada metodę request.get, z parametrami basic_url, oraz params_to_pass

scripts.py

# Get response from API
def getResponseFromApi(description, location):

	global response_result

	basic_url = 'https://jobs.github.com/positions.json'
	params_to_pass = {
		'description' : description,
		'location' : location
	}

	response_result = requests.get(basic_url, params = params_to_pass)
	return response_result

Metoda request.get(‚query’), wysyła zapytanie do serwera, czekając na jego odpowiedź. Jeśli statusem odpowiedzi będzie kod 200, znaczy że serwer zezwala na połączenie i dostęp do pobrania danych. Więcej o kodach dostępu możecie przeczytać na:
https://requests.kennethreitz.org/en/master/user/quickstart/?fbclid=IwAR0laGDTgb13QK9idyTodREVee6fZnbFXBk58JqtR7n07s892BFhGwiazUU#passing-parameters-in-urls

Kolejno napiszmy 2 dodatkowe funkcje: saveToJson() oraz readFormJson(), które, jak sama nazwa wskazuje obsłużą zapisywanie danych do JSON oraz ich odczytywanie.

scripts.py

# Save response data to JSON file
def saveToJson(description, location):
	getResponseFromApi(description, location)
	with open('jobs.json', 'w', encoding="utf-8") as file:
		file.write(response_result.text)
# Read data from JSON file
def readFromJson():
	with open('jobs.json', encoding="utf-8") as jobs_file:
		load_jobs = json.load(jobs_file)
		return load_jobs

Funkcja saveToJson() w swoim ciele wywoła funkcję getResponseFromApi(), otworzy plik „jobs.json”, w którym zapiszemy dane, jednak z flagą „w”, przez co plik otworzy się w trybie zapisu (jeśli plik nie istnieje, Python go stworzy). Dodajmy także kodowanie, aby móc spokojnie z niego korzystać. Cały proces przypisany jest do zmiennej file. Wewnątrz instrukcji do zmiennej file wpisujemy odpowiedź z serwera w formie tekstowej, czyli response_result.text

Ostatnia tutaj funkcja readFromJson(), otwiera plik JSON dzięki czemu odczytamy dane i przekażemy je później na front. Działa bardzo podobnie co funkcja wyżej, jednak tutaj nie dodajemy flagi, aby plik po prostu się otworzył.

Finalnie funkcje w pliku scripts.py prezentuje się następująco:

scripts.py

import requests
import json

# Get response from API
def getResponseFromApi(description, location):

	global response_result

	basic_url = 'https://jobs.github.com/positions.json'
	params_to_pass = {
		'description' : description,
		'location' : location
	}

	response_result = requests.get(basic_url, params = params_to_pass)
	return response_result

# Save response data to JSON file
def saveToJson(description, location):
	getResponseFromApi(description, location)
	with open('jobs.json', 'w', encoding="utf-8") as file:
		file.write(response_result.text)

# Read data from JSON file
def readFromJson():
	with open('jobs.json', encoding="utf-8") as jobs_file:
		load_jobs = json.load(jobs_file)
		return load_jobs

Tak oto powstanie nasz plik jobs.json, z którego w pliku core aplikacji wczytamy dane i wyświetlimy na froncie.

Przejdźmy do zdefiniowania core naszej aplikacji, czyli pliku main.py

Będziemy operować w pliku main.py, gdzie znajdzie się cała logika, czyli routing, obsługa formularza oraz wywołanie naszych funkcji scripts.py

Zaimportujmy podstawowe moduły, niezbędne do obsługi naszej aplikacji:

main.py

# imports
from flask import Flask, render_template, request
from scripts import getResponseFromApi, saveToJson, readFromJson

Z modułu flask, poza klasą Flask z poprzedniego odcinka, dodajemy render_template oraz request. Pierwszy odpowiada za system templatek stron, a drugi za obsługę zapytania do API.
Z modułu scripts (czyli scripts.py) importujemy funkcje.

Idźmy więc dalej!

Zacznijmy od zaimportowania reszty niezbędnych modułów Flask, Pythona oraz instalacji WTForms, a więc w pliku main.py dodajemy:

main.py

# imports
from flask import Flask, render_template, request
from wtforms import Form, TextField, validators, StringField, SubmitField
from scripts import getResponseFromApi, saveToJson, readFromJson

W przypadku, kiedy chcemy skorzystać w formularza, konieczna jest instalacja dodatkowego modułu, czyli WTForms. Niestety mimo wielu standardowych bibliotek Pythona, które są bardzo potężne, obsługę formularzy trzeba instalować oddzielnie.

Pamiętacie jak instalowaliśmy Flask oraz moduły requests i json?

Zróbmy to samo i doinstalujmy WTForms. W tym celu:

env\Scripts\activate

pip install Flask-WTF

deactivate

Gotowe nasz moduł zainstalowany, wystarczy zaimportować, co zrobiliśmy już wyżej 🙂

No dobrze, zacznijmy pobierać dane od użytkownika.

Zdefinujmy klasę SearchForm, która stworzy pola formularza:

main.py

# Fomrularz
class SearchForm(Form):
	# create the fiels
	technology = TextField('Technologia:',  validators=[validators.required()])
	place = TextField('Lokalizacja:')

Klasa SearchForm, dziedziczy po modułowej klasie Form. Stwórzmy 2 pola formularza: technology oraz place, mają reprezentować technologię oraz miejsce. validators.required() to meotda która powoduje, że to pole jest obowiązkowe. W naszym przypadku jest to tylko jedno pole z technologią, nie ograniczajmy się do miejsca, przecież można pracować zdalnie 🙂

Aby wyrenderować formularz na stornie głównej, w funkcji za to odpowiedzialnej dodajmy zmienną form i przypisaną do niej, wcześniej stworzoną klasą SearchForm i parametrem pobierającym dane z pól, czyli request.form. Return render_template, przyjmuje parametry: nazwa templatki, oraz zmienna (która będzie reprezentowała formularz na froncie), kolejno ‚index.html’, oraz form=form

main.py

@app.route('/')
def index():
	form = SearchForm(request.form)
	return render_template('index.html', form=form)

Pozostaje napisanie funkcji dla podstrony w wynikami

Stronę z wynikami nazwiemy results, tworzymy funkcję search_results(), jako część routingu pod stroną główną index():

Dekorator @app.route, przyjmuje 2 parametry. URL, czyli adres_aplikacji/results, oraz metody obsługi formularza GET lub POST

main.py

#results
@app.route('/results', methods=('GET', 'POST'))

Funkcja search_results() definiuje sposób w jaki będziemy pobierać dane z inputów, oraz pobierać dane z pliku jobs.json

main.py

#results
@app.route('/results', methods=('GET', 'POST'))
def search_resluts():

	if request.method == 'POST':
		#Get data from forms input
		description = request.form['technology']
		location = request.form['place']
		#Pass data to function and get the request
		saveToJson(description, location)

	#read JSON file
	all_jobs = readFromJson()

	return render_template('results.html', jobs = all_jobs, description = description , location = location)

Instrukcja warunkowa if, określa, że jeśli metoda pobierania danych z formularza to POST (a tak jest), wykonać ma się następujący kod:

#Get data from forms input
description = request.form['technology']
location = request.form['place']
#Pass data to function and get the request
saveToJson(description, location)

Zmienna description przechowuje dane z pola technology formularza, location z pola place, a funckja saveToJson() zapisuje nasze dane z API do pliku JSON

Ostatnim elementem jest wywołanie funkcji readFromJson(), do zmiennej all_jobs, którą będziemy formatować na froncie.

#read JSON file
all_jobs = readFromJson()

Na sam koniec renderujemy wszystko jak w funkcji search_resluts(), jednak dodamy kilka zmiennych, które wykorzystamy na froncie:

return render_template('results.html', jobs = all_jobs, description = description , location = location)

Pierwszy paramter to oczywiście templatka HTML, jobs to zmienna która przypisuje wszystkie dane z JSON, będziemy mogli je sformatować w templatce results.html, zmienna description i location, posłuży do powiadomienia użytkownika w jakich lokacjach i czego szuka.

Cała funkcja wygląda następująco:

#results
@app.route('/results', methods=('GET', 'POST'))
def search_resluts():

	if request.method == 'POST':
		#Get data from forms input
		description = request.form['technology']
		location = request.form['place']
		#Pass data to function and get the request
		saveToJson(description, location)

	#read JSON file
	all_jobs = readFromJson()

	return render_template('results.html', jobs = all_jobs, description = description , location = location)

A cały plik main.py tak:

main.py

# imports
from flask import Flask, render_template, request
from wtforms import Form, TextField, validators, StringField, SubmitField
from scripts import getResponseFromApi, saveToJson, readFromJson

app = Flask(__name__)

# home
class SearchForm(Form):
	# create the fiels
	technology = TextField('Technologia:',  validators=[validators.required()])
	place = TextField('Lokalizacja:')

@app.route('/')
def index():
	form = SearchForm(request.form)
	return render_template('index.html', form=form)

#results
@app.route('/results', methods=('GET', 'POST'))
def search_resluts():

	if request.method == 'POST':
		#Get data from forms input
		description = request.form['technology']
		location = request.form['place']
		#Pass data to function and get the request
		saveToJson(description, location)

	#read JSON file
	all_jobs = readFromJson()

	return render_template('results.html', jobs = all_jobs, description = description , location = location)

if __name__=='__main__':
	app.run(debug=True)

Uwaga! Finał jest bliski!

Pozostaje nam teraz wyświetlić wszystkie dane w templatkach HTML i ładnie sformatować

W folderze templates tworzymy 3 pliki:

  1. base.html – będzie naszą podstawą pod resztę plików HTML
  2. index.html – strona główna z wyszukiwarką
  3. results.html – strona wyników wyszukiwania

Templatki Jinja2 pozwalają na stworzenie podstawowego szablonu, w tym przypadku base.html, gdzie napiszemy header, znów kolejne templatki będą rozszerzać base.html o kolejne moduły.

Tak w templatce base.html dodajmy standardowy markup HTML, razem z Bootstrapem 4

base.html

<!DOCTYPE html>
<html lang="pl">

<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<meta http-equiv="X-UA-Compatible" content="ie=edge">
	<title>GitHub Jobs API</title>

	<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
		integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
	<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
		integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous">
	</script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
		integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous">
	</script>
	<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
		integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous">
	</script>

	<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,700,800&amp;display=swap&amp;subset=latin-ext"
		rel="stylesheet">

	<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">

	<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" />

</head>

<body>
	<div class="container-fluid">
		{% block content %}

		{% endblock %}
	</div>
</body>

</html>

Nie będę się tutaj rozpisywał na temat HTML, jak również dalszego CSS i Bootstrap4, bo nie o tym jest artykuł, jeśli nie znacie tych technologii, zwyczajnie skopiujcie je do swoich plików, lub zasięgnijcie wiedzy w sieci, to nic trudnego, ale zakładam że większość z Was wie z czym się to je 🙂

Skupimy się tylko na dwóch elementach:

<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block content %}

{% endblock %}

Pierwszy z nich określa katalog oraz nazwę pliku CSS we Flask, drugi znów określa w którym miejscu ma pojawić się dodatkowy moduł templatki Jinja. nazwa block, mówi nam że w tym miejscu blok się rozpocznie, content stawowi jego nazwę.

index.html

{% extends "base.html" %}
{% block content %}

	<div class="site-title text-center my-5">
		<h1>GitHub Jobs</h1>
		<h2>Znajdź pracę na portalu GitHub</h2>
		<p>Wszystkie ogłoszenia w jednym miejscu!</p>
	</div>
	<form class="search-form text-center" action="/results" method="POST">
		<div class="input text">
			{{ form.technology.label }} {{ form.technology }}
			{{ form.place.label }} {{ form.place }}
		</div>
		<div class="input submit my-5">
			<input type="submit" value="Wyszukaj"></div>
	</form>

{% endblock %}

W powyższej templatce index.html, w pierwszej linijce znajduje się informacja że jest ona rozszerzeniem base.html i zaczyna się blok content

Następnie jest standardowy formularz, ze zmiennej form (którą udostępniliśmy w render_template, funkcji index(), jako form=form)

Ostatnią templatką jest results.html:

{% extends "base.html" %}
{% block content %}

	<div class="site-title text-center my-5">
		<h1>GitHub Jobs</h1>
		<h2>Znajdź pracę na portalu GitHub</h2>
		<p>Wszystkie ogłoszenia w jednym miejscu!</p>
	</div>
	<div class="searching-for text-center my-5">
		<h3>Szukasz ogłoszeń dla: {{description.title()}}
			{% if loc %}
			, w lokalizacji: {{location.title()}}
			{% endif %}
		</h3>
	</div>
	{% if jobs |length == 0 %}
	<div class="not-found text-center">
		<h3>Przykro nam, nie znaleziono ofert w tej lokalizacji. Spróbuj ponownie</h3>
		<a href="/">Powrót</a>
	</div>

	{% else %}
	<div class="row no-gutters d-flex justify-content-center flex-wrap">
		{% for job in jobs %}
		<div class="job-container col-lg-3 col-12 px-5 py-5 mx-4 my-4">
			<div class="company">
				<a target="_blank" href="{{job.company_url}}">
					{% if job.company_logo %}
					<img src="{{job.company_logo}}" alt="Company Logo">
					{% else %}
					<span>Brak logo</span>
					{% endif %}
				</a>
				<p>{{job.company}}</p>
			</div>
			<div class="job-title">{{job.title}}</div>
			<div class="job-type my-3">{{job.type}}</div>
			<a class="job-link" target="_blank" href="{{job.url}}">Aplikuj</a>
		</div>
		{% endfor %}
	</div>
	{% endif %}

{% endblock %}

W divie z klasą searchng-for znajduje się zmienna, z funkcji search_results(), czyli description z metodą title() wyświetlającą pierwszą literę jako dużą, oraz instrukcja warunkowa, która obsługuje dalszy rendering, jeśli użytkownik poda również lokalizację.

Kolejna instrukcja warunkowa if, bada czy zmienna jobs ma więcej niż 0 znaków, czyli czy wpisano coś w pole input formularza, co spowodowało że JSON nie jest pusty.

Jeśli nie jest pusty, możemy sformatować plik i wyświetlić ogłoszenia. Dzięki pętli for przechodzimy przez wszystkie rekordy JSON. Zmienna job pętli pozwala wejść nieco głębiej w strukturę pliku i wyświetlić pojedyncze parametry po „.”, jak np.: {{ job.company_logo }}. Parametr company_logo, to ten sam, który znajdziecie w pliku JSON.

Dodajmy style w folderze static, pod nazwą style.css. Najlepiej jeśli je pobierzecie, lub skopiujecie:

:root {
	/* FONTS */
	--small: 16px;
	--normal: 22px;
	--huge: 30px;

	/* FONT WEIGHT */
	--light: 300;
	--bold: 700;
	--extra-bold: 800;

	/* COLORS */
	--basic: #6a67ce;
	--light-grey: #e6e6e6;
}


body {
	font-family: 'Open Sans', sans-serif;
}

a, a:hover {
	text-decoration: none;
}

input:focus, input:active {
	box-shadow: none;
	outline: none;
	border: none;
}

/* SITE */
.site-title h1 {
	font-size: var(--huge);
	font-weight: var(--extra-bold);
}

.site-title h2 {
	font-size: var(--normal);
	font-weight: var(--bold);
}

.site-title p {
	font-size: var(--small);
	font-weight: var(--light);
}

/* SEARCH */
.search-form .input label {
	font-weight: var(--bold);
	color: var(--basic);
}

.search-form .input input {
	padding: 5px;
	font-weight: var(--light);
	border: 1px solid var(--light-grey);
	border-radius: 5px;
}

.search-form .input.submit input {
	color: #fff;
	font-weight: var(--bold);
	background: var(--basic);
	padding: 10px 20px;
	transition: .3s all ease;
}

.search-form .input.submit input:hover {
	background: #fff;
	color: var(--basic);
	box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1);
}

.searching-for h3{
	font-size: var(--normal);
	font-weight: var(--bold);
	color: var(--basic);
}

/* JOB OFFER */
.job-container {
	background: #fff;
	margin: 10px;
	border: 1px solid var(--light-grey);
	border-radius: 5px;
	transition: .3s all ease;
}

.job-container:hover {
	box-shadow: 0 10px 10px rgba(0, 0, 0, 0.1);
}

.job-container .company img {
	width: 80px;
	height: auto;
}

.job-container .company a span{
	font-size: var(--small);
	color: #000;
	font-weight: var(--bold);
}

.job-container .job-title {
	font-size: var(--normal);
	font-weight: var(--extra-bold);
}

.job-container .job-type {
	font-size: var(--small);
	font-weight: var(--light);
}

.job-container a.job-link {
	display: inline-block;
	color: #000;
	border: 1px solid var(--light-grey);
	border-radius: 5px;
	padding: 10px 20px;
	margin-right: 10px;
	font-weight: var(--bold);
}

.job-container a.job-link:hover {
	color: #fff;
	background: var(--basic);
	transition: .3s all ease;
}

.not-found h3{
	font-weight: var(--extra-bold);
	font-size: var(--huge);
}

.not-found a {
	color: var(--basic);
	font-weight: var(--bold);
	font-size: var(--normal);
}

@media(max-width: 992px) {
    .job-container {
		margin-left: auto !important;
		margin-right: auto !important;
	}
}

I to by było na tyle!

W katalogu app odpalicie komendę python main.py i zobaczycie formularz, po wypełnieniu którego wyświetlą się wyniki na podstronie /result

Pięknie prawda?

W ostatniej III części tego tutoriala napiszemy proste testy jednostkowe, aby upewnić się, że nasza apka nie będzie zachowywała się nieprzewidywalnie, oraz zrobimy deploy na serwer live Herroku

Mam nadzieję, że sobie poradziliście! 🙂

Jeśli macie jakieś pytania, zapraszam do komentowania, jeśli artykuł Ci się spodobał zawsze możesz go udostępnić 🙂