Update REAME with first part
This commit is contained in:
parent
ca45afa8bc
commit
049cd26e40
394
README.md
394
README.md
@ -33,8 +33,402 @@ python3 app.py
|
|||||||
|
|
||||||
## Workshop (Taller)
|
## Workshop (Taller)
|
||||||
|
|
||||||
|
### Minimum requirements (Requisitos mínimos)
|
||||||
|
|
||||||
|
* python3
|
||||||
|
* wget
|
||||||
|
* Editor de texto enriquecido: Vim, Sublime Text, VSCode, Atom...
|
||||||
|
* sqlite3
|
||||||
|
* Conexión a internet
|
||||||
|
|
||||||
|
### We checked everything (Comprobamos que tenemos todo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 --help
|
||||||
|
wget --help
|
||||||
|
sqlite3 --help
|
||||||
|
ping -c 5 google.com
|
||||||
|
```
|
||||||
|
|
||||||
### Part 1 - Flask Core y Search (Parte 1 - Nucleo de Flask y Buscador) 50 min
|
### Part 1 - Flask Core y Search (Parte 1 - Nucleo de Flask y Buscador) 50 min
|
||||||
|
|
||||||
|
#### 1.1
|
||||||
|
|
||||||
|
[EN] We prepare our virtual environment.
|
||||||
|
|
||||||
|
[ES] Preparamos nuestro entorno virtual.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir flask-wallapop-watcher
|
||||||
|
cd flask-wallapop-watcher
|
||||||
|
pip3 install virtualenv
|
||||||
|
virtualenv --python=python3 .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
wget https://raw.githubusercontent.com/tanrax/flask-wallapop-watcher/master/requirements.txt
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.1 Hello PyConES17
|
||||||
|
|
||||||
|
[EN] Template Flask. We created a new file called **app.py**.
|
||||||
|
|
||||||
|
[ES] Plantilla Flask. Creamos un nuevo archivo llamado **app.py**.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask import Flask
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'mi secreto'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def buscador():
|
||||||
|
return 'Hello PyConES17 !!!'
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] We run and check that everything works.
|
||||||
|
|
||||||
|
[ES] Ejecutamos y comprobamos que todo funciona.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http://127.0.0.1:5000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.2 Templates
|
||||||
|
|
||||||
|
[EN] We created a folder called **templates**. Inside we make two more folders: **layouts** and **items**. In **layouts** we will make a new one with the name **master.html**.
|
||||||
|
|
||||||
|
[ES] Creamos una carpeta llamada **templates**. Dentro dos más: **layouts** y **items**. En **layouts** haremos uno nuevo con el nombre **master.html**.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %} | Vigilador de Wallapop</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://bootswatch.com/superhero/bootstrap.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<ul class="nav nav-pills nav-justified">
|
||||||
|
<li role="presentation" {% if active_page == "buscador" or not active_page %}class="active"{% endif %}><a href="">Buscador</a></li>
|
||||||
|
<li role="presentation" {% if active_page == "programadas" %}class="active"{% endif %}><a href="#">Programadas</a></li>
|
||||||
|
</ul>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] In **items** we are going to have our first real page that will inherit from **master.html**. Within **items** we create **searcher.html**.
|
||||||
|
|
||||||
|
[ES] En **items** vamos a tener nuestra primera página real que va a heredar de **master.html**. Dentro de **items** creamos **buscador.html**.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
{% extends 'layouts/master.html' %}
|
||||||
|
{% set active_page = "buscador" %}
|
||||||
|
{% block title %}Buscador{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Buscador</h1>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] You update **app.py** to work with our template engine.
|
||||||
|
|
||||||
|
[ES] Actulizamos **app.py** para que trabaje nuestro motor de plantillas.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'mi secreto'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def buscador():
|
||||||
|
return render_template('items/buscador.html')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] We create the second page where we will have our searches stored. Within **items** we create a new file with the name of **programadas.html**.
|
||||||
|
|
||||||
|
[ES] Creamos la segunda página donde tendremos nuestras busquedas almacenadas. Dentro de **items** creamos un fichero nuevo con el nombre de **programadas.html**.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
{% extends 'layouts/master.html' %}
|
||||||
|
{% set active_page = "programadas" %}
|
||||||
|
{% block title %}Programadas{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Soy la página donde estará las programadas</h1>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] We update **app.py** with the new page.
|
||||||
|
|
||||||
|
[ES] Actulizamos **app.py** con la nueva página.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask import Flask, render_template
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'mi secreto'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def buscador():
|
||||||
|
return render_template('items/buscador.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/programadas')
|
||||||
|
def programadas():
|
||||||
|
return render_template('items/programadas.html')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] As a final detail we will make our browser buttons have the correct routes.
|
||||||
|
|
||||||
|
[ES] Como último detalle haremos que nuestros botones del navegador tengan las rutas correctas.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}{% endblock %} | Vigilador de Wallapop</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="https://bootswatch.com/superhero/bootstrap.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<ul class="nav nav-pills nav-justified">
|
||||||
|
<li role="presentation" {% if active_page == "buscador" or not active_page %}class="active"{% endif %}><a href="{{ url_for('buscador') }}">Buscador</a></li>
|
||||||
|
<li role="presentation" {% if active_page == "programadas" %}class="active"{% endif %}><a href="{{ url_for('programadas') }}">Programadas</a></li>
|
||||||
|
</ul>
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.3 Forms
|
||||||
|
|
||||||
|
[EN] We make the new file **forms.py**.
|
||||||
|
|
||||||
|
[ES] Realizamos el nuevo archivo **forms.py**.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import StringField, IntegerField
|
||||||
|
from wtforms.validators import DataRequired, Length, NumberRange, Optional
|
||||||
|
|
||||||
|
|
||||||
|
class SearchForm(FlaskForm):
|
||||||
|
name = StringField('Nombre', [Length(min=1, max=100, message='Es demasiado largo'), DataRequired(message='Campo obligatorio')])
|
||||||
|
price_max = IntegerField('Precio', [NumberRange(1, message='No puede ser inferior a 1'), Optional()])
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] We load it and pass it to the template.
|
||||||
|
[ES] Lo cargamos y se lo pasamos a la plantilla.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from forms import SearchForm
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'mi secreto'
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def buscador():
|
||||||
|
form = SearchForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
pass
|
||||||
|
return render_template('items/buscador.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/programadas')
|
||||||
|
def programadas():
|
||||||
|
return render_template('items/programadas.html')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] We print the fields with a **loop** in our template **buscador.html**.
|
||||||
|
|
||||||
|
[ES] Imprimimos los campos con un **bucle** en nuestra plantilla **buscador.html**.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
{% extends 'layouts/master.html' %}
|
||||||
|
{% set active_page = "buscador" %}
|
||||||
|
{% block title %}Buscador{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Buscador</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<form method="post">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% for input in form %}
|
||||||
|
{% if input.type != 'CSRFTokenField' %}
|
||||||
|
<div class="form-group">
|
||||||
|
{# Label #}
|
||||||
|
{{ input.label }}
|
||||||
|
{# Input #}
|
||||||
|
{{ input(class="form-control") }}
|
||||||
|
{# Errors #}
|
||||||
|
{% if input.errors %}
|
||||||
|
<div class="has-error">
|
||||||
|
{% for error in input.errors %}
|
||||||
|
<label class="help-block">
|
||||||
|
{{ error }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" class="btn btn-primary" value="Buscar">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 1.4 Search
|
||||||
|
|
||||||
|
[EN] It's time for fun. First we update our **app.py** to get the form data if you pass the validations. Then, with that information, we will make a call to the Wallapop API. We will only need the URL that they use in your APP. With **urllib3** we will have all the results in a simple dictionary. Which is great, since it is easy to iterate within our template.
|
||||||
|
|
||||||
|
[ES] Ha llegado la hora de lo divertido. Primero actulizamos nuestro **app.py** para obtener los datos del formulario si pasa las validaciones. Después, con esa información, haremos una llamada al API de Wallapop. Solo necesitaremos la URL que utilizan en su APP. Con *urllib3* tendremos todos los resultados en un sencillo diccionario. Lo cual es magnífico, ya que es fácil de iterar dentro de nuestra plantilla.
|
||||||
|
|
||||||
|
```python3
|
||||||
|
from flask import Flask, render_template, request
|
||||||
|
from forms import SearchForm
|
||||||
|
# Get data Wallapop
|
||||||
|
import json
|
||||||
|
from urllib3 import PoolManager
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
app = Flask(__name__)
|
||||||
|
app.config['DEBUG'] = True
|
||||||
|
app.config['SECRET_KEY'] = 'mi secreto'
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def buscador():
|
||||||
|
form = SearchForm()
|
||||||
|
results = None
|
||||||
|
if form.validate_on_submit():
|
||||||
|
name = form.name.data
|
||||||
|
price_max = form.price_max.data or ''
|
||||||
|
# Search in Wallapop
|
||||||
|
results = get_resultados(name, price_max)
|
||||||
|
return render_template('items/buscador.html', form=form, results=results)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/programadas')
|
||||||
|
def programadas():
|
||||||
|
return render_template('items/programadas.html')
|
||||||
|
|
||||||
|
|
||||||
|
def get_resultados(name='', price_max=''):
|
||||||
|
http = PoolManager()
|
||||||
|
url_api = 'http://es.wallapop.com/rest/items?minPrice=&maxPrice={price_max}&dist=&order=creationDate-des&lat=41.398077&lng=2.170432&kws={kws}'.format(
|
||||||
|
kws=urllib.parse.quote(name, safe=''),
|
||||||
|
price_max=price_max
|
||||||
|
)
|
||||||
|
results = http.request('GET', url_api)
|
||||||
|
results = json.loads(
|
||||||
|
results.data.decode('utf-8')
|
||||||
|
)
|
||||||
|
return results['items']
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] And in our template of **buscador.html**.
|
||||||
|
|
||||||
|
[ES] Y en nuestra plantilla de **buscador.html**.
|
||||||
|
|
||||||
|
```jinja2
|
||||||
|
{% extends 'layouts/master.html' %}
|
||||||
|
{% set active_page = "buscador" %}
|
||||||
|
{% block title %}Buscador{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<h1>Buscador</h1>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<form method="post">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
{% for input in form %}
|
||||||
|
{% if input.type != 'CSRFTokenField' %}
|
||||||
|
<div class="form-group">
|
||||||
|
{# Label #}
|
||||||
|
{{ input.label }}
|
||||||
|
{# Input #}
|
||||||
|
{{ input(class="form-control") }}
|
||||||
|
{# Errors #}
|
||||||
|
{% if input.errors %}
|
||||||
|
<div class="has-error">
|
||||||
|
{% for error in input.errors %}
|
||||||
|
<label class="help-block">
|
||||||
|
{{ error }}
|
||||||
|
</label>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" class="btn btn-primary" value="Buscar">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if results %}
|
||||||
|
<table class="table">
|
||||||
|
{% for item in results %}
|
||||||
|
<tr>
|
||||||
|
<td><img class="img-responsive" src="{{ item.pictureURL }}" alt="{{ item.title }}"></td>
|
||||||
|
<td>{{ item.title }}</td>
|
||||||
|
<td>{{ item.price }}</td>
|
||||||
|
<td><a href="#" class="btn btn-success">+</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
[EN] And ... *Voilá*.
|
||||||
|
|
||||||
|
[ES] Y... *Voilá*.
|
||||||
|
|
||||||
|
|
||||||
### Break (Descanso) - 10 min
|
### Break (Descanso) - 10 min
|
||||||
|
|
||||||
[EN] We debug bugs and prepare for the next point.
|
[EN] We debug bugs and prepare for the next point.
|
||||||
|
18
app.py
18
app.py
@ -17,8 +17,17 @@ def buscador():
|
|||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
name = form.name.data
|
name = form.name.data
|
||||||
price_max = form.price_max.data or ''
|
price_max = form.price_max.data or ''
|
||||||
|
|
||||||
# Search in Wallapop
|
# Search in Wallapop
|
||||||
|
results = get_resultados(name, price_max)
|
||||||
|
return render_template('items/buscador.html', form=form, results=results)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/programadas')
|
||||||
|
def programadas():
|
||||||
|
return render_template('items/programadas.html')
|
||||||
|
|
||||||
|
|
||||||
|
def get_resultados(name='', price_max=''):
|
||||||
http = PoolManager()
|
http = PoolManager()
|
||||||
url_api = 'http://es.wallapop.com/rest/items?minPrice=&maxPrice={price_max}&dist=&order=creationDate-des&lat=41.398077&lng=2.170432&kws={kws}'.format(
|
url_api = 'http://es.wallapop.com/rest/items?minPrice=&maxPrice={price_max}&dist=&order=creationDate-des&lat=41.398077&lng=2.170432&kws={kws}'.format(
|
||||||
kws=urllib.parse.quote(name, safe=''),
|
kws=urllib.parse.quote(name, safe=''),
|
||||||
@ -28,12 +37,7 @@ def buscador():
|
|||||||
results = json.loads(
|
results = json.loads(
|
||||||
results.data.decode('utf-8')
|
results.data.decode('utf-8')
|
||||||
)
|
)
|
||||||
results = results['items']
|
return results['items']
|
||||||
return render_template('items/buscador.html', form=form, results=results)
|
|
||||||
|
|
||||||
@app.route('/programadas')
|
|
||||||
def programadas():
|
|
||||||
return render_template('items/programadas.html')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
<title>{% block title %}{% endblock %} | Vigilador de Wallapop</title>
|
<title>{% block title %}{% endblock %} | Vigilador de Wallapop</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="https://bootswatch.com/cerulean/bootstrap.css">
|
<link rel="stylesheet" href="https://bootswatch.com/superhero/bootstrap.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="nav nav-pills nav-justified">
|
<ul class="nav nav-pills nav-justified">
|
||||||
<li role="presentation" {% if active_page == "buscador" %}class="active"{% endif %}><a href="{{ url_for('buscador') }}">Buscador</a></li>
|
<li role="presentation" {% if active_page == "buscador" or not active_page %}class="active"{% endif %}><a href="{{ url_for('buscador') }}">Buscador</a></li>
|
||||||
<li role="presentation" {% if active_page == "programadas" %}class="active"{% endif %}><a href="{{ url_for('programadas') }}">Programadas</a></li>
|
<li role="presentation" {% if active_page == "programadas" %}class="active"{% endif %}><a href="{{ url_for('programadas') }}">Programadas</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user