Registro de usuarios en Flask con Mysql

Registro de usuarios en flaskEn el post anterior habíamos conectado nuestra base de datos Mysql usando SQLAlchemy, ahora es momento de crear nuestro modelo para elaborar un registro de usuarios en Flask.

Ya hemos hablado antes del modelo y la importancia que tiene como «puente» entre nuestro controlador y la base de datos según la arquitectura MVC (Modelo – Vista – Controlador).

Por lo que es necesario crear un «modelo» para hacer esta interacción, pero antes de elaborar el modelo debemos comprender a fondo que es y porque al momento de crear el modelo estaremos diagramando una base de datos.

 

¿Qué es el modelo en la arquitectura MVC de Flask?

Recordemos que habíamos dicho que el modelo era algo así como una plantilla en base a la cual se crean objetos, como también esta definición podría aplicar perfectamente para las clases en la programación orientada a objetos. Pues sí, el modelo es una clase en la que se definen atributos y métodos. Y al momento de ser creada una base de datos estos atributos pasan a conformar columnas de la tabla donde se respetan ciertos requisitos especificados en la clase respecto del tipo de dato a almacenar y otras características clave. Así como los métodos de la clase modelo define como se trabajará con estos datos y otras acciones necesarias para el intercambio con la base de datos de nuestra app.

En el modelo definimos como se llevará a cabo la interacción entre nuestro controlador y la base de datos así como también los requisitos de validación y lógica del manejo de los datos.

He hablado del modelo en el post de MVC que si quieres puedes leer para reforzar aquí:

El modelo - La lógica de datos
Fíjate en el siguiente ejemplo:

Ejemplo de registro de usuarios en flask - Modelo

Esto sería un modelo para la tabla usuarios (fíjate que en el definimos los atributos de cada «Column» como el tipo de dato y si se le permite ser «null»). Al ejecutar nuestra app por primera vez se creará una tabla en la base de datos existente a partir de esta clase.

Ideando nuestro modelo y las relaciones (entidad – relación)

entidad relaciónEn nuestro caso antes de crear esta clase y comenzar a añadir los atributos debemos tener en claro que campos necesitaremos, además claro de los clásicos de cualquier registro de usuario como su Nombre, Email, etc. Porque recordemos que en nuestra aplicación de películas puede haber distintos rangos de usuarios y también debemos mostrar que películas han visto y en el caso de que su rango sea «Uploader» debería mostrar las películas que ha subido.

Y claro habíamos dicho también que las películas serían almacenadas en una base de datos no relacional en formato JSON. ¿Cuál es la lógica de nuestros datos?. ¿Cómo podríamos diagramar nuestra relación entre las tablas y con ella la base de datos JSON?

Y este es el momento del embrollo. Existen relaciones y debemos organizarlas para no tener problemas en el futuro a la hora de añadir registros a ambas bases de datos.

Recordarás en base de datos hemos hablado cuando explique «claves foraneas» que podemos realizar relaciones simplemente colocando dentro de la misma tabla un id que haga referencia a otra o bien podemos crear tablas aparte para las relaciones.

Embrollo relacionalPues en este caso podríamos colocar dentro de la tabla Usuarios un atributo que haga referencia al rango; por ejemplo todos los usuarios se crearán con rango «0» (cero) por defecto, pero aquellos que sean uploader’s o moderator tengan en este atributo «1» o «2» correspondiente.

Y también podemos añadir otros atributos que sean una característica común para todos los usuarios. Sin embargo; hay otras características que no son comunes a todos los usuarios sino que varían según su rango. Por ejemplo ¿Qué películas ha subido determinado usuario?.

Podríamos colocar este atributo a todos los usuarios con un «default» en el caso de que su rango no sea uploader, y trabajar directamente estableciendo una relación solo con el número de «id» de cada película. ¿Pero que pasaría cuando un usuario suba 200 películas?. Significa que en determinado campo de la tabla usuarios debería almacenar 200 id’s distintas (claves) y eso sería algo problemático y desorganizado.

Por ende creo que será mejor crear también la tabla «Movies» donde se almacenará el id de la película, el usuario que la subió y otros datos como la fecha y demás. Entonces cuando queremos saber que películas subió determinado usuario solo tendremos que filtrar los registros dentro de la tabla Movies.

Y esta tabla Movies según el «id» o llave se relacionará directamente con una base de datos JSON donde se almacenarán otros datos más informativos de las mismas como su «Sinopsis», «enlace», etc.

Para comprenderlo mejor realizamos un diagrama (cutre, humilde, pero muy explicativo):

entidad-relación

Las tablas Usuarios y Movies serán tablas de nuestra base de datos relacional (mysql) y en ella alojaremos los datos importantes de nuestros usuarios. Pero debemos resaltar que la tabla «Movies» alojará mayormente relaciones, es decir claves foráneas :

«Las llamadas llaves foráneas son una limitación referencial entre dos tablas, esta identifica una columna o grupo de columna (atributos) en una tabla (hija o referendo)  que se refiere en otra tabla (maestra o referenciada). Las columnas en la tabla hija o referendo deben ser la clave primaria u otra candidata en la tabla referenciada

Por lo que como puedes ver la tabla «Movies» guardará los datos de acciones que han realizado los usuarios según el «id» o clave principal en cada película. Si te fijas por ejemplo el usuario 2 «Juan» ha subido la película cuyo id es 547 «Fight Club» y también ha visto «Titanic» ya que en la columna «action» hacemos referencias con el numero 0 (cero) a que ha visto, con el «1» que ha subido y con el «2» que ha modificado o eliminado una película.

Es importante tener en cuenta que cada «acción» que se realice incrementará aún más nuestra tabla «Movies«. Claro, eso no es problema en un principio, pero si habláramos de un sitio web enorme con muchas visitas sería muy útil el tema de las fechas para eliminar registros antiguos. Por ejemplo, podemos borrar todas las views (action = 0) de los usuarios haciendo un filtro y solo guardar las de la última semana. También podríamos limpiar cada cierto tiempo el registro de películas eliminadas o modificadas por moderadores (action = 2).

 

 

Crear el modelo «models.py» para el registro de usuarios en flask

Para crear el modelo deberemos ayudarnos con ciertas librerías, por ejemplo para las contraseñas. No vas a guardar las contraseñas textualmente como las escriben en la base de datos ¿verdad?. Así que vamos a usar dos librerías en nuestro modelo: Flask-login y Werkzeug.

Para ello primero vamos a instalarlas en nuestro entorno virtual:

python -m pip install Flask-login, Werkzeug

La librería Flask-Login nos proporciona entre otras cosas la clase UserMixin que nos brindará un modelo base para cumplir con ciertos requerimientos referentes al tema de la sesión de los usuarios. Y Werkzeug lo podemos utilizar para definir y almacenar las contraseñas cifradas, además de ayudarnos en el tema de la autenticación.

 

Instanciar la DB desde el Modelo e importarla en el Controlador

Una vez instaladas ambas librerías vamos a crear el archivo «models.py» junto a nuestro controlador principal e ingresamos lo siguiente:

Declarar la DB en el Modelo
Deberás importar SQLAlchemy en el modelo y crear una instancia para «db» y luego importar esa instancia a run.py. De lo contrario es posible que tengas problemas.. Fíjate en el código, debe quedarte igual:

  • Archivo models.py:
    from flask_sqlalchemy import SQLAlchemy #Importamos SQLAlchemy 
    db = SQLAlchemy() #Creamos la instancia db

     

  • Archivo run.py:
    #Import
    from flask import Flask, render_template
    import config #importamos el archivo de configuración "config.py"
    from models import db #Importamos la DB
    
    #Instancia +config 
    app = Flask(__name__)
    app.config.from_object(config)
    db.init_app(app)
    #Rutas
    @app.route('/')
    def inicio():
        return ('Hola esta es la página de inicio')
    
    #Run
    if __name__ == '__main__':
        app.run(debug=True)

    Como ves luego de la instancia «app» y la configuración añadimos la sentencia «db.init_app(app)» para iniciar la conexión con la base de datos mediante el método init_app() de la instancia «db» de SQLAlchemy que en este caso ahora proviene del modelo.

 

La clase Usuarios en nuestro modelo y sus atributos

Vamos a proceder a crear la clase usuario y más adelante explicaré en detalle:

from flask_login import UserMixin #Importamos la clase UserMixin
from werkzeug.security import generate_password_hash, check_password_hash #Password
from datetime import datetime #Importamos "datetime" para las fechas.

from flask_sqlalchemy import SQLAlchemy #Importamos SQLAlchemy
db = SQLAlchemy() #Creamos la instancia db

class Usuarios(db.Model, UserMixin):
    __tablename__="Usuarios"
    id = db.Column(db.Integer, primary_key = True)
    Nombre = db.Column(db.String(45), nullable=False)
    Apellido = db.Column(db.String(45), nullable = False)
    Edad = db.Column(db.Integer, nullable = False, default = 0)
    Genero = db.Column(db.String(45), nullable = True, default = ('None'))
    Correo = db.Column(db.String(45), nullable = False)
    Pais = db.Column(db.String(45), nullable = True, default = ('None'))
    Password = db.Column(db.String(250), nullable = False)
    Rango = db.Column(db.Integer, nullable = False, default = 0)

 

Como puedes ver creamos la clase «Usuarios» donde especificamos que hereda de db.Model (Modelo de SQLAlchemy) y de UserMixin (clase de Flask-Login) que en este caso aún no utilizamos todas las características de esta clase.

Seguido damos el nombre que tendrá la tabla con __tablename__ al momento de ser creada. Luego definimos para cada atributo de la tabla una columna donde brindamos como argumentos los requisitos. Por ejemplo:

  • En la columna «id» indicamos que se trata del tipo de dato entero y es una Clave Primaria. Por lo que almacenará solo numeros enteros y cada uno será una referencia a determinado registro.
  • Luego añadimos columnas como «Nombre» y «Apellido» que especificamos almacenarán datos de tipo String (texto) solo permitiendo hasta 45 caracteres y lo más importante NO PUEDEN SER «NULL», es decir, el usuario no puede dejar estos datos en blanco. Deben si o si almacenar un nombre y apellido.
  • Luego tenemos «Edad» que también almacenará datos enteros y asignamos un «default» que en caso de que el usuario no lo completara permitiríamos el valor «0» cero. Pero no el valor «null», por ello «null = False». Es decir, tampoco quedará nunca en blanco, siempre se asignará un valor, así sea cero.
  • Seguimos con el «Genero» que permite valor de tipo String (texto), permite valor Null y también asignamos un default a «None». Esto lo he realizado así adrede para luego explicar algo más adelante.
  • Luego el «Correo» o «Email» de tipo String hasta 45 caracteres (puedes añadir más) y obviamente no podrá quedar jamás en «Null».
  • «País» igual que el genero, puede ser null pero definimos un valor por default del tipo «None». En este caso en el formulario de registro tendrás una lista de paises donde el usuario podrá elegir su nacionalidad y entonces añadiremos textualmente este valor.
  • En el caso del «Password» será una columna del tipo String pero permitiendo hasta 250 caracteres ya que el password se guardara «encriptado» y por ende necesita ocupar mucho más que 45 caracteres. Obviamente es un atributo que nunca puede ser «null».
  • Finalmente tenemos el «Rango» y fíjate que esta columna solo permitirá números enteros, no permitirá el valor «null» y por defecto será «0» cero. Que es el rango que asignaremos a los «viewers» digamos aquellos que solo verán películas y no tendrán ningún otro permiso.
Consistencia
La tabla "usuarios" se creará a partir de este modelo, por lo que todos los requerimientos deben estar especificados en el modelo. NO puedes permitirte modificar ningún requerimiento de estos luego dentro de la tabla en mysql usando PhpMyAdmin. Si quieres modificar un requerimiento debes hacerlo en el modelo y volver a crear la base de datos en base a este. De lo contrario habrá inconsistencias entre el modelo de tu aplicación y las tablas de la base de datos, lo que en algún momento podría provocar un error.

La clase Usuarios en nuestro modelo y sus métodos

Ahora bien hasta ahí tenemos nuestro modelo de usuarios, se creará la tabla en determinado momento. Pero cuando un usuario se registre completando correctamente estos campos en un formulario ¿a qué método vamos a llamar para crear el registro en la base de datos?. Para esto debemos crear una sesión de SQLAlchemy y crear el método "guardar" o "save()" para que ese usuario se añada a la base de datos, pero además de ello debemos también crear un método que se encargue de cifrar y comprobar el password. Así que añadiendo estos métodos nuestra clase usuarios quedará así:

from flask_login import UserMixin #Importamos la clase UserMixin
from werkzeug.security import generate_password_hash, check_password_hash #Password
from datetime import datetime #Importamos "datetime" para las fechas.

from flask_sqlalchemy import SQLAlchemy #Importamos SQLAlchemy
db = SQLAlchemy() #Creamos la instancia db

class Usuarios(db.Model, UserMixin):
    __tablename__="Usuarios"
    id = db.Column(db.Integer, primary_key = True)
    Nombre = db.Column(db.String(45), nullable=False)
    Apellido = db.Column(db.String(45), nullable = False)
    Edad = db.Column(db.Integer, nullable = False, default = 0)
    Genero = db.Column(db.String(45), nullable = True, default = ('None'))
    Correo = db.Column(db.String(45), nullable = False)
    Pais = db.Column(db.String(45), nullable = True, default = ('None'))
    Password = db.Column(db.String(250), nullable = False)
    Rango = db.Column(db.Integer, nullable = False, default = 0)
    
    #Referencia del usuario - Mostrar el correo.
    def __repr__(self):
        return f'<Usuario {self.Correo}>'
    #Generar un Hash del password
    def set_Password(self, Password):
        self.Password = generate_password_hash(Password)
    #Comprobar que el hash del password coincida
    def check_password(self, password):
        return check_password_hash(self.Password, password)
    #Guardar en la base de datos
    def save(self):
        if not self.id:
            db.session.add(self)
        db.session.commit()

 

He añadido un comentario antes de cada método explicando su función. Pero el más importante y que más me gustaría explicar es el último; el cual comprueba que dicho valor "id" no exista aún para finalmente añadir los cambios a una session de SQLAlchemy y finalmente hacer "commit", es decir, aplicar los cambios en la base de datos. Si quieres saber más acerca de estos métodos sería bueno leas la documentación de las librerías que utilizamos, sobre todo de SQLAlchemy.

 

La clase Movies en nuestro modelo

Ahora para crear la clase "Movies" que será el modelo de nuestra tabla "Movies" donde almacenaremos la relación entre usuarios, peliculas y acciones es un poco más sencillo y menos engorroso, ya que solo almacenará valores enteros y no permitirá ningún valor nulo ("null"). Excepto en el caso de la fecha que sería una entrada del tipo "Date", que si, venga es algo un poquito más complejo.

Para asignar la fecha y hora actual de cada nuevo registro añadido en la base de datos "Movies" importamos el módulo "datetime" y finalmente creamos el tipo de dato "date" que obtenga como valor "default" la fecha y hora actual.

Esta clase solo heredará del modelo SQLAlchemy. Nuestro models.py quedaría así:

from flask_login import UserMixin #Importamos la clase UserMixin
from werkzeug.security import generate_password_hash, check_password_hash #Password
from datetime import datetime #Importamos "datetime" para las fechas.

from flask_sqlalchemy import SQLAlchemy #Importamos SQLAlchemy
db = SQLAlchemy() #Creamos la instancia db

class Usuarios(db.Model, UserMixin):
    __tablename__="Usuarios"
    id = db.Column(db.Integer, primary_key = True)
    Nombre = db.Column(db.String(45), nullable=False)
    Apellido = db.Column(db.String(45), nullable = False)
    Edad = db.Column(db.Integer, nullable = False, default = 0)
    Genero = db.Column(db.String(45), nullable = True, default = ('None'))
    Correo = db.Column(db.String(45), nullable = False)
    Pais = db.Column(db.String(45), nullable = True, default = ('None'))
    Password = db.Column(db.String(250), nullable = False)
    Rango = db.Column(db.Integer, nullable = False, default = 0)
    
    #Referencia del usuario - Mostrar el correo.
    def __repr__(self):
        return f'<Usuario {self.Correo}>'
    #Generar un Hash del password
    def set_Password(self, Password):
        self.Password = generate_password_hash(Password)
    #Comprobar que el hash del password coincida
    def check_password(self, password):
        return check_password_hash(self.Password, password)
    #Guardar en la base de datos
    def save(self):
        if not self.id:
            db.session.add(self)
        db.session.commit()


class Movies(db.Model):
    __tablename__="Movies"
    id = db.Column(db.Integer, primary_key = True)
    id_user = db.Column(db.Integer, nullable = False)
    id_movie = db.Column(db.Integer, nullable = False)
    action = db.Column(db.Integer, nullable = True)
    date = db.Column(db.DateTime, nullable = False, default=datetime.utcnow)
    def __init__(self,id_user, id_movie, action):
        self.id_user = id_user
        self.id_movie = id_movie
        self.action = action
        self.date = datetime.utcnow()
    def save(self):
        if not self.id:
            db.session.add(self)
        db.session.commit()

 

En la clase "Movies" al igual que con "Usuarios" definimos los atributos del  tipo entero y asignamos a la columna "id" como llave primaria, pero esta será obviamente una llave para ubicar cada relación. Finalmente tenemos "id_user" y "id_movie" que serán las claves foráneas que representan la relación entre usuario y película. Ademas añadimos el atributo "action" que almacenará la acción que se realizó en numero entero como había explicado antes. Y finalmente añadimos otro atributo del formato "date" que nos permitirá almacenar la fecha en que se crea cada registro siguiendo que tiene como "default" la fecha y hora actual pasada por el módulo "datetime".

Luego el mismo método save() para añadir a la session de SQLAlchemy y guardar los cambios en la base de datos.

Fíjate que la clase "Movies" tiene un método __init__ que nos exigirá pasar estos 3 parámetros para poder crear un registro, el único que puede excluirse es la fecha!. Lo cual no es así en la clase "Usuarios" ya que en esta los registros se crearán mediante un formulario y en el controlador se realiza la instancia mediante la cual luego se llamará al método save().

Bieeen hasta aquí todo correcto!. Ahora.. Debemos crear un formulario de registro!!

Crear un formulario de registro con WTForms

Flask-WTForms es una librería que se integra a flask facilitándonos la creación de formularios otorgándonos beneficios como la comprobación de validación de distintos campos así como también asegurando nuestro formulario mediante la integración del token CSRF y Google ReCaptcha entre otras cosas. Ya hemos hablado antes de la creación de formularios con esta librería en "Crear un formulario de contacto en flask" donde te mostré dos maneras de crear formularios, la tradicional HTML y usando esta librería. También instalaremos email-validator para la validación de los emails en el registro.

Lo primero será instalarlas en el entorno virtual de nuestra aplicación:

python -m pip install flask-wtf email-validator

Lo siguiente será crear el archivo forms.py dentro de nuestro directorio principal del proyecto e importar aquellas clases de WTForms que vamos a usar. Estas clases se corresponden a nuestro formulario de registro de usuario por lo que si recordamos en el modelo establecimos ciertos requerimientos para los atributos de la base de datos. Estas clases son básicamente campos con parámetros que establecen validaciones o requisitos, como por ejemplo StringField que corresponde a campo de texto nos permitirá establecer una validación si se trata de un campo obligatorio a completar (requerido) y el límite de caracteres que el usuario podrá rellenar.

Podemos crear un formulario así:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, IntegerField, BooleanField, SelectMultipleField, RadioField
from wtforms.validators import DataRequired, Email, Length

#Formulario de registro
class Registro(FlaskForm):
    Nombre = StringField('Nombre', validators=[DataRequired(), Length(max=45)])
    Apellido = StringField('Apellido', validators=[DataRequired(), Length(max=45)])
    Edad = IntegerField('¿Qué edad tienes?', validators=[DataRequired()])
    Genero = SelectMultipleField('Selecciona genero con el que te identificas:', choices=[('Masculino', 'Masculino'), ('Femenino', 'Femenino')], validators=[DataRequired(), Length(max=45)])
    Correo = StringField('Correo', validators=[DataRequired(), Email()])
    Pais = SelectMultipleField('Selecciona país de recidencia:', choices=Paises, validators=[DataRequired(), Length(max=45)])
    Password = PasswordField('Contraseña', validators=[DataRequired()])
    submit = SubmitField('Registro')

 

  1. Fíjate que primero importamos las clases de WTForms que vamos a usar y que ya expliqué en el post antes mencionado cada una.
  2. Creamos la clase "Registro" que hereda de FlaskForm lo que especifica que se trata de un formulario.
  3. Ingresamos los atributos según lo requerido en nuestro modelo para la clase "Usuarios" y definimos en cada clase las restricciones, en la mayoría de los casos si se trata de un campo obligatorio usamos "DataRequired" y definimos la cantidad máxima de caracteres que coincide con las definidas en nuestro modelo.
  4. Presta atención a que no incluimos en el formulario el campo "id" ni  "Rango". Ya que de estos campos el primero será asignado automáticamente por el modelo y el segundo tomará el valor por default "0" (cero) como lo especificamos en el modelo.
  5. Verás en el campo de selección de "País" o nacionalidad que he dado como argumento en "Choises" un diccionario llamado "Países". Este es un diccionario que contiene todos los países para que al momento del registro se le permita seleccionar uno de una lista desplegable como lo declara la clase "SelectMultipleField". Esta lista la puedes guardar en un archivo aparte o bien colocarlo dentro del forms.py. En mi caso lo guardaré dentro de la carpeta static que aún no hemos creado y lo importaré al formulario quedando así:

Archivo "Lista_de_paises.py" en la carpeta "static":

Paises = [
    ('Afganistan', 'Afghanistan'),
    ('Albania', 'Albania'),
    ('Algeria', 'Algeria'),
    ('American Samoa', 'American Samoa'),
    ('Andorra', 'Andorra'),
    ('Angola', 'Angola'),
    ('Anguilla', 'Anguilla'),
    ('Antigua & Barbuda', 'Antigua & Barbuda'),
    ('Argentina', 'Argentina'),
    ('Armenia', 'Armenia'),
    ('Aruba', 'Aruba'),
    ('Australia', 'Australia'),
    ('Austria', 'Austria'),
    ('Azerbaijan', 'Azerbaijan'),
    ('Bahamas', 'Bahamas'),
    ('Bahrain', 'Bahrain'),
    ('Bangladesh', 'Bangladesh'),
    ('Barbados', 'Barbados'),
    ('Belarus', 'Belarus'),
    ('Belgium', 'Belgium'),
    ('Belize', 'Belize'),
    ('Benin', 'Benin'),
    ('Bermuda', 'Bermuda'),
    ('Bhutan', 'Bhutan'),
    ('Bolivia', 'Bolivia'),
    ('Bonaire', 'Bonaire'),
    ('Bosnia & Herzegovina', 'Bosnia & Herzegovina'),
    ('Botswana', 'Botswana'),
    ('Brazil', 'Brazil'),
    ('British Indian Ocean Ter', 'British Indian Ocean Ter'),
    ('Brunei', 'Brunei'),
    ('Bulgaria', 'Bulgaria'),
    ('Burkina Faso', 'Burkina Faso'),
    ('Burundi', 'Burundi'),
    ('Cambodia', 'Cambodia'),
    ('Cameroon', 'Cameroon'),
    ('Canada', 'Canada'),
    ('Canary Islands', 'Canary Islands'),
    ('Cape Verde', 'Cape Verde'),
    ('Cayman Islands', 'Cayman Islands'),
    ('Central African Republic', 'Central African Republic'),
    ('Chad', 'Chad'),
    ('Channel Islands', 'Channel Islands'),
    ('Chile', 'Chile'),
    ('China', 'China'),
    ('Christmas Island', 'Christmas Island'),
    ('Cocos Island', 'Cocos Island'),
    ('Colombia', 'Colombia'),
    ('Comoros', 'Comoros'),
    ('Congo', 'Congo'),
    ('Cook Islands', 'Cook Islands'),
    ('Costa Rica', 'Costa Rica'),
    ('Cote DIvoire', 'Cote DIvoire'),
    ('Croatia', 'Croatia'),
    ('Cuba', 'Cuba'),
    ('Curaco', 'Curacao'),
    ('Cyprus', 'Cyprus'),
    ('Czech Republic', 'Czech Republic'),
    ('Denmark', 'Denmark'),
    ('Djibouti', 'Djibouti'),
    ('Dominica', 'Dominica'),
    ('Dominican Republic', 'Dominican Republic'),
    ('East Timor', 'East Timor'),
    ('Ecuador', 'Ecuador'),
    ('Egypt', 'Egypt'),
    ('El Salvador', 'El Salvador'),
    ('Equatorial Guinea', 'Equatorial Guinea'),
    ('Eritrea', 'Eritrea'),
    ('Estonia', 'Estonia'),
    ('Ethiopia', 'Ethiopia'),
    ('Falkland Islands', 'Falkland Islands'),
    ('Faroe Islands', 'Faroe Islands'),
    ('Fiji', 'Fiji'),
    ('Finland', 'Finland'),
    ('France', 'France'),
    ('French Guiana', 'French Guiana'),
    ('French Polynesia', 'French Polynesia'),
    ('French Southern Ter', 'French Southern Ter'),
    ('Gabon', 'Gabon'),
    ('Gambia', 'Gambia'),
    ('Georgia', 'Georgia'),
    ('Germany', 'Germany'),
    ('Ghana', 'Ghana'),
    ('Gibraltar', 'Gibraltar'),
    ('Great Britain', 'Great Britain'),
    ('Greece', 'Greece'),
    ('Greenland', 'Greenland'),
    ('Grenada', 'Grenada'),
    ('Guadeloupe', 'Guadeloupe'),
    ('Guam', 'Guam'),
    ('Guatemala', 'Guatemala'),
    ('Guinea', 'Guinea'),
    ('Guyana', 'Guyana'),
    ('Haiti', 'Haiti'),
    ('Hawaii', 'Hawaii'),
    ('Honduras', 'Honduras'),
    ('Hong Kong', 'Hong Kong'),
    ('Hungary', 'Hungary'),
    ('Iceland', 'Iceland'),
    ('Indonesia', 'Indonesia'),
    ('India', 'India'),
    ('Iran', 'Iran'),
    ('Iraq', 'Iraq'),
    ('Ireland', 'Ireland'),
    ('Isle of Man', 'Isle of Man'),
    ('Israel', 'Israel'),
    ('Italy', 'Italy'),
    ('Jamaica', 'Jamaica'),
    ('Japan', 'Japan'),
    ('Jordan', 'Jordan'),
    ('Kazakhstan', 'Kazakhstan'),
    ('Kenya', 'Kenya'),
    ('Kiribati', 'Kiribati'),
    ('Korea North', 'Korea North'),
    ('Korea Sout', 'Korea South'),
    ('Kuwait', 'Kuwait'),
    ('Kyrgyzstan', 'Kyrgyzstan'),
    ('Laos', 'Laos'),
    ('Latvia', 'Latvia'),
    ('Lebanon', 'Lebanon'),
    ('Lesotho', 'Lesotho'),
    ('Liberia', 'Liberia'),
    ('Libya', 'Libya'),
    ('Liechtenstein', 'Liechtenstein'),
    ('Lithuania', 'Lithuania'),
    ('Luxembourg', 'Luxembourg'),
    ('Macau', 'Macau'),
    ('Macedonia', 'Macedonia'),
    ('Madagascar', 'Madagascar'),
    ('Malaysia', 'Malaysia'),
    ('Malawi', 'Malawi'),
    ('Maldives', 'Maldives'),
    ('Mali', 'Mali'),
    ('Malta', 'Malta'),
    ('Marshall Islands', 'Marshall Islands'),
    ('Martinique', 'Martinique'),
    ('Mauritania', 'Mauritania'),
    ('Mauritius', 'Mauritius'),
    ('Mayotte', 'Mayotte'),
    ('Mexico', 'Mexico'),
    ('Midway Islands', 'Midway Islands'),
    ('Moldova', 'Moldova'),
    ('Monaco', 'Monaco'),
    ('Mongolia', 'Mongolia'),
    ('Montserrat', 'Montserrat'),
    ('Morocco', 'Morocco'),
    ('Mozambique', 'Mozambique'),
    ('Myanmar', 'Myanmar'),
    ('Nambia', 'Nambia'),
    ('Nauru', 'Nauru'),
    ('Nepal', 'Nepal'),
    ('Netherland Antilles', 'Netherland Antilles'),
    ('Netherlands', 'Netherlands (Holland, Europe)'),
    ('Nevis', 'Nevis'),
    ('New Caledonia', 'New Caledonia'),
    ('New Zealand', 'New Zealand'),
    ('Nicaragua', 'Nicaragua'),
    ('Niger', 'Niger'),
    ('Nigeria', 'Nigeria'),
    ('Niue', 'Niue'),
    ('Norfolk Island', 'Norfolk Island'),
    ('Norway', 'Norway'),
    ('Oman', 'Oman'),
    ('Pakistan', 'Pakistan'),
    ('Palau Island', 'Palau Island'),
    ('Palestine', 'Palestine'),
    ('Panama', 'Panama'),
    ('Papua New Guinea', 'Papua New Guinea'),
    ('Paraguay', 'Paraguay'),
    ('Peru', 'Peru'),
    ('Phillipines', 'Philippines'),
    ('Pitcairn Island', 'Pitcairn Island'),
    ('Poland', 'Poland'),
    ('Portugal', 'Portugal'),
    ('Puerto Rico', 'Puerto Rico'),
    ('Qatar', 'Qatar'),
    ('Republic of Montenegro', 'Republic of Montenegro'),
    ('Republic of Serbia', 'Republic of Serbia'),
    ('Reunion', 'Reunion'),
    ('Romania', 'Romania'),
    ('Russia', 'Russia'),
    ('Rwanda', 'Rwanda'),
    ('St Barthelemy', 'St Barthelemy'),
    ('St Eustatius', 'St Eustatius'),
    ('St Helena', 'St Helena'),
    ('St Kitts-Nevis', 'St Kitts-Nevis'),
    ('St Lucia', 'St Lucia'),
    ('St Maarten', 'St Maarten'),
    ('St Pierre & Miquelon', 'St Pierre & Miquelon'),
    ('St Vincent & Grenadines', 'St Vincent & Grenadines'),
    ('Saipan', 'Saipan'),
    ('Samoa', 'Samoa'),
    ('Samoa American', 'Samoa American'),
    ('San Marino', 'San Marino'),
    ('Sao Tome & Principe', 'Sao Tome & Principe'),
    ('Saudi Arabia', 'Saudi Arabia'),
    ('Senegal', 'Senegal'),
    ('Seychelles', 'Seychelles'),
    ('Sierra Leone', 'Sierra Leone'),
    ('Singapore', 'Singapore'),
    ('Slovakia', 'Slovakia'),
    ('Slovenia', 'Slovenia'),
    ('Solomon Islands', 'Solomon Islands'),
    ('Somalia', 'Somalia'),
    ('South Africa', 'South Africa'),
    ('Spain', 'Spain'),
    ('Sri Lanka', 'Sri Lanka'),
    ('Sudan', 'Sudan'),
    ('Suriname', 'Suriname'),
    ('Swaziland', 'Swaziland'),
    ('Sweden', 'Sweden'),
    ('Switzerland', 'Switzerland'),
    ('Syria', 'Syria'),
    ('Tahiti', 'Tahiti'),
    ('Taiwan', 'Taiwan'),
    ('Tajikistan', 'Tajikistan'),
    ('Tanzania', 'Tanzania'),
    ('Thailand', 'Thailand'),
    ('Togo', 'Togo'),
    ('Tokelau', 'Tokelau'),
    ('Tonga', 'Tonga'),
    ('Trinidad & Tobago', 'Trinidad & Tobago'),
    ('Tunisia', 'Tunisia'),
    ('Turkey', 'Turkey'),
    ('Turkmenistan', 'Turkmenistan'),
    ('Turks & Caicos Is', 'Turks & Caicos Is'),
    ('Tuvalu', 'Tuvalu'),
    ('Uganda', 'Uganda'),
    ('United Kingdom', 'United Kingdom'),
    ('Ukraine', 'Ukraine'),
    ('United Arab Erimates', 'United Arab Emirates'),
    ('United States of America', 'United States of America'),
    ('Uraguay', 'Uruguay'),
    ('Uzbekistan', 'Uzbekistan'),
    ('Vanuatu', 'Vanuatu'),
    ('Vatican City State', 'Vatican City State'),
    ('Venezuela', 'Venezuela'),
    ('Vietnam', 'Vietnam'),
    ('Virgin Islands (Brit)', 'Virgin Islands (Brit)'),
    ('Virgin Islands (USA)', 'Virgin Islands (USA)'),
    ('Wake Island', 'Wake Island'),
    ('Wallis & Futana Is', 'Wallis & Futana Is'),
    ('Yemen', 'Yemen'),
    ('Zaire', 'Zaire'),
    ('Zambia', 'Zambia'),
    ('Zimbabwe', 'Zimbabwe')]

 

Y luego la importamos a nuestro formulario quedando así:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, IntegerField, BooleanField, SelectMultipleField, RadioField
from wtforms.validators import DataRequired, Email, Length
from static.Lista_de_paises import Paises

#Formulario de registro
class Registro(FlaskForm):
    Nombre = StringField('Nombre', validators=[DataRequired(), Length(max=45)])
    Apellido = StringField('Apellido', validators=[DataRequired(), Length(max=45)])
    Edad = IntegerField('¿Qué edad tienes?', validators=[DataRequired()])
    Genero = SelectMultipleField('Selecciona genero con el que te identificas:', choices=[('Masculino', 'Masculino'), ('Femenino', 'Femenino')], validators=[DataRequired(), Length(max=45)])
    Correo = StringField('Correo', validators=[DataRequired(), Email()])
    Pais = SelectMultipleField('Selecciona país de recidencia:', choices=Paises, validators=[DataRequired(), Length(max=45)])
    Password = PasswordField('Contraseña', validators=[DataRequired()])
    submit = SubmitField('Registro')

Fíjate que la importamos de esta manera ya que si la importaramos como un módulo normal "from static import Lista_de_Paises.py" nos saldrá un error de tipo "Module is not callable". Por lo que solo debemos importar la lista  dentro del mismo.

Ahora sería momento de probar nuestro formulario, para ello debemos desarrollar el controlador que se encargará de procesar este formulario y también debemos definir la ruta y la vista que tendrá.

Esto es algo sencillo, venga!!

Procesar los datos del formulario en el controlador

Ahora cuando nuestro usuario rellene este formulario correctamente se procesarán los datos y se creará un nuevo usuario en la base de datos, pero para ello debemos programar una serie de solicitudes, requisitos y condicionales a comprobar en el controlador (run.py). Por ejemplo cuando nuestro usuario ingresa por primera vez a ver el formulario de registro realiza una solicitud GET a través del navegador, algo que también ya explique antes aquí: Procesar solicitudes GET y POST...

Una vez rellenado los campos al dar el botón "submit" estará enviando una solicitud POST, es decir, pretende enviarnos datos. Por lo que nosotros además de procesar esto en el controlador mediante un condicional también debemos comprobar que ese usuario no existe aún, o que no tiene una segunda cuenta entre otros miles de requisitos que se te puedan ocurrir!. Pero vamos a sostenernos en lo más básico por ahora, ya luego añadiremos otras funcionalidades a nuestro "registro de usuarios en flask".

  1. Debemos importar nuestro formulario al controlador run.py "from forms import Registro"
  2. En nuestro controlador debemos definir la ruta donde se mostrará el formulario.
  3. Debemos indicar cual es el formulario a utilizar.
  4. Y debemos crear un condicional en caso que los datos del formulario sean correctos para crear un nuevo usuario, o en caso de ser incorrectos indicar algún error. (POST)
  5. Si el usuario que visita el sitio del formulario pero no ha completado nada solo debemos retornar el template (GET)

Esto también lo he explicado antes en el enlace que dejé anteriormente, nos quedaría en un principio algo así:

#Import
from flask import Flask
import config #importamos el archivo de configuración "config.py"
from forms import Registro #Importamos el formulario de registro
from models import db #Importamos la DB

#Instancia +config 
app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)

#Rutas
@app.route('/')
def inicio():
    return ('Hola esta es la página de inicio')

@app.route('/registro', methods = ['GET','POST'])
def registro():
    form = Registro()
    return()

Teniendo ahora la ruta registro solo nos quedaría añadir el condicional en caso de que se cumplan los requisitos del formulario para procesarlos dentro y el return para retornar el template con el formulario en caso de solicitud GET:

#Import
from flask import Flask, render_template
import config #importamos el archivo de configuración "config.py"
import forms #Importamos el formulario de registro
from models import db, Usuarios, Movies #Importamos la DB

#Instancia +config 
app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)

#Rutas
@app.route('/')
def inicio():
    return ('Hola esta es la página de inicio')

@app.route('/registro', methods = ['GET','POST'])
def registro():
    form = forms.Registro()
    if form.validate_on_submit():
        Nombre = form.Nombre.data
        Apellido = form.Apellido.data
        Edad = form.Edad.data
        Genero = form.Genero.data
        Correo = form.Correo.data
        Pais = form.Pais.data
        Password = form.Password.data

        user = Usuarios(Nombre=Nombre, Apellido = Apellido, Edad = Edad, Genero = Genero, Correo=Correo, Pais = Pais, Password = Password)
        user.set_Password(Password)
        user.save()
            # Dejamos al usuario logueado
    return render_template('registro.html', form=form)

 

Atención!
Debes estar atento a que importamos las clases Usuario y Movies. También importamos forms, y luego a partir de este import referenciamos el formulario como forms.Registro() y la clase usuario como Usuarios.

Crear un usuario es básicamente crear una instancia y llamar al método set_Password() y posteriormente al método save() que como ves en el modelo se encarga de añadirlo a la base de datos y hacer «commit»

No olvides que aún no creamos las vistas, ni siquiera la carpeta "templates" yo simplemente coloqué esa dirección en el render_template porque luego pienso crear este archivo más adelante. Pero primero..

Explicando el código del controlador línea a línea y resaltando detalles importantes

Creamos la ruta "registro()" donde mostraremos el template html con el respectivo formulario. Al existir un formulario significa que el usuario deberá enviar datos por lo que también en esta ruta debemos permitir la solicitud POST.

En la función indicamos que nuestro formulario es  "Registro()" y luego nos encontramos con un condicional en el que solo se entrará si los datos del formulario son válidos, y aquí es donde nuestro controlador decidirá si se trata de una solicitud POST, eso significaría que los datos del formulario fueron validados y entonces pueden ser procesados. Así que dentro del condicional almacenamos cada campo del formulario y procedemos a crear una instancia de la clase "Usuarios" con estos datos como argumentos para los atributos antes definidos en la clase. Hacemos un "set_password" del password, método de la clase usuarios para generar el hash. Finalmente guardamos el nuevo usuario llamando al método save(). En caso de no cumplirse el condicional, simplemente se procede a cargar la url (solicitud GET).

Es decir, si presionas "submit" se creará un usuario porque entrará dentro del condicional, si no lo presionas solo se cargara una solicitud GET mostrando el sitio web donde está incrustado el formulario.

Pero eso aún no sucederá porque no existen estas tablas en nuestra base de datos!!. Recuerdan que están vacías y si, yo te dije que se iban a crear automáticamente!!. Pero para ello debemos añadir esta sentencia a nuestro controlador.

Crear las tablas "Usuarios" y "Movies" en la base de datos

Si nuestra aplicación se ejecuta se supone que la base de datos existe, de lo contrario obviamente obtendríamos un error. Pero si mediante SQLAlchemy estamos conectados a la base de datos nuestra aplicación iniciará correctamente. Eso si, al momento de usar un formulario o realizar una solicitud a la base de datos las tablas deben existir, ya que si la DB está vacía obviamente se producirá un error ya que no encuentra estos campos.

A nosotros lo que nos interesa es que al ejecutarse la aplicación se creen las tablas en caso de no existir, y si ya existen, ¡pues nada que el destino siga su curso normal! ¡Para ello podemos usar en el controlador la siguiente sentencia!

with app.app_context():
    db.create_all() #Crear las tablas en base al models
    db.session.commit() #Guardar los cambios
    
    users = Usuarios.query.all() #Obtener todos los usuarios
    print(users) #Imprimir en terminal los usuarios

Por lo tanto, nuestro controlador quedará así:

#Import
from flask import Flask, render_template
import config #importamos el archivo de configuración "config.py"
from forms import Registro#Importamos el formulario de registro
from models import db, Usuarios, Movies #Importamos la DB

#Instancia +config 
app = Flask(__name__)
app.config.from_object(config)
db.init_app(app)

#Rutas
@app.route('/')
def inicio():
    return ('Hola esta es la página de inicio')

@app.route('/registro', methods = ['GET','POST'])
def registro():
    form = Registro()
    if form.validate_on_submit():
        Nombre = form.Nombre.data
        Apellido = form.Apellido.data
        Edad = form.Edad.data
        Genero = form.Genero.data
        Correo = form.Correo.data
        Pais = form.Pais.data
        Password = form.Password.data

        user = Usuarios(Nombre=Nombre, Apellido = Apellido, Edad = Edad, Genero = Genero, Correo=Correo, Pais = Pais, Password = Password)
        user.set_Password(Password)
        user.save()
            # Dejamos al usuario logueado
    return render_template('registro.html', form=form)

with app.app_context():
    db.create_all()
    db.session.commit()
    
    users = Usuarios.query.all()
    print(users)
#Run
if __name__ == '__main__':
    app.run(debug=True)
Revisa que todo funcione bien!
Ahora, si corres tu aplicación: python run.py

Y abres PhpMyAdmin: «127.0.0.1/phpmyadmin»

Deberías ver creada las tablas Usuarios y Movies en tu base de datos Mysq como en la imagen!

Creada bases de datos FLASK

 

Crear la vista del formulario con Bootstrap

Antes que nada vamos a instalar bootstrap para que nuestro formulario se vea bien; para ello:

python -m pip install flask_bootstrap

Y ahora una vez instalado debemos aplicarlo a nuestra app en el controlador "run.py" así:

from flask_bootstrap import Bootstrap
Bootstrap(app)
Fíjate como queda el controlador:
#Import
from flask import Flask, render_template
from flask_bootstrap import Bootstrap
import config #importamos el archivo de configuración "config.py"
from forms import Registro#Importamos el formulario de registro
from models import db, Usuarios, Movies #Importamos la DB

#Instancia +config 
app = Flask(__name__)
Bootstrap(app)
app.config.from_object(config)
db.init_app(app)

#Rutas
@app.route('/')
def inicio():
    return ('Hola esta es la página de inicio')

@app.route('/registro', methods = ['GET','POST'])
def registro():
    form = Registro()
    if form.validate_on_submit():
        Nombre = form.Nombre.data
        Apellido = form.Apellido.data
        Edad = form.Edad.data
        Genero = form.Genero.data
        Correo = form.Correo.data
        Pais = form.Pais.data
        Password = form.Password.data

        user = Usuarios(Nombre=Nombre, Apellido = Apellido, Edad = Edad, Genero = Genero, Correo=Correo, Pais = Pais, Password = Password)
        user.set_Password(Password)
        user.save()
            # Dejamos al usuario logueado
    return render_template('registro.html', form=form)

with app.app_context():
    db.create_all()
    db.session.commit()
    
    users = Usuarios.query.all()
    print(users)
#Run
if __name__ == '__main__':
    app.run(debug=True)

 

Ahora debemos crear nuestro archivo "registro.html" dentro de la carpeta "templates" aplicando Bootstrap a nuestro formulario haciendo uso de la sintaxis JINJA2.

Pues simplemente bastará con crear el archivo HTML "templates/registro.html" y dentro de este incluir nuestro formulario usando la sintaxis JINJA2, quedando así:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title%} {%endblock%}</title>
    <!--BOOTSTRAP CSS --> 
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
{% import "bootstrap/wtf.html" as wtf %}
{{ wtf.quick_form(form) }}
</body>
</html>

Y pues nada, aquí estamos importando la librería bootstrap y aplicandola a el objeto WTF que representa la librería WTForms y es en base a la que luego creamos nuestro formulario usando el método .quick_form().

Recuerda que en el controlador en la ruta de "/registro" hemos pasado como argumento de render_template el formulario!

Corre tu servidor y accede a 127.0.0.1:5000/registro:

python run.py

Si has llegado hasta aquí todo debería ir bien, excepto por el siguiente error al cargar la página de registro:

ERROR - registro de usuarios en flask

meme¿Quéeee?. Pues nos está diciendo que si vamos a utilizar WTForms debemos crear una clave secreta para usar CSRF. Y he hablado de esto en un post sobre crear un formulario de contacto en flask.

Simplemente es un método de seguridad que se implementa para los formularios y aquí explico como añadirlo: El token CSRF.

 

 

Simplemente debemos añadir una clave secreta a nuestra app dentro del controlador "run.py" o mejor aún en nuestro archivo "config.py", así:

#Conexion a la base de datos
SQLALCHEMY_DATABASE_URI='mysql+pymysql://root:pythones.net@localhost/db_usuarios'
SQLALCHEMY_TRACK_MODIFICATIONS = False

#CLAVE SECRETA 
SECRET_KEY = "pythones.netelmejorblog"

La secret_key puede ser cualquiera, puedes usar alguna aplicación para generarla, puede ser simple o complicada depende el uso que le vas a dar a tu aplicación. Aquí solo estoy ejemplificando!

 

Y en el archivo HTML del "registro.html" incluimos el Token CSRF con JINJA2, quedando así:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title%} {%endblock%}</title>
    <!--BOOTSTRAP CSS --> 
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
{% import "bootstrap/wtf.html" as wtf %}
{{ form.csrf_token() }}
{{ wtf.quick_form(form) }}
</body>
</html>

 

Y ahora si recargamos la página veremos nuestro formulario:

formulario de registro de usuario en flask

Se ve algo extraño, pero claro no está definido ni siquiera dentro de un div. Por lo que ocupará el 100% del ancho del navegador, esto puedes corregirlo muy fácilmente, así como añadir un título y otros detalles más "Frontend" a la aplicación que lo haremos ahora en adelante.

El formulario se ve feo o no se aplica BOOTSTRAP
A mi me sucedió que a ratos no se aplicaba Bootstrap al formulario y luego descubrí que más allá de instalar esta librería debemos incluir el CSS de BOOTSTRAP. En este caso lo incluí solo en «registro.html» para terminar esta lección, pero obviamente es mejor trabajar con templates y ubicarlo directamente en el template BASE.html como explico aquí:

Crear template base con bootstrap en Flask.

Igualmente lo veremos en la próxima entrada.

Venga puedes probar tu formulario y revisar la base de datos con PhpMyAdmin para ver si todo ha ido correcto! En mi caso funciona, si tú tienes algún inconveniente o error no dudes consultar en nuestro chat de discord:

 

Bien por aquí yo haré un git commit.

Registro de usuarios en flask-

Y esto ha sido todo amigos!. Este post estaba para publicar hace mucho tiempo, pero como siempre digo.. "Mi talento está limitado por mi pereza". Ahora sí, está online.. A meterle!

 

 

 


 

 

Continúa leyendo!
Entrada anterior! Siguiente entrada!(En proceso)
Compartir es agradecer! :)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.