users app en google app engine - python
Configurar google app engine
Crear pagina de registro
Crear modelo de base de datos datastore
Cifrar contraseña
Crear login
Validar datos de formulario
Agregar home page
Cerrar sesion
Opción para cambiar el avatar
Validar imagen
En este tutorial vamos a crear un registro de usuarios en google app engine, usando python como lenguaje y datastore como base de datos
DEMO
Configurar google app engine
Para empezar creamos una carpeta llamada users y le agregamos estos dos archivos
app.yaml
application: usersapp version: 1 runtime: python27 api_version: 1 threadsafe: true handlers: - url: /static static_dir: static - url: /.* script: app.app libraries: - name: jinja2 version: latest
app.py
import os import webapp2 import jinja2 from google.appengine.ext import db template_dir = os.path.join(os.path.dirname(__file__), 'templates') jinja_env = jinja2.Environment(loader = jinja2.FileSystemLoader(template_dir), autoescape = True) def render_str(template, **params): t = jinja_env.get_template(template) return t.render(params) class Handler(webapp2.RequestHandler): def render(self, template, **kw): self.response.out.write(render_str(template, **kw)) def write(self, *a, **kw): self.response.out.write(*a, **kw) class Register(Handler): def get(self): self.render("register.html") app = webapp2.WSGIApplication([('/', Register), ], debug=True)
Creamos un router que escucha en la dirección raíz y una clase Register que recibe las peticiones, respondemos llamando al archivo register.html en el método render, el cual vamos a crear posteriormente.
En users agregamos una carpeta llamada templates y ponemos el siguiente archivo
register.html
<html> <head> <title></title> <link rel="stylesheet" type="text/css" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/static/styles.css"> </head> <body> <div class="container"> <h1 style="color:rgb(79, 151, 207);">Register</h1> <a href="/login">Login</a> <div id="wrap-form"> <form role="form" method="post"> <div class="form-group"> <label for="username">Username</label> <input type="text" class="form-control" name="username" id="username" placeholder="Enter your username"> </div> <div class="form-group"> <label for="email">Email</label> <input type="text" name="email" class="form-control" id="email" placeholder="Enter email"> </div> <div class="form-group"> <label for="password1">Password</label> <input type="password" name="password1" class="form-control" id="password1" placeholder="Password"> </div> <div class="form-group"> <label for="password2">Repite el Password</label> <input type="password" name="password2" class="form-control" id="password2" placeholder="Password"> </div> <hr> <div class="radio"> <p><b>Sexo</b></p> <label> <input type="radio" name="sex" id="optionsRadios1" value="male" checked> Masculino </label> </div> <div class="radio"> <label> <input type="radio" name="sex" id="optionsRadios2" value="female"> Femenino </label> </div> <br> <div class="checkbox"> <label> <input type="checkbox" name="accept"> Acepto las condiciones </label> <p class="error-form">{{ conditions }}</p> </div> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </body> </html>
En el header llamamos al framework bootstrap css y a un archivo css propio El formulario de registro contiene campos de texto para el nombre, email , password, además de un input radio ( para seleccionar el sexo ) y un input checkbox para aceptar las condiciones
En el directorio del proyecto creamos una carpeta llamada static y le ponemos un archivo llamado styles.css
#wrap-form{ width: 60%; margin-top: 40px; } .error-form{ color:red; font-weight: bold; margin-top: 5px; }
Estas líneas simplemente configuran el ancho del formulario al 60 % relativo a lo que tenga como valor su elemento padre( en este caso el div con la clase container) y deja un espacio de 40 píxeles entre el h1 y el formulario
la clase error-form pone el texto en rojo y negrita. Esta clase la vamos a utilizar cuando validemos el formulario
Para ver en funcionamiento el registro , debemos configurar una aplicación en google app engine
Abran el programa ( si estan en windows presionen el botón inicio y en el campo de texto escriban el nombre del programa hasta que aparezca en el cuadro de resultados )
Una vez ejecutado el programa seguimos estos pasos
File => Add existing applicacions => Browse ( en el explorador seleccione la carpeta users ) y click en Add.
Después que de seleccionar la carpeta presionen Run y despues Browse, si todo esta bien se a va abrir el navegador que tengamos configurado por defecto y nos va a mostrar el formulario
Crear modelo de base de datos datastore
Como base de datos vamos a usar app engine Datastore , que provee un robusto y escalable sistema de almacenamiento.
Datastore posee un lenguaje de consultas similar a SQL llamada GQL y una api de modelos de datos
En app.py debajo del codigo que importa los modulos ponemos esto
class User(db.Model): username = db.StringProperty(required=True) email = db.StringProperty(required=True) password = db.StringProperty(required=True) avatar = db.BlobProperty() sex = db.StringProperty(required=True)
Creamos una clase usando la palabra clave de python class y le damos como nombre User, heredamos todos los métodos y atributos de la clase db.Model.
En el cuerpo de la función definimos las propiedades de la aplicación y el tipo de dato que contiene
Todos las propiedades requieren una cadena ( StringProperty ), excepte el avatar que guarda un objeto binario.
El parámetro required=True , define que el valor debe estar presente cuando se agregue una nueva entidad Para insertar datos en la base de datos , ponemos esto en la clase Register en app.py
def post(self): username = self.request.get("username").strip() email = self.request.get("email").strip() password1 = self.request.get("password1").strip() password2 = self.request.get("password2").strip() sex = self.request.get("sex").strip() accept = self.request.get("accept").strip() if accept == "on": if username and email and password1 and sex: user = User(username=username, email=email, password=password1, sex=sex ) user.put() self.write(“User added!”) else: self.write("Fill all inputs") else: self.write("Please accept conditions")
Este método responde al formulario de registro que envía una petición POST a la dirección raíz de la aplicación
La primera parte crea un conjunto de variables usando el método self.request.get(“form”) , request toma como parámetro el nombre del atributo name de los elementos del formulario.
por ej <input type=”text” name=”username”>
strip() se encarga de eliminar los espacios en blanco del texto
Después creamos un condicional que verifica el valor de la variable accept( el checkbox que acepta las condiciones ) sea igual a la cadena on.
En caso de que el valor sea off , mostramos una cadena en el navegador pidiendo que el usuario acepte las condiciones( más adelante vamos a mostrar estos errores en el formulario de registro )
Si nos da verdadero verificamos que las variables tengan algún texto usando otra vez un if.
Para ingresar datos usamos la clase creada anteriormente( User el modelo de datos ) y pasamos como parámetros los atributos que queremos insertar.
Después de esto usamos el método put para ejecutar la transacción
Cifrar contraseña
En el paso anterior mostramos como insertar una nueva entidad a la base de datos , pero esto tiene un problema, el password esta en texto plano lo cual conlleva un grave problema ya que si alguien logra tomar control de la base de datos , puede tomar la cuenta de cualquier usuario registrado.
Para evitar este problema vamos a cifrar la contraseña con la ayuda del módulo hashlib
en app.py al principio del archivo agregamos esta linea
import hashlib
haslib implementa una interfaz común para usar diferentes hash ( md5, sha1, SHA224 , etc).
Para hacer más difícil la tarea de quien intente conocer la contraseña, vamos a agregar otra capa de seguridad usando una cadena antes del hash
app.py
SALT = “mysecretsalt”
Para generar el hash concatenamos el password cifrado más el salt
passhash = hashlib.md5(password1+salt).hexdigest()
if accept == "on": if username and email and password1 and sex: passhash = hashlib.md5(password1+SALT).hexdigest() user = User(username=username, email=email, password=passhash, sex=sex ) self.write(user.put()) else: self.write("Fill all inputs") else: self.write("Please accept conditions")
Probemos insertando un usuario y veamos los resultados en la SDK console de google app engine Para abrir la consola click en el botón SDK Console o presionen el comando ctrl+k, despues que se abra en el navegador click en Datastore Viewer
Crear login
Una vez realizado el formulario de registro vamos a crear otro template html llamado login.html en la carpeta nombrada template
<html> <head> <title></title> <link rel="stylesheet" type="text/css" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/static/styles.css"> </head> <body> <div class="container"> <h1 style="color:rgb(79, 151, 207);">Login</h1> <a href="/">Register</a> <div id="wrap-form"> <form action="" method="post"> <div class="form-group"> <label for="email">Email</label> <input type="email" name="email" class="form-control" id="email" placeholder="Enter email"> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" name="password" class="form-control" id="password" placeholder="Password"> </div> </div> <button type="submit" class="btn btn-primary">Login</button> </form> </div> </body> </html>
El formulario contiene un campo de texto donde se va a insertar el email y un input de tipo password
Para renderizar este archivo tenemos que modificar app.py
class Login(Handler): def get(self): self.render("login.html") app = webapp2.WSGIApplication([('/', Index), ('/login', Login) , ] , debug=True)
Primero creamos una clase llamada Login que contiene un método llamado get que responde a las peticiones que se realicen a la ruta “/login” ( definida en el router abajo ), mostrando el archivo login.html
De la misma manera que lo hicimos en register.html el atributo method de la etiqueta form en login tiene un valor post, para responder a esta peticion tenemos que agregar otro método en la clase Login
def post(self): email = self.request.get("email").strip() password = self.request.get("password").strip() if email and password: password = hashlib.md5(password+salt).hexdigest() user = db.GqlQuery("select * from User where email=:email and password=:password" , email=email, password=password) if user.count(): self.write("You are in") else: self.write("Get out") else: self.write("insert email and password")
En el inicio del método post creamos dos variables que contienen los datos que nos envía el formulario en los campos de texto.
El if verifica si estas variables contienen algún valor, en caso de que esta definición nos de falso enviamos un mensaje al navegador con la cadena "insert email and password"
Si el condicional es verdadero , ciframos el password de la misma manera que lo hicimos en la clase Register usando el módulo hashlib y concatenando la variable SALT
Después creamos una consulta usando el método GqlQuery del módulo db( familiar al lenguaje SQL ) pasando como placeholders el valor de los datos del formulario.
user.count() devuelve la cantidad de entradas que contiene la consulta, en caso de que exista el usuario mostramos un mensaje en el navegador
Validar datos de formulario
Vamos a validar que el formato del email sea válido, la longitud del nombre y que los passwords sean iguales.
Para lograr este objetivo vamos a usar expresiones regultares, en app.py ponemos esta linea
import re
Primero vamo a validar el nombre de usuario de la siguiente manera
USER_RE = re.compile(r"^[a-zA-Z0-9_-]{3,20}$") def valid_username(username): return username and USER_RE.match(username)
la varialbe USER_RE compila el patron de la expresión regular a un objeto .
Patron
r"^[a-zA-Z0-9_-]{3,20}$"
la r que esta el comienzo indica que el el patron esta en texto crudo( útil para evitar conflictos con algunos caracteres ). el patron esta delimitado en comillas chequea que la cadena contenga un carácter( en minuscula o mayuscula ) , un guión o guion bajo, tres o más veces
La funcion valid_username usa el método match del módulo re, el cual devuelve un valor booleano( True o False )
Validar password
PASS_RE = re.compile(r"^.{3,20}$") def valid_password(password): return password and PASS_RE.match(password)
La sintaxis para validar el password es similar a lo visto anteriormente , en este caso solo se modifica el patrón de la expresión regular
r"^.{3,20}$"
el signo ^ verifica que lo sigue a continuación este al comienzo de la cadena , el . encuentra cualquier caracter excepto un salto de linea , tres o más veces
El signo $ marca el fin de la cadena
Validar email
EMAIL_RE = re.compile(r'^[\S]+@[\S]+\.[\S]+$') def valid_email(email): return email or EMAIL_RE.match(email) '^[\S]+@[\S]+\.[\S]+$'
El [\S]+ encuentra todo lo que no sea un espacio en blanco una o más veces seguido de un arroba
\. encuentra un punto literal( el \ es necesario ya que le . es un carácter especial )
Para usar estas funcion tenemos que modificar el metodo post de la clase Register en app.py
if accept == "on": errors = {} if not valid_username(username): errors["error_username"] = "This is not a valid username" if not valid_email(email): errors["error_email"] = "This is not a valid email" if not valid_password(password1): errors["error_pass"] = "The password must be to 3 a 20 characterers" if password1 != password2: errors["error_samepass"] = "The passwords doesn't match" if len(errors): self.render("register.html", **errors) else: passhash = hashlib.md5(password1+salt).hexdigest() user = User(username=username, email=email, password=passhash, sex=sex ) user.put() self.render("register.html", message="Register completed!") else: self.render("register.html", conditions="Accept conditions")
En la variable errors creamos un diccionario vacío, el cual va a contener los errores que se encuentren
Usamos el condicional if not en las funciones definidas anteriormente pasando como atributo las variables que tienen los valores del formulario, en caso de que nos de verdadero , guardamos el error correspondiente en el diccionario errors
la función len(errors) devuelve el número de valores que tenga errores, en caso de tener 1 o más valores , renderizados otra vez register.html pero esta vez pasando un segundo argumento ( el diccionario errors )
En caso de que len(errors) devuelve 0 ( lo que significa que el formulario es correcto ) ingresamos el usuario a la base de datos como hicimos antes, pero en vez de mostrar una cadena en el navegador esta vez enviamos un mensaje a register.html
Para poder ver los mensajes que enviamos en app.py vamos a usar el template jinja
{% if message %} {{ message }} {% endif %}
Este mensaje se muestra si se inserto el usuario en la base de datos , usamos el condicional if para verificar si la variable message existe y en caso de ser verdadero mostramos el mensaje usando las clases de bootstrap css alert y alert-success
<p class="error-form">{{ error_username }}</p>
Creamos un parrafo con la clase error-form( esta pone el texto de color rojo ) y entre dos llaves ponemos el nombre de la clave del diccionario que enviamos en app.py
Agregar home page
Hasta el momento tenemos creado el formulario de registro y el login, en esta sección vamos a crear la página de inicio del usuario que se va a mostrar cuando el usuario ingrese al sitio
en templates ponemos un archivo llamado home.html
<html> <head> <title></title> <link rel="stylesheet" type="text/css" href="http://netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> <link rel="stylesheet" type="text/css" href="/static/styles.css"> </head> <body> <div class="container"> <h1>Hello {{ username }}</h1> <div class="media"> <a class="pull-left" href="#"> {% if avatar == "false" %} <img class="media-object" src="/static/images/default_user.png" alt="..."> {% else %} <img class="media-object" src="/img?img_id={{key}}" alt="..."> {% endif %} </a> <div class="media-body"> {{ email }} </div> </div> <br> <h4>Change avatar</h4> <form role="form" method="post" enctype="multipart/form-data"> <div class="form-group"> <input type="file" name="avatar"> </div> <button type="submit" class="btn btn-primary">Send</button> </form> {% if message %} <h4 class="alert alert-danger">{{ message }}</h4> {% endif %} <a href="/logout" class="btn">Salir</a> </div> </body> </html>
Este archivo muestra los datos del usuario usando nuevamente el template jinja ( estos datos lo vamos a guardar en cookies una vez que el login sea exitoso ),
También un formulario que permite al usuario cambiar su avatar y un condicional que verifica si existe la cookie avatar para mostrar en caso negativo una imagen por defecto
Crear login cookies
En app.py modificamos el metodo post de la clase Login
errors = {} if not valid_email(email): errors["error_email"] = "This is not a valid email" if not valid_password(password): errors["error_pass"] = "The password must be to 3 a 20 characterers" if len(errors): self.render("login.html", **errors) else: password = hashlib.md5(password+salt).hexdigest() #self.write(password) user = db.GqlQuery("select * from User where email=:email and password=:password" , email=email, password=password) if user.count(): for u in user: self.write(u.username + " ") self.write(str(u.key()) + " ") self.write(u.email) if u.avatar == None: self.response.headers.add_header('Set-Cookie',"avatar=false") self.response.headers.add_header('Set-Cookie',"logged=true") self.response.headers.add_header('Set-Cookie',"username="+str(u.username)) self.response.headers.add_header('Set-Cookie',"email="+str(u.email)) self.response.headers.add_header('Set-Cookie',"key="+str(u.key())) self.redirect("/home") #self.response.headers.add_header('Set-Cookie',"logged="+str(helpers.make_secure_val("true"))) else: self.render("login.html", message="The email or password is not correct")
Tal como lo hicimos en la clase Register creamos un diccionario llamado erros que va a albergar los mensajes de error que genere el formulario.
Otra diferencia se nota en el condicional if user.count(), en este paso hacemos un for por la variable user y creamos algunas cookies con los datos del usuario
self.response.headers.add_header('Set-Cookie',"username="+str(u.username))
Después de esto redireccionamos al router home Modificamos el router en app.py
class Home(Handler): def get(self): if self.request.cookies.get("logged") == "true": username = self.request.cookies.get("username") email = self.request.cookies.get("email") avatar = self.request.cookies.get("avatar") key = self.request.cookies.get("key") self.render("home.html", username=username, email=email, avatar=avatar, key=key) else: self.redirect("/") app = webapp2.WSGIApplication([('/', Index), ('/login', Login) , ('/home', Home), ] , debug=True)
En el método get chequeamos si existe una cookie llamada logged con valor igula a true, en caso de que nos de falso , redireccionamos la aplicación mostrando el registro.
De otra manera nos encargamos de renderizar el home.html enviando los datos que se encuentran en las cookies
Cerrar sesión
Modificamos app.py para poder cerrar sesion
class Logout(Handler): def get(self): self.response.headers.add_header("Set-Cookie", "logged=; Expires=Thu, 01-Jan-1970 00:00:00 GMT") self.response.headers.add_header("Set-Cookie", "username=; Expires=Thu, 01-Jan-1970 00:00:00 GMT") self.response.headers.add_header("Set-Cookie", "email=; Expires=Thu, 01-Jan-1970 00:00:00 GMT") self.response.headers.add_header("Set-Cookie", "avatar=; Expires=Thu, 01-Jan-1970 00:00:00 GMT") self.redirect("/login") app = webapp2.WSGIApplication([('/', Index), ('/login', Login) , ('/home', Home), ('/logout', Logout), ] , debug=True)
El método de la clase Logout es bastante simple , este solo se encarga de eliminar todas las cookies , pasando el nombre de la cookie y como atributo una fecha menor a la actual.
Despues de esto redireccionamos la página mostrando el formulario de logueo
Modificar avatar
Si echamos un vistazo a home.html vamos a encontrar un formulario al final del archivo
<form role="form" method="post" enctype="multipart/form-data"> <div class="form-group"> <input type="file" name="avatar"> </div> <button type="submit" class="btn btn-primary">Send</button> </form>
La diferencia con respecto a los anteriores formulario es que este contiene un atributo llamado enctype con el valor "multipart/form-data" el cual nos permite enviar archivo al servidor
Otra diferencia es que el campo de texto tiene un valor de tipo file
Agregamos un método post en la clase Home
def post(self): # CHECK VALID IMAGE TYPE img = self.request.params['avatar'].filename ext = img.split(".")[1] valid = ["jpg","jpeg","gif","png"] if ext in valid: avatar = images.resize(self.request.get('avatar'),120,120) username = self.request.cookies.get("username") user = db.GqlQuery("select * from User where username=:username", username=username) for u in user: u.avatar = avatar u.put() self.response.headers.add_header('Set-Cookie',"avatar=; Expires=Thu, 01-Jan-1970 00:00:00 GMT") self.redirect("home") else: self.render("home.html", message="The avatar would be jpg, gif or png type")
La parte inicial se encarga de validar si el archivo que seleccionó el usuario es una imagen ( jpg, gif o png ), usamos el método split para separar la cadena en una lista separada por el carácter . y mediante un condicional verificamos si lo que la variable ext contiene es igual a alguno de los valores de la lista valid.
Si este valor nos da verdadero cambiamos el tamaño de la imagen usando el método resize del módulo image ( 120 x 120)
Tiene que importar este módulo
from google.appengine.api import images
Buscamos al usuario en la base de datos y actualizamos el avatar usando el método put, después sobreescribimos la cookie de nombre avatar y nos vamos al home
Para poder ver la imagen agregen lo siguiente en app.py
class Image(webapp2.RequestHandler): def get(self): greeting = db.get(self.request.get('img_id')) if greeting.avatar: self.response.headers['Content-Type'] = 'image/png' self.response.out.write(greeting.avatar) else: self.error(404) app = webapp2.WSGIApplication([('/', Index), ('/login', Login) , ('/home', Home), ('/logout', Logout), ('/img', Image), ] , debug=True)









