From 15ac576058c5219419c6fbfc859d4bef7fdda400 Mon Sep 17 00:00:00 2001 From: coolneng Date: Thu, 9 Jan 2020 03:17:31 +0100 Subject: [PATCH] Add login functionality and basic HTML pages --- code/Pipfile | 1 + code/Pipfile.lock | 9 +++++++- code/app/__init__.py | 4 +++- code/app/forms.py | 10 ++++++++ code/app/models.py | 33 ++++++++++++++++----------- code/app/routes.py | 43 +++++++++++++++++++++++++++++------ code/app/templates/admin.html | 6 +++++ code/app/templates/base.html | 22 +++++++++++++++--- code/app/templates/index.html | 6 ++--- code/app/templates/login.html | 24 +++++++++++++++++++ code/config.py | 1 + code/database/parser.py | 9 ++++++-- data/annual_data.csv | 2 +- data/glacier.csv | 2 +- data/user.csv | 4 ++-- docs/Project.org | 19 +++++----------- 16 files changed, 147 insertions(+), 48 deletions(-) create mode 100644 code/app/forms.py create mode 100644 code/app/templates/admin.html create mode 100644 code/app/templates/login.html diff --git a/code/Pipfile b/code/Pipfile index ad15fdd..b6fe635 100644 --- a/code/Pipfile +++ b/code/Pipfile @@ -12,6 +12,7 @@ flask-sqlalchemy = "*" pandas = "*" iso3166 = "*" flask-wtf = "*" +flask-login = "*" [requires] python_version = "3.8" diff --git a/code/Pipfile.lock b/code/Pipfile.lock index ea74bfb..646671d 100644 --- a/code/Pipfile.lock +++ b/code/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f4ff138e539e1c5bcdc3a3f5046b1d5087a0f0315d1a18181fc2c6eeb1227671" + "sha256": "234ca1fc7cbb10534d17febc8a9d0320ecb5c6317351bcd97e161c860201b5a5" }, "pipfile-spec": 6, "requires": { @@ -31,6 +31,13 @@ "index": "pypi", "version": "==1.1.1" }, + "flask-login": { + "hashes": [ + "sha256:c815c1ac7b3e35e2081685e389a665f2c74d7e077cb93cecabaea352da4752ec" + ], + "index": "pypi", + "version": "==0.4.1" + }, "flask-sqlalchemy": { "hashes": [ "sha256:0078d8663330dc05a74bc72b3b6ddc441b9a744e2f56fe60af1a5bfc81334327", diff --git a/code/app/__init__.py b/code/app/__init__.py index 6d6c9a3..94eb5a9 100644 --- a/code/app/__init__.py +++ b/code/app/__init__.py @@ -1,10 +1,12 @@ from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy +from flask_login import LoginManager app = Flask(__name__) app.config.from_object(Config) db = SQLAlchemy(app) - +login = LoginManager(app) +login.login_view = "login" from app import routes, models diff --git a/code/app/forms.py b/code/app/forms.py new file mode 100644 index 0000000..55a4d2d --- /dev/null +++ b/code/app/forms.py @@ -0,0 +1,10 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms.validators import DataRequired + + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired()]) + password = PasswordField("Password", validators=[DataRequired()]) + remember_me = BooleanField("Remember Me") + submit = SubmitField("Sign In") diff --git a/code/app/models.py b/code/app/models.py index 0fc8dfd..d652a02 100644 --- a/code/app/models.py +++ b/code/app/models.py @@ -1,16 +1,16 @@ -from app import db -from subprocess import run -from database.constants import DB_USER, DB_PW, DB_NAME +from app import db, login +from flask_login import UserMixin +from werkzeug.security import check_password_hash class Glacier(db.Model): - uid = db.Column(db.String(5), primary_key=True) + id = db.Column(db.String(20), primary_key=True) country = db.Column(db.String(60)) name = db.Column(db.String(60)) annual_data = db.relationship("Annual_Data") - def __init__(self, uid, country, name): - self.uid = uid + def __init__(self, id, country, name): + self.id = id self.country = country self.name = name @@ -18,7 +18,7 @@ class Glacier(db.Model): class Annual_Data(db.Model): __tablename__ = "annual_data" year = db.Column(db.Integer, primary_key=True) - uid = db.Column(db.ForeignKey("glacier.uid"), primary_key=True) + id = db.Column(db.ForeignKey("glacier.id"), primary_key=True) surface = db.Column(db.Float) length = db.Column(db.Float) elevation = db.Column(db.Float) @@ -30,15 +30,22 @@ class Annual_Data(db.Model): self.elevation = elevation -class User(db.Model): - uid = db.Column(db.Integer, primary_key=True) +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True) registration_date = db.Column( db.DateTime, nullable=False, server_default=db.func.now() ) username = db.Column(db.String(20), nullable=False, unique=True) - password = db.Column(db.String(60)) + password_hash = db.Column(db.String(128), unique=True) - def __init__(self, uid, username, password): - self.uid = uid + def __init__(self, id, username, password_hash): + self.id = id self.username = username - self.password = password + self.password_hash = password_hash + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + @login.user_loader + def load_user(id): + return User.query.get(int(id)) diff --git a/code/app/routes.py b/code/app/routes.py index 772bbc8..1eca20e 100644 --- a/code/app/routes.py +++ b/code/app/routes.py @@ -1,13 +1,42 @@ from app import app -from flask import render_template +from app.forms import LoginForm +from app.models import User +from flask import flash, redirect, render_template, url_for, request +from flask_login import current_user, login_user, logout_user, login_required +from werkzeug.urls import url_parse @app.route("/") @app.route("/index") def index(): - user = {"username": "Bolaji"} - posts = [ - {"author": {"username": "Miloud"}, "body": "Beautiful day in Meknes!"}, - {"author": {"username": "Sebtaoui"}, "body": "The Farkouss movie was lit!"}, - ] - return render_template("index.html", title="Home", user=user, posts=posts) + return render_template("index.html", title="Home Page") + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("admin")) + form = LoginForm() + if form.validate_on_submit(): + user = User.query.filter_by(username=form.username.data).first() + if user is None or not user.check_password(form.password.data): + flash("Invalid username or password") + return redirect(url_for("login")) + login_user(user, remember=form.remember_me.data) + next_page = request.args.get("next") + if not next_page or url_parse(next_page).netloc != "": + next_page = url_for("admin") + return redirect(next_page) + return render_template("login.html", title="Sign In", form=form) + + +@app.route("/logout") +def logout(): + logout_user() + return redirect(url_for("index")) + + +@app.route("/admin") +@login_required +def admin(): + return render_template("admin.html", title="Admin Page") diff --git a/code/app/templates/admin.html b/code/app/templates/admin.html new file mode 100644 index 0000000..2410e53 --- /dev/null +++ b/code/app/templates/admin.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Hi, {{ current_user.username }}!

+ Do you want to nuke the database? +{% endblock %} diff --git a/code/app/templates/base.html b/code/app/templates/base.html index db4394d..b7c588c 100644 --- a/code/app/templates/base.html +++ b/code/app/templates/base.html @@ -7,8 +7,24 @@ {% endif %} -
IGDB: Home
+
IGDB: + Home + {% if current_user.is_anonymous %} + Login + {% else %} + Administration + Logout + {% endif %} +

- {% block content %}{% endblock %} - + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + {% block content %}{% endblock %} diff --git a/code/app/templates/index.html b/code/app/templates/index.html index b580e8c..f7d761d 100644 --- a/code/app/templates/index.html +++ b/code/app/templates/index.html @@ -1,8 +1,6 @@ {% extends "base.html" %} {% block content %} -

Hi, {{ user.username }}!

- {% for post in posts %} -

{{ post.author.username }} says: {{ post.body }}

- {% endfor %} +

IGDB: International Glacier Database

+ The IGDB is a database, that uses data from the WGMS to illustrate the consequences of climate change. {% endblock %} diff --git a/code/app/templates/login.html b/code/app/templates/login.html new file mode 100644 index 0000000..806e09a --- /dev/null +++ b/code/app/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }}
+ {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+
+{% endblock %} diff --git a/code/config.py b/code/config.py index aef351a..958888a 100644 --- a/code/config.py +++ b/code/config.py @@ -4,3 +4,4 @@ from database.constants import CONNECTION_URI class Config(object): SQLALCHEMY_DATABASE_URI = CONNECTION_URI SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = "trolaso" diff --git a/code/database/parser.py b/code/database/parser.py index f2861cd..9dc3226 100644 --- a/code/database/parser.py +++ b/code/database/parser.py @@ -3,6 +3,7 @@ from pandas import DataFrame, concat, read_csv from csv import QUOTE_NONNUMERIC from database.constants import ADMIN_PW from os import path +from werkzeug.security import generate_password_hash def country_conversion(political_unit) -> str: @@ -40,7 +41,7 @@ def rename_fields(df_list): new_fields = { "POLITICAL_UNIT": "country", "NAME": "name", - "WGMS_ID": "uid", + "WGMS_ID": "id", "YEAR": "year", "MEDIAN_ELEVATION": "elevation", "AREA": "surface", @@ -57,7 +58,11 @@ def create_databases(df): "annual_data": "../data/annual_data.csv", "user": "../data/user.csv", } - user = {"uid": [7843], "username": ["admin"], "password": [ADMIN_PW]} + user = { + "id": [7843], + "username": ["admin"], + "password_hash": [generate_password_hash(ADMIN_PW)], + } dataframes = { "glacier": df[["POLITICAL_UNIT", "NAME", "WGMS_ID"]].drop_duplicates(), "annual_data": df[["WGMS_ID", "YEAR", "AREA", "MEDIAN_ELEVATION", "LENGTH"]], diff --git a/data/annual_data.csv b/data/annual_data.csv index 121985b..ad15308 100644 --- a/data/annual_data.csv +++ b/data/annual_data.csv @@ -1,4 +1,4 @@ -"uid","year","surface","elevation","length" +"id","year","surface","elevation","length" 10452,2018,1.68,"",2.1 2665,2014,12.9,390.0,7.6 2665,2015,12.9,390.0,7.6 diff --git a/data/glacier.csv b/data/glacier.csv index 7d08530..99da022 100644 --- a/data/glacier.csv +++ b/data/glacier.csv @@ -1,4 +1,4 @@ -"country","name","uid" +"country","name","id" "Afghanistan","PIR YAKH",10452 "Antarctica","BAHIA DEL DIABLO",2665 "Antarctica","BELLINGSHAUSEN",6833 diff --git a/data/user.csv b/data/user.csv index 22b914f..3ce723b 100644 --- a/data/user.csv +++ b/data/user.csv @@ -1,2 +1,2 @@ -"uid","username","password" -7843,"admin","fuckmonsanto" +"id","username","password_hash" +7843,"admin","pbkdf2:sha256:150000$YedV5Qqy$2faccb322cfb1a546d68286de06ebee0888ffafae1f0444073de85d0c59c3748" diff --git a/docs/Project.org b/docs/Project.org index d746b46..783c689 100644 --- a/docs/Project.org +++ b/docs/Project.org @@ -20,27 +20,20 @@ datos relevantes para estudios acerca del cambio climático, y acotando éstos a 1. *RD1*: Datos del glaciar - País - /Cadena de 60 caracteres máximo/ - Nombre del glaciar - /Cadena de 60 caracteres máximo/ - - ID del glaciar (Compatible con la WGMS) - /Cadena de 5 caracteres/ + - ID del glaciar (Compatible con la WGMS) - /Cadena de 20 caracteres/ 2. *RD2*: Datos anuales de un glaciar - - ID del glaciar (Compatible con la WGMS) - /Cadena de 5 caracteres/ + - ID del glaciar (Compatible con la WGMS) - /Cadena de 20 caracteres/ - Área - /Decimal/ - Volumen - /Decimal/ - Altura - /Decimal/ - - Año - /Decimal/ - -3. *RD3*: Datos de cambio de un glaciar - - ID del glaciar (Compatible con la WGMS) - /Cadena de 5 caracteres/ - - Variación de área - /Decimal/ - - Variación de volumen - /Decimal/ - - Variación de altura - /Decimal/ - - Año - /Decimal de 10 dígitos/ - -4. *RD4*: Datos del administrador + - Año - /Entero de 11 dígitos/ + +3. *RD3*: Datos del administrador - ID - /Entero de 11 dígitos/ - Fecha y hora de alta - /Fecha y hora en formato yyyy-mm-dd hh:mm/ - Nombre de usuario - /Cadena de 20 caracteres máximo/ - - Contraseña - /Cadena de 60 caracteres máximo/ + - Hash de la contraseña - /Cadena de 128 caracteres máximo/ *** Funcionales