Blog
En esta lección vamos a aprender la manera en subir archivos al servidor forma segura aunque existen varías maneras de hacerlo.
En primer lugar vamos a dirigirnos a nuestro archivo de formularios.py y agregamos dos campos nuevos al formulario de los posts, que está representado como la clase Publicaciones:
from wtforms import FileField class Publicaciones(FlaskForm): post = TextAreaField('Escribele algo al mundo', validators=[ DataRequired(), Length(min=1, max=140) ]) imagen = FileField('Imagen') submit = SubmitField('Postear')
Lo otro que debemos hacer, es modificar también una de nuestras clases modelos, la cual es el modelo de publicaciones que se representa por la clase Pubs, entonces vamos a agregar a dicho modelo un nuevo campo:
class Pubs(bdd.Model): id = bdd.Column(bdd.Integer, primary_key=True) cuerpo = bdd.Column(bdd.String(140)) timestamp = bdd.Column(bdd.DateTime, index=True, default=datetime.utcnow) post_imagen = bdd.Column(bdd.String(), nullable=True) id_usuario = bdd.Column(bdd.Integer, bdd.ForeignKey('usuario.id'))
El campo que hemos agregado es el campo donde se guardarán las imágenes que vayan con dicha publicación, además de eso le hemos colocado como argumentos nullable=True
este argumento cuando está en True lo que hace es decirle a la base de datos que el campo puede ser NULL (Nulo o vacío). Esto se hace con la finalidad de decirle que no se requiere que la publicación lleve imagen, ya que si no lo colocamos su origen por defecto es False lo cual hace que sea el campo obligatorio y prácticamente tenemos que obligar al usuario a que agregue una imagen para poder hacer una publicación, entonces gracias a este argumento le permitimos hacer un post con o sin imagen y así es como debería de ser.
Lo próximo por hacer es realizar la migración de la base de datos, ya que agregamos un nuevo campo a nuestra tabla Pubs, así que vamos a realizarlo con flask migrate:
flask db migrate -m "campo post_imagen"
Luego aplicamos los cambios:
flask db upgrade
La estructura de nuestra carpeta app debe ser la siguiente:
Al momento de subir los archivos al servidor debemos decirle donde se van a guardar a través de nuestro backend, pero por ahora vamos solamente a crear donde se van a guardar dichos archivos, dentro de la carpeta app crearemos una carpeta llamada static y dentro de esta otras dos carpetas una llamada uploads (app/static/uploads
) y otra llamada icons, esta carpeta uploads es donde estarán los archivos que se suban al servidor, la carpeta icons es para introducir nuestros iconos que hasta ahora usaremos 1 solo (pero eres libre de agregarle más a tu aplicación si lo deseas)
Una vez creada la estructura de la carpeta app debe ser la siguiente:
app:
│ enviar_email.py
│ errores.py
│ formularios.py
│ modelos.py
│ rutas.py
│ __init__.py
│
├───settings/
│
│
├───static
│ │
│ │
│ └───uploads
│ │
│ │
│ │
│ └───icons
│
│
├───templates/
Una vez hecho esto, debemos ahora habilitar dicho formulario en nuestra plantilla index.html, además de habilitar el formulario también vamos a agregar un par de clases de bootstrap y de estilos css para mejorar la presentación de nuestra plantilla index.html:
{% extends "base.html" %} {% block contenido %} <div class="container-fluid" style="background-color: #dedeee;"> <h1 class="pt-4">Hola, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post" enctype="multipart/form-data" novalidate> {{ form.hidden_tag() }} <div class="form-group mb-0"> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4, class="form-control") }} {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </div> <div class="form-group"> {{ form.imagen(class="form-control-file") }} </div> {{ form.submit(class="btn btn-primary btn-sm") }} </form> {% endif %} {% for post in posts %} <p> {% include '$post.html' %} <hr> </p> {% endfor %} <nav aria-label="..."> <ul class="pagination pagination-md"> {% if pagina_ant %} <li class="page-item"> <a class="page-link" href="{{ pagina_ant }}">Página anterior</a> </li> {% endif %} {% if pagina_sig %} <li class="page-item"> <a class="page-link" href="{{ pagina_sig }}">Página siguiente</a> {% endif %} </li> </ul> </nav> </div> {% endblock %}
Hasta ahora, hemos agregado los campos a la base de datos para que se guarde la imagen que se adjunte al post, hemos habilitado el input para adjuntar dicha imagen al post y también le agregamos un par de estilos CSS y otras clases de Bootstrap de las que ya poseía la plantilla index.html, es importante resaltar que los estilos CSS que estoy agregando acá no es recomendable agregarlos en la etiqueta con el atributo style, estos estilos deben ir en un archivo css por separados y deben ser enlazado en nuestro proyecto, yo aquí lo he hecho así porque quiero avanzar con el curso cuanto antes, sabiendo esto, si vamos a nuestra app hasta ahora nuestra ruta index debería de lucir así:
Ya tenemos cargada la interfaz en nuestro archivo html, ahora falta hacer el backend y para ello debemos ir a nuestro archivo de rutas.py y realizar las siguientes modificaciones en la ruta /index
y nuestro archivo debería de quedar de la siguiente manera:
from werkzeug.utils import secure_filename import os @app.route('/', methods=['GET', 'POST']) @app.route('/index', methods=['GET', 'POST']) @login_required def index(): form = Publicaciones() if form.validate_on_submit(): post = Pubs(cuerpo=form.post.data, autor=current_user) imagen = form.imagen.data nombre_imagen = secure_filename(current_user.username + '_' + imagen.filename) ruta_imagen = os.path.abspath('app\\static\\uploads\\{}'.format(nombre_imagen)) ruta_html = '../static/uploads/{}'.format(nombre_imagen) imagen.save(ruta_imagen) if imagen.filename != '': post.post_imagen = ruta_html else: pass bdd.session.add(post) bdd.session.commit() flash('Publicación enviada correctamente.') return redirect(url_for('index')) pagina = request.args.get('pagina', 1, type=int) posts = current_user.pubs_seguidores().paginate( pagina, app.config['POSTS_PER_PAGE'], False) pagina_sig = url_for('index', pagina=posts.next_num) \ if posts.has_next else None pagina_ant = url_for('index', pagina=posts.prev_num) \ if posts.has_prev else None return render_template('index.html', titulo='Página de inicio', form=form, posts=posts.items, pagina_sig=pagina_sig, pagina_ant=pagina_ant)
Lo nuevo que hemos agregado es guardar en una variable imagen
la imagen que se reciben del formulario, es decir la imagen que se anexa al post, además de eso debemos recibir la imagen de una manera segura y lo hacemos con la función que importamos anteriormente secure_filename()
esta función recibe un argumento tipo string el cual es el nombre de la imagen a recibir, lo que hemos hecho es concatenar el nombre del usuario logueado + “_” + “nombre de la imagen” y esto lo que hará además de guardar el archivo de forma segura en nuestro servidor, también le cambiará el nombre, le he dicho que le coloque el nombre de usuario al principio como método de identificar que usuario subío esa imagen. El nombre de la imagen lo obtenemos a través del método de clase propio de python .filename
es por ello que le agregamos este método de clase a la variable imagen
que es la que recibimos desde el formulario.
Para este caso he utilizado el mismo usuario con el que vengo trabajando a lo largo del curso “jose19”, entonces, al momento que el usuario suba una imagen que tenga el nombre “mi_amigo.jpg”, esa imagen será recibida por el servidor como “jose19_mi_amigo.jpg”.
Ya hemos programado la recepción de imágenes a nuestro servidor, ahora solo falta agregar un poquito de código JavaScript para estilizar más el botón de adjuntar la imagen al post. Entonces para esto vamos a dirigirnos a nuestra plantilla index.html y vamos a agregar 1 campo de html estático, pero antes descarga de aquí el icono que he usado para este campo html estático aunque también se puede usar font-awesome si tienes conocimientos de esta biblioteca de iconos, una vez descargado introduce dicho icono dentro de la carpeta icons que creamos anteriormente y procedemos a agregar lo siguiente en nuestro index.html:
<div class="form-group"> <input id="input-html" class="form-control-file d-inline mt-3" style="width: 30px;" type="image" src="../static/icons/picture.png"><p id="p-imagen" style="margin-left: 2.2rem; margin-top: -1.65rem;">Adjuntar imagen al post</p> </div>
Este código lo que hace es cargar ese icono en forma de input en nuestra plantilla, pero aún falta por hacer y es que debemos agregar al input que se genera desde nuestro formulario Flask una clase de Bootstrap llamada d-none
para que se oculte, entonces lo agregamos:
<div class="form-group d-none"> {{ form.imagen(class="form-control-file") }} </div>
Finalmente nuestro index.html debe verse así:
(no olvides cambiar el nombre de la imagen por picture.png)
{% extends "base.html" %} {% block contenido %} <div class="container-fluid" style="background-color: #dedeee;"> <h1 class="pt-4">Hola, {{ current_user.username }}!</h1> {% if form %} <form action="" method="post" enctype="multipart/form-data" novalidate> {{ form.hidden_tag() }} <div class="form-group mb-0"> {{ form.post.label }}<br> {{ form.post(cols=32, rows=4, class="form-control") }} {% for error in form.post.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} </div> <div class="form-group d-none"> {{ form.imagen(class="form-control-file") }} </div> <div class="form-group"> <input id="input-html" class="form-control-file d-inline mt-3" style="width: 30px;" type="image" src="../static/icons/picture.png"><p id="p-imagen" style="margin-left: 2.2rem; margin-top: -1.65rem;">Adjuntar imagen al post</p> </div> {{ form.submit(class="btn btn-primary btn-sm") }} </form> {% endif %} {% for post in posts %} <p> {% include '$post.html' %} <hr> </p> {% endfor %} <nav aria-label="..."> <ul class="pagination pagination-md"> {% if pagina_ant %} <li class="page-item"> <a class="page-link" href="{{ pagina_ant }}">Página anterior</a> </li> {% endif %} {% if pagina_sig %} <li class="page-item"> <a class="page-link" href="{{ pagina_sig }}">Página siguiente</a> {% endif %} </li> </ul> </nav> </div> {% endblock %}
Ahora, nuestra vista index con estos cambios que hemos hecho debe lucir así:
Por útlimo falta escribir un poco de código JavaScript utilizando la librería Jquery y para quienes tengan dudas, Jquery es el mismo lenguaje JavaScript solo que esta librería de JavaScript te permite escribir código JavaScript simplificado ya que las carencias de este lenguaje son las sintaxis cortas, como por ejemplo si necesito seleccionar un elemento html de mi web que tenga el id elemento1
y cuando el usuario haga click en dicho elemento me muestre un “Hola mundo” por consola:
En JavaScript puro luciría así:
document.getElementById('elemento1').addEventListener('click', function(){ console.log('Hola mundo') });
Mientras que en JavaScript con Jquery luciría así:
$('#elemento1').click(function(){ console.log('Hola mundo') })
Entonces si nos fijamos Jquery simplifica muchas cosas, y ambos códigos hacen lo mismo. Una vez explicado esto que es importante saberlo vamos a dirigirnos a nuestra plantilla base.html y agregaremos el siguiente código debajo del código JavaScript que escribimos anteriormente para el Moment.js:
/* Input para la imagen del post */ $(document).ready(function () { $('#input-html').click(function (event) { event.preventDefault(); $('#imagen').click() $('#imagen').change(function () { var $nombreArchivo = $('#imagen').val() var $nombreSplit = $nombreArchivo.split('\\') $('#p-imagen').text($nombreSplit[2]) }) }) });
Este código lo que hace en un inicio es decirle que cuando el documento este cargado es decir este “listo” lo cual lo hacemos con $(document).ready()
, le decimos que ejecute una función, dicha función selecciona el elemento que tiene el id input-html
este id lo posee el elemento input que muestra la imagen y le dice que luego de selecionarlo cuando ocurra un click en dicho elemento se ejecute otra función, esta función recibe un evento, este evento es el evento que viene por defecto en el input, una vez se comienza a ejecutar la función le decimos que cancele dicho evento por defecto con la instrucción event.preventDefault()
.
Acto seguido le decimos que seleccione el elemento que posee el id imagen
y le haga un click automático ya que esto hará que hagamos click en el icono que adjuntamos, pero en realidad estaremos haciendo click en el elemento input del tipo file que viene desde Flask , luego se vuelve a seleccionar el mismo elemento con el id imagen
pero con un evento diferente esta vez el cual es el evento .change()
este evento lo que hace es verificar si ocurre un cambio en elemento y cuando ocurra ese cambio ejecute otra función, pero antes de continuar debo explicarte el cambio que ocurre.
Inicialmente antes de adjuntar un archivo el elemento tiene su atributo value
vacío y cuando se carga la imagen que el usuario quiera agregar al post este value
cambia, entonces al ocurrir ese cambio se ejecutará la función que guarda en una variable $nombreArchivo
el value del elemento seleccionado que básicamente es la ruta de la imagen, pero una ruta formateada por fakepath por cuestiones de seguridad, luego en una variable $nombreSplit
hacemos uso de la función de JavaScript .split()
para quitar las barras de la ruta fake y luego seleccionar el elemento p-imagen
este elemento es la etiqueta parrafo que contiene el texto “Adjuntar imagen al post” luego le decimos que le agregue al texto de ese elemento lo que contiene la variable $nombreSplit
pero en su indice [2]
el cual es el nombre de la imagen que se esta adjuntando al post, lo que hará esto es remplazar el texto “Adjuntar imagen al post” por el nombre de la imagen que se esta anexando al post.
Así se vería nuestra interfaz cuando Carguemos una imágen al post:
Por útlimo, falta decirle a la subplantilla $post.html que muestre dicha imagen si es que la posee el post, para ello hacemos que luzca de esta manera la subplantilla $post.html:
<table class="table table-hover"> <tr> <td width="70px"> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> <img class="rounded-circle" src="{{ post.autor.imagen_perfil(70) }}" /> </a> </td> <td> <a href="{{ url_for('perfil_usuario', username=post.autor.username) }}"> {{ post.autor.username }} </a> dijo {{ moment(post.timestamp).fromNow() }}: <br> <p class="mt-2">{{ post.cuerpo }}</p> <img class="img-fluid mt-1" style="width: 300px; border-radius: 1rem;" src="{% if post.post_imagen %} {{ post.post_imagen }} {% else %} {% endif %}" alt=""> </td> </tr> </table>
Listo, con esto hemos habilitado que todos los usuarios registrados en nuestra app puedan compartir las imágenes que quieran, por otro lado te anexo otra captura de como se verían las publicaciones con un usuario que siga con la cuenta “jose19”:
➡ Continúa aprendiendo con nuestro Curso de Flask – Python: