Commit 8566df61 authored by Nelso Jost's avatar Nelso Jost

FIX: refactor and improved UI

parent 545d69c8
......@@ -95,6 +95,7 @@ clean-logs-nginx:
clean-all: clean-venv
py3clean app
rm -f app/db.sqlite
logtail-nginx-access:
tail production/nginx-gunicorn/logs/nginx-access.log
......
......@@ -71,7 +71,7 @@ class ChangeEmailForm(Form):
confirm_new_email = StringField('Confirmar Email', validators=[
Required(), Length(1, 64), Email()])
submit = SubmitField('Enviar')
submit = SubmitField('Prosseguir')
def __init__(self):
super().__init__()
......
from flask import render_template, redirect, request, url_for, flash, session
from flask.ext.login import login_user, logout_user, login_required, \
current_user
from . import auth
from . import auth # blueprint
from ..models import *
from .forms import *
from .. import db
......@@ -10,6 +10,7 @@ from ..email import send_email
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
print(form.email.data, request.form)
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user is not None and user.verify_password(form.password.data):
......@@ -104,6 +105,8 @@ def change_email():
if form.validate_on_submit():
print(request.form)
current_user.email = form.new_email.data
current_user.confirmed = False
db.session.add(current_user)
......
#-------------------------------------------------------------------------------
# Author: Nelso G. Jost <nelsojost@gmail.com> Copyright(C) 2015
# License: AGPL
# Purpose: Holds application-wide settings.
# Purpose: Hold application-wide settings.
#-------------------------------------------------------------------------------
import os
basedir = os.path.abspath(os.path.dirname(__file__))
......@@ -9,6 +9,8 @@ basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
host = '0.0.0.0'
WTF_CSRF_ENABLED = True
# wtforms secure submit transactions
SECRET_KEY = os.environ.get('EMMPASS')
......@@ -24,7 +26,6 @@ class Config:
MAIL_USE_TLS = True
MAIL_USERNAME = 'cta.emm.web'
MAIL_PASSWORD = os.environ['EMMPASS']
MAIL_SUBJECT_PREFIX = '[CTA-EMM-WEB] '
MAIL_SENDER = 'CTA-EMM-WEB <cta.emm.web@gmail.com>'
......@@ -32,6 +33,14 @@ class Config:
def init_app(app):
pass
class MESSAGES:
CONFIRM_DELETE_BOARD =\
"""\
ATENÇÃO! Todos os dados relativos a essa estação serão\
perdidos! Certifique-se de possuir um backup antes de continuar.\
<br><br>Deseja mesmo prosseguir?\
"""
class DevelopmentConfig(Config):
# activat es auto reload on file changes and werkzeug's debugger
......@@ -43,7 +52,7 @@ class DevelopmentConfig(Config):
class ProductionConfig(Config):
DEBUG = False
# format: mysql+ENGINE://USER:PASSWORD@localhost/DATABASE
# format: mysql+DRIVER://USER:PASSWORD@localhost/DATABASE
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://ctaemm:ctaemm@localhost/emmdb'
......@@ -57,7 +66,3 @@ class AUTO_PAGE_RELOAD:
active = False
interval = 2000
MSG_CONFIRM_DELETE =\
"""ATENÇÃO! Todos os dados relativos a essa estação serão perdidos!!!\
<br><br>Deseja mesmo prosseguir?"""
......@@ -13,9 +13,8 @@ from .. import db
from ..models import *
from ..utils import ClassProperty
__all__ = ['BoardSelectionForm', 'ManageBoardForm', 'SensorListForms',
'MyFileField',
'ImportCSVForm', 'ExportCSVForm']
__all__ = ['BoardSelectionForm', 'ManageBoardForm', 'SensorListItemForm',
'MyFileField', 'ImportCSVForm', 'ExportCSVForm']
class BoardSelectionForm(Form):
......@@ -71,16 +70,81 @@ class BoardSelectionForm(Form):
session['current_board']['username'] = board.user.username
class ManageBoardForm(Form):
id = StringField('ID')
nickname = StringField('Apelido')
latitude = StringField('Latitude')
longitude = StringField('Longitude')
description = TextAreaField('Descrição')
board_nickname = StringField('Apelido', validators=[
Required("Preenchimento obrigatório"),
Length(3, 20, "Deve possuir entre 3 e 20 caracteres"),
Regexp('^[A-Za-z][A-Za-z0-9_]*$', 0,
"Deve possuir apenas letras letras, números ou sublinhados.")])
board_latitude = StringField('Latitude')
board_longitude = StringField('Longitude')
board_description = TextAreaField('Descrição')
_SESSION_KEY = 'manage_board_form'
def __init__(self):
super().__init__(request.form)
self.sensors = self.init_sensor_list_forms()
def init_session(self):
session[self._SESSION_KEY] = []
for s in current_app.config['DB_INIT']['SUPPORTED_SENSORS']:
session[self._SESSION_KEY].append(s.copy())
self.sensors = self.init_sensor_list_forms()
# print("initing session['{}']".format(self._SESSION_KEY))
# pprint(session[self._SESSION_KEY])
def update_session(self):
for component, value in request.form.items():
if component.startswith('sensor_'):
field = component[1:component.rfind('_')]
i = int(component[component.rfind('_')+1:])
session[self._SESSION_KEY][i][field] = value
pprint(session[self._SESSION_KEY])
def init_sensor_list_forms(self):
if not self._SESSION_KEY in session: return []
forms = []
for i, json_data in enumerate(session[self._SESSION_KEY]):
f = SensorListItemForm(**json_data)
f.index = i
# if cls.nickname_focus_i is not None and cls.nickname_focus_i == i:
# form.nickname_has_focus = True
for field in SensorListItemForm.empty_json:
getattr(f, field).name = 'sensor_' + field + '_' + str(i)
forms.append(f)
return forms
def add_sensor(self):
pass
def delete_sensor(self):
# there was any btn_delete_sensor click?
for key in request.form:
if 'btn_delete_sensor' in key:
del session[self._SESSION_KEY][int(key[key.rfind('_')+1:])]
break
def validate_and_save(self):
self.update_session()
print('validating...')
if self.validate():
try:
d = {k[k.find('_')+1:]: v for k, v in request.form.items()
if k.startswith('board_')}
d.update(user=current_user)
board = Board.add(**d)
print(board)
db.session.commit()
return board
except Exception as e:
print('Exception: ', e)
else:
print('invalid!')
class SensorListForms(Form):
class SensorListItemForm(Form):
nickname = StringField('Apelido', validators=[
Required(), Length(1, 20), Regexp('^[A-Za-z][A-Za-z0-9_]*$', 0,
"Apelidos devem possuir apenas letras letras, números, "
......@@ -91,8 +155,6 @@ class SensorListForms(Form):
unity_label = SelectField('Unidade', coerce=str)
nickname_has_focus = False
_SESSION_KEY = 'sensor_list_manager'
_ID_CHAR = '@'
def __init__(self, **kwargs):
super().__init__()
......@@ -118,58 +180,27 @@ class SensorListForms(Form):
else:
self.unity_label.choices = [('', ' ')]
@classmethod
def generate_forms(cls):
forms = []
for i, json_data in enumerate(session[cls._SESSION_KEY]):
form = cls(**json_data)
form.index = i
# if cls.nickname_focus_i is not None and cls.nickname_focus_i == i:
# form.nickname_has_focus = True
for field in cls.empty_json:
getattr(form, field).name = cls._ID_CHAR + field + '_' + str(i)
forms.append(form)
return forms
@classmethod
def validate_and_save(cls):
pprint(request.form)
form = ManageBoardForm()
if form.validate_on_submit():
board_data = {k: v for k, v in request.form.items()
if k in Board.__fields__}
board_data.update(sensors=session[cls._SESSION_KEY],
user_id=current_user.id)
board = Board.add(**board_data)
db.session.commit()
return True
return False
success = False
for form in cls.generate_forms():
success = success and form.validate_on_submit()
if not success:
return False
board_data = {k: v for k, v in request.form.items()
if k in Board.__fields__}
board_data.update(sensors=session[cls._SESSION_KEY],
user_id=current_user.id)
board = Board.add(**board_data)
db.session.commit()
@classmethod
def update_session(cls):
for component, value in request.form.items():
if component.startswith(cls._ID_CHAR):
field = component[1:component.rfind('_')]
i = int(component[component.rfind('_')+1:])
session[cls._SESSION_KEY][i][field] = value
pprint(session[cls._SESSION_KEY])
@classmethod
def delete_sensor(cls):
# there was any btn_delete_sensor click?
for key in request.form:
if 'btn_delete_sensor' in key:
del session[cls._SESSION_KEY][int(key[key.rfind('_')+1:])]
break
@classmethod
def init_session(cls):
session[cls._SESSION_KEY] = []
for s in current_app.config['DB_INIT']['SUPPORTED_SENSORS']:
session[cls._SESSION_KEY].append(s.copy())
class MyFileField(FileInput):
......
......@@ -28,7 +28,7 @@ from ..models import *
from .forms import *
from ..email import send_email
from flask import Blueprint, url_for, render_template, \
from flask import Blueprint, url_for, render_template, abort, \
request, abort, redirect, Response, flash, session,\
send_from_directory, after_this_request
......@@ -59,8 +59,8 @@ def index():
if request.method == 'POST':
return BoardSelectionForm.handle_post()
return render_template('index.html', config = config,
board_selection_form = BoardSelectionForm())
return render_template('index.html',
board_selection_form=BoardSelectionForm())
@main.route('/board/<int:id>', methods=['GET', 'POST'])
......@@ -73,8 +73,8 @@ def view_board(id):
if request.method == 'POST':
return BoardSelectionForm.handle_post(board)
return render_template('viewboard.html', config = config, board = board,
board_selection_form = BoardSelectionForm(selected_id=board.id))
return render_template('viewboard.html', board=board,
board_selection_form=BoardSelectionForm(selected_id=board.id))
@main.route('/board/<int:id>/<string:sensor_nickname>',
......@@ -91,8 +91,7 @@ def view_board_sensor(id, sensor_nickname):
##TODO: improve loading system (data interpolation maybe?)
return render_template('viewsensor.html', config = config, board = board,
the_sensor = sensor,
return render_template('viewsensor.html', board=board, the_sensor=sensor,
board_selection_form = BoardSelectionForm(selected_id=board.id))
......@@ -101,32 +100,33 @@ def view_board_sensor(id, sensor_nickname):
def view_insert_board():
''' GET --> templates/insertboard.html
POST | btn_cancel --> main.index
| btn_save --> main.view_board (new board created)
| btn_save --> main.view_board (new board)
| btn_add_sensor --> refresh (sensor list management)
| btn_delete_sensor --> refresh (sensor list management)
'''
##TODO: maybe its better to do the list managment on client side?
manage_board_form = ManageBoardForm()
pprint(request.form)
if request.method == 'POST':
# got any button click? if so, the btn will appear on the request.form
SensorListForms.update_session()
if 'btn_cancel' in request.form:
return redirect(url_for('main.index'))
elif 'btn_save' in request.form:
if SensorListForms.validate_and_save():
board = manage_board_form.validate_and_save()
print('THE NEW BOARD:', board)
if board:
return redirect(url_for('main.view_board', id=board.id))
elif 'btn_add_sensor' in request.form:
SensorListForms.add_sensor()
manage_board_form.add_sensor()
else:
SensorListForms.delete_sensor()
manage_board_form.delete_sensor()
else:
SensorListForms.init_session()
manage_board_form.init_session()
return render_template('insertboard.html', board_form=ManageBoardForm(),
sensor_list_forms=SensorListForms.generate_forms())
return render_template('insertboard.html', form=manage_board_form)
@main.route('/board/manage/<int:id>', methods=['GET', 'POST'])
......@@ -249,24 +249,21 @@ def view_clean_board(id):
@main.route('/board/delete/<int:id>')
@login_required
def view_delete_board(id):
''' GET: performs db delete of the board, flash msg and redirect to index
This view exists as an alternative for the /api/board/delete that, beside
performing the database deletion, also redirects to the index page with
a flashed informative message.
def delete_board(id):
''' GET: --> main.index
'''
board = Board.get(id)
if board is None:
if board is None or board.user_id != current_user.id:
abort(400)
nickname = board.nickname
Board.delete(id)
db.session.commit()
flash("A estação <id={} nickname='{}'> foi completamente removida!"
.format(id, nickname))
flash("Sua estação '{}' (id={}) foi completamente removida!"
.format(nickname, id))
return redirect(url_for('.index'))
return redirect(url_for('main.index'))
@main.route('/rawsensordata/importcsv/<int:board_id>', methods=['GET', 'POST'])
......
/*!
* bootstrap-vertical-tabs - v1.2.1
* https://dbtek.github.io/bootstrap-vertical-tabs
* 2014-11-07
* Copyright (c) 2014 İsmail Demirbilek
* License: MIT
*/
.tabs-left, .tabs-right {
border-bottom: none;
padding-top: 2px;
}
.tabs-left {
border-right: 1px solid #ddd;
}
.tabs-right {
border-left: 1px solid #ddd;
}
.tabs-left>li, .tabs-right>li {
float: none;
margin-bottom: 2px;
}
.tabs-left>li {
margin-right: -1px;
}
.tabs-right>li {
margin-left: -1px;
}
.tabs-left>li.active>a,
.tabs-left>li.active>a:hover,
.tabs-left>li.active>a:focus {
border-bottom-color: #ddd;
border-right-color: transparent;
}
.tabs-right>li.active>a,
.tabs-right>li.active>a:hover,
.tabs-right>li.active>a:focus {
border-bottom: 1px solid #ddd;
border-left-color: transparent;
}
.tabs-left>li>a {
border-radius: 4px 0 0 4px;
margin-right: 0;
display:block;
}
.tabs-right>li>a {
border-radius: 0 4px 4px 0;
margin-right: 0;
}
.sideways {
margin-top:50px;
border: none;
position: relative;
}
.sideways>li {
height: 20px;
width: 120px;
margin-bottom: 100px;
}
.sideways>li>a {
border-bottom: 1px solid #ddd;
border-right-color: transparent;
text-align: center;
border-radius: 4px 4px 0px 0px;
}
.sideways>li.active>a,
.sideways>li.active>a:hover,
.sideways>li.active>a:focus {
border-bottom-color: transparent;
border-right-color: #ddd;
border-left-color: #ddd;
}
.sideways.tabs-left {
left: -50px;
}
.sideways.tabs-right {
right: -50px;
}
.sideways.tabs-right>li {
-webkit-transform: rotate(90deg);
-moz-transform: rotate(90deg);
-ms-transform: rotate(90deg);
-o-transform: rotate(90deg);
transform: rotate(90deg);
}
.sideways.tabs-left>li {
-webkit-transform: rotate(-90deg);
-moz-transform: rotate(-90deg);
-ms-transform: rotate(-90deg);
-o-transform: rotate(-90deg);
transform: rotate(-90deg);
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
.bs-example{
margin: 20px;
}
/* navbar
.navbar-default {
background-color: #557ec8;
border-color: #E7E7E7;
} */
/* title
.navbar-default .navbar-brand {
color: #eee;
font-weight: bold;
}
.navbar-default .navbar-brand:hover,
.navbar-default .navbar-brand:focus {
color: #5E5E5E;
} */
/* link
.navbar-default .navbar-nav > li > a {
color: #fff;
}
.navbar-default .navbar-nav > li > a:hover,
.navbar-default .navbar-nav > li > a:focus {
color: #bbb;
} */
This source diff could not be displayed because it is too large. You can view the blob instead.
This diff is collapsed.
This diff is collapsed.
function bb_confirm(message, dest_url)
{
bootbox.confirm(message,
function(result)
{
if (result)
window.location.replace(dest_url);
});
}
......@@ -9,16 +9,18 @@
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<hr><br>
<p>Novo usuário?
<a href="{{ url_for('auth.register') }}"
>Clique aqui para se registrar!</a>
</p>
<br>
<p>Esqueceu a senha?
<a href="{{ url_for('auth.ask_password_reset') }}"
>Clique aqui para solicitar alteração.</a>
</p>
</p>
<hr><br>
<p>Novo usuário?</p>
<button type="reset" class="btn btn-default"
onclick="location.href='{{ url_for('auth.register') }}';">
<span class="glyphicon glyphicon-user"></span>
<span>Registre-se</span>
</button>
</div>
</div>
{% endblock %}
......@@ -14,10 +14,11 @@
você precisa confirmar a sua conta.
Confira a caixa de entrada do seu email cadastrado:<br><br>
{{ current_user.email }}
<b>{{ current_user.email }}</b>
<br><br>
onde você deverá ter recebido um email com o link de confirmação.
</p>
<hr>
<p>
Precisa de um novo link para confirmação?
<a href="{{ url_for('auth.resend_confirmation') }}">Clique aqui!</a>
......
......@@ -5,32 +5,25 @@
{% block head %}
{{ super() }}
<style type="text/css">
.bs-example{
margin: 20px;
}
</style>
<script type="text/javascript">
function storePagePosition()
{
// save current window Y offset for later use
localStorage.setItem("page_y", window.pageYOffset);
}
window.addEventListener("scroll", storePagePosition);
window.onload = function ()
{
// reload window Y scroll offset saved previously
try {
var page_y = localStorage.getItem("page_y");
if (page_y === undefined) page_y = 0;
window.scrollTo(0, page_y);
} catch (e) {}
}
</script>
<!-- <script type="text/javascript"> -->
<!-- function storePagePosition() -->
<!-- { -->
<!-- // save current window Y offset for later use -->
<!-- localStorage.setItem("page_y", window.pageYOffset); -->
<!-- } -->
<!-- window.addEventListener("scroll", storePagePosition); -->
<!-- window.onload = function () -->
<!-- { -->
<!-- // reload window Y scroll offset saved previously -->
<!-- try { -->
<!-- var page_y = localStorage.getItem("page_y"); -->
<!-- if (page_y === undefined) page_y = 0; -->
<!-- window.scrollTo(0, page_y); -->
<!-- } catch (e) {} -->
<!-- } -->
<!-- </script> -->
<!--
......@@ -75,9 +68,41 @@ window.onload = function ()
<!-- } -->
<!-- </script> -->
{% endblock %}
<!-- ===================================================================== -->
{% block styles %}
<link rel="stylesheet"
href="{{url_for('static', filename='css/bootstrap.min.css')}}">
<link rel="stylesheet"
href="{{url_for('static', filename='css/main.css')}}">
{% endblock %}
<!-- ===================================================================== -->
{% block scripts %}
<!-- required for bootbox-->
<!-- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script> -->
<script src="{{ url_for('static', filename='js/jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootbox.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% endblock %}
<!-- ===================================================================== -->
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="navbar navbar-default" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle"
......@@ -108,7 +133,7 @@ window.onload = function ()
<li><a href="#">Editar estação</a></li>
<li><a href="#">Apagar todos os dados</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Excluir estação</a></li>
<li><a href="javascript:void(0)" onclick="confirm_delete_board();" >Excluir estação</a></li>
{% endif %}
</ul>
</li>
......@@ -128,20 +153,22 @@ window.onload = function ()
<ul class="dropdown-menu">
<li><a href={{ url_for('auth.show_user_data') }}
>Meus dados</a></li>
<li role="separator" class="divider"></li>
<li role="separator" class="divider"></li>
<li><a href="{{ url_for('auth.change_email') }}"
>Alterar e-mail</a></li>
<li><a href="{{ url_for('auth.change_password') }}"
>Alterar senha</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Encerrar conta</a></li>
<li><a href="javascript:void(0)"
onclick="javascript:bb_confirm('{{ config.MESSAGES.CONFIRM_DELETE_BOARD }}',
'{{ url_for('main.index') }}')">Encerrar conta</a></li>
<li role="separator" class="divider"></li>
<li><a href="{{ url_for('auth.logout') }}"
>Sair</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Entrar</a></li>
<li><a href="{{ url_for('auth.login') }}">Entrar/Registrar-se</a></li>
{% endif %}
</ul>
</div>
......@@ -149,6 +176,8 @@ window.onload = function ()
</div>
{% endblock %}
<!-- ===================================================================== -->
{% block content %}
{% for message in get_flashed_messages() %}
......
......@@ -7,31 +7,48 @@
<form id="bootstrapSelectForm" method='POST' class="form-horizontal"
enctype=multipart/form-data>
<div class="form-group">
{{ board_form.nickname.label(class="col-xs-3 control-label") }}
{% if form.errors %}
<ul class="errors">
{% for field_name, field_errors in form.errors|dictsort if field_errors %}
{% for error in field_errors %}
<li>{{ form[field_name].label }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
{% endif %}
{{ form.csrf_token }}