Commit 545d69c8 authored by Nelso Jost's avatar Nelso Jost

NEW: refactor for better maintainence

parent 40286137
......@@ -24,6 +24,11 @@ from flask.ext.bootstrap import Bootstrap
from flask.ext.login import LoginManager
from flask.ext.mail import Mail
import json
import os
def path_here(x):
return os.path.join(os.path.abspath(os.path.dirname(__file__)), x)
db = SQLAlchemy()
bootstrap = Bootstrap()
mail = Mail()
......@@ -42,6 +47,9 @@ def create_app(config_name):
app.config.from_object(config[config_name])
config[config_name].init_app(app)
with open(path_here('../data/DB_INIT.json')) as f:
app.config['DB_INIT'] = json.load(f)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
......
......@@ -106,8 +106,9 @@ class AskPasswordResetForm(Form):
class UserDataForm(Form):
user_hash = MyStringField('User Hash', htmlattr={'readonly': True})
user_hash = MyStringField('User Hash (utilizado para autenticar na API. '
'Informação sensível, não divulgar!)', htmlattr={'readonly': True})
def __init__(self):
super().__init__()
self.user_hash.data = current_user.user_hash
self.user_hash.data = current_user.password_hash
......@@ -184,6 +184,7 @@ def ask_password_reset():
return render_template('auth/askpasswordreset.html', form=form)
@auth.route('/userdata', methods=['GET'])
@login_required
def show_user_data():
form = UserDataForm()
......
......@@ -5,7 +5,7 @@
#-------------------------------------------------------------------------------
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
host = '0.0.0.0'
......@@ -51,55 +51,12 @@ config = {'dev': DevelopmentConfig,
'prod': ProductionConfig,
'default': DevelopmentConfig}
class DATETIME:
INTERNAL_FORMAT = "%Y%m%d%H%M%S"
PRINT_FORMAT = "%Y-%m-%d %H:%M:%S"
# [app] -- include js client code to peform page reload periodically
# BEWARE: may overload server connection
class AUTO_PAGE_RELOAD:
active = False
interval = 2000
class DB_ENUMS:
PHYSICAL_QUANTITIES = ['Temperatura', 'Pressão', 'Luminosidade', 'Umidade']
DATA_FORMATS = ['INTEGER', 'FLOAT', 'STRING']
MEASURED_OBJECT = ['ar', 'solo', 'ambiente']
DEFAULT_SENSORS = [
{
'nickname': 'DHT22_TEMP',
'quantity': 'Temperatura',
'measured_object': 'ar',
'unity': 'ºC',
'data_format': 'FLOAT',
'description': 'https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf'
},
{
'nickname': 'DHT22_AH',
'quantity': 'Umidade',
'measured_object': 'ar',
'unity': '%',
'data_format': 'FLOAT',
'description': 'https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf'
},
{
'nickname': 'BMP085_PRESSURE',
'quantity': 'Pressão',
'measured_object': 'ar',
'unity': 'Pa',
'data_format': 'FLOAT',
'description': 'http://www.adafruit.com/datasheets/BMP085_DataSheet_Rev.1.0_01July2008.pdf'
},
{
'nickname': 'LDR',
'quantity': 'Luminosidade',
'measured_object': 'ambiente',
'unity': '%',
'data_format': 'FLOAT',
'description': 'http://www.biltek.tubitak.gov.tr/gelisim/elektronik/dosyalar/40/LDR_NSL19_M51.pdf'
},]
MSG_CONFIRM_DELETE =\
"""ATENÇÃO! Todos os dados relativos a essa estação serão perdidos!!!\
......
......@@ -9,13 +9,14 @@ def send_async_email(app, msg):
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(config.MAIL_SUBJECT_PREFIX + subject,
sender = config.MAIL_SENDER,
app = current_app._get_current_object()
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject,
sender = app.config['MAIL_SENDER'],
recipients = [to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
app = current_app._get_current_object()
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
......@@ -2,16 +2,19 @@ from flask.ext.wtf import Form
from wtforms import SelectField, FileField, SubmitField, StringField, \
TextAreaField, BooleanField
from wtforms.widgets import FileInput
# from wtforms.validators import Required
from wtforms.validators import Required, Length, Regexp, EqualTo
from flask import session, request, redirect, url_for
from flask import session, request, redirect, url_for, current_app
from flask.ext.login import current_user
from ..models import *
from pprint import pprint
from .. import config
from .. import db
from ..models import *
from ..utils import ClassProperty
__all__ = ['BoardSelectionForm', 'ManageBoardForm', 'SensorForm', 'MyFileField',
__all__ = ['BoardSelectionForm', 'ManageBoardForm', 'SensorListForms',
'MyFileField',
'ImportCSVForm', 'ExportCSVForm']
......@@ -77,16 +80,95 @@ class ManageBoardForm(Form):
description = TextAreaField('Descrição')
class SensorForm(Form):
nickname = StringField('Apelido')
quantity = SelectField('Grandeza Física',
choices=[('', ' ')] + [(v, v) for v in config.DB_ENUMS.PHYSICAL_QUANTITIES])
measured_object = SelectField('Objeto medido',
choices=[('', ' ')] + [(v, v) for v in config.DB_ENUMS.MEASURED_OBJECT])
data_format = SelectField('Formato de dados',
choices=[('', ' ')] + [(v, v) for v in config.DB_ENUMS.DATA_FORMATS])
unity = StringField('Unidade')
class SensorListForms(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, "
"ou sublinhados.")])
description = TextAreaField('Descrição')
measurement_name = SelectField('Medição', coerce=str)
unity_label = SelectField('Unidade', coerce=str)
nickname_has_focus = False
_SESSION_KEY = 'sensor_list_manager'
_ID_CHAR = '@'
def __init__(self, **kwargs):
super().__init__()
self.nickname.data = kwargs.get('nickname', '')
self.description.data = kwargs.get('description', '')
self.measurement_name.choices = [('', ' ')] + \
[(m.name, m.label_br) for m in Measurement.query.all()]
self.measurement_name.data = kwargs.get('measurement_name', -1)
self.update_unity_label(kwargs.get('unity_label', None))
@ClassProperty
@classmethod
def empty_json(cls):
return {'nickname': '', 'measurement_name': '', 'unity_label': '',
'description': ''}
def update_unity_label(self, value=None):
if self.measurement_name.data != '':
self.unity_label.choices = [
(um.unity.label, um.unity.label) for um in
Measurement.query.filter_by(name = self.measurement_name.data)
.first().unity_measurements.all()]
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):
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):
......
......@@ -32,7 +32,7 @@ from flask import Blueprint, url_for, render_template, \
request, abort, redirect, Response, flash, session,\
send_from_directory, after_this_request
from flask.ext.login import login_required, current_user
from flask.ext.login import login_required, current_user, current_app
import csv
import os
......@@ -42,47 +42,6 @@ import json
from . import main
def init_sensor_forms(sensors, nickname_index_focus=None, include_stats=False):
''' Creates a list of NewSensorForm objects for each sensor json data on
'sensors' parameter. If provided, 'set_focus' must be an integer that
represent a valid index on the sensor list (-1 means 'last').
'''
all_forms = []
f = None
for i, sensor in enumerate(sensors):
if sensor['board_id'] is None:
continue
f = SensorForm()
f.index = i
f.nickname.data = sensor['nickname']
f.nickname.name = '@nickname_' + str(i)
f.nickname.has_focus = False
if nickname_index_focus == i:
f.nickname.has_focus = True
f.quantity.data = sensor['quantity']
f.quantity.name = '@quantity_' + str(i)
f.measured_object.data = sensor['measured_object']
f.measured_object.name = '@measured_object_' + str(i)
f.unity.data = sensor['unity']
f.unity.name = '@unity_' + str(i)
f.data_format.data = sensor['data_format']
f.data_format.name = '@data_format_' + str(i)
f.description.data = sensor['description']
f.description.name = '@description_' + str(i)
if include_stats:
f.stats = {'points_count': Sensor.get(sensor['id']).rawdata.count()}
all_forms.append(f)
if nickname_index_focus == -1 and f:
f.nickname.has_focus = True
return all_forms
def update_session_sensors():
for component, value in request.form.items():
if component.startswith('@'):
field = component[1:component.rfind('_')]
i = int(component[component.rfind('_')+1:])
session['sensors'][i][field] = value
def update_need_refresh():
main.need_refresh = False
config.AUTO_PAGE_RELOAD.url_need_refresh = url_for('.check_need_refresh')
......@@ -95,7 +54,7 @@ def check_need_refresh():
@main.route('/', methods=['GET', 'POST'])
def index():
''' GET --> templates/index.html
POST --> handle_post_board_selection_form()
POST --> main.view_board()
'''
if request.method == 'POST':
return BoardSelectionForm.handle_post()
......@@ -118,7 +77,8 @@ def view_board(id):
board_selection_form = BoardSelectionForm(selected_id=board.id))
@main.route('/board/<int:id>/<string:sensor_nickname>', methods=['GET', 'POST'])
@main.route('/board/<int:id>/<string:sensor_nickname>',
methods=['GET', 'POST'])
def view_board_sensor(id, sensor_nickname):
''' GET --> templates/viewsensor.html (uses Dygraph.js)
POST --> view_board(id = form_board_select.board_id.data)
......@@ -129,14 +89,10 @@ def view_board_sensor(id, sensor_nickname):
if request.method == 'POST':
return BoardSelectionForm.handle_post(board)
sensor_json = sensor.to_json
sensor_json['dygraph_data'] = sensor.get_dygraph_data()
sensor_json['rawdata_count'] = sensor.rawdata.count()
##TODO: improve loading system (data interpolation maybe?)
return render_template('viewsensor.html', config = config, board = board,
the_sensor = sensor_json,
the_sensor = sensor,
board_selection_form = BoardSelectionForm(selected_id=board.id))
......@@ -144,65 +100,33 @@ def view_board_sensor(id, sensor_nickname):
@login_required
def view_insert_board():
''' GET --> templates/insertboard.html
POST --> cancel/save; add/remove sensors
The rendering is very similar to the view_manage_board(), except that
this new board still has no data on its sensors, so no alerts will be
issued when removing them.
POST | btn_cancel --> main.index
| btn_save --> main.view_board (new board created)
| 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?
sensor_list_was_edited = False
removed_sensor_index = None
focus_nick = None
if request.method == 'POST':
# got any button click?
# if yes, the button name will appear in request.form
# 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'))
if 'btn_save' in request.form:
update_session_sensors()
board_data = {k: v for k, v in request.form.items()
if hasattr(Board, k)}
board_data['sensors'] = session['sensors']
board = Board.add(**board_data)
board.user = current_user
db.session.commit()
return redirect(url_for('main.view_board', id=board.id))
elif 'btn_save' in request.form:
if SensorListForms.validate_and_save():
return redirect(url_for('main.view_board', id=board.id))
if 'btn_add_sensor' in request.form:
session['sensors'].append(Sensor.get_json_skel(board_id=0))
sensor_list_was_edited = True
focus_nick = -1 # last one
update_session_sensors()
elif 'btn_add_sensor' in request.form:
SensorListForms.add_sensor()
else:
# look for any 'btn_delete_sensor_XX' in the request form dict
# where XX is the index of the sensor to be removed
for key in request.form:
if 'btn_delete_sensor' in key:
i = int(key[key.rfind('_')+1:])
update_session_sensors()
del session['sensors'][i]
sensor_list_was_edited = True
focus_nick = i - 1
break
form = ManageBoardForm()
form.set_nickname_focus = not sensor_list_was_edited
if not sensor_list_was_edited:
session['sensors'] = []
for d in config.DEFAULT_SENSORS:
x = d.copy()
x.update(id=None, board_id=0)
session['sensors'].append(x)
SensorListForms.delete_sensor()
else:
SensorListForms.init_session()
return render_template('insertboard.html', config=config, form=form,
sensor_forms=init_sensor_forms(session['sensors'],
focus_nick, include_stats=False))
return render_template('insertboard.html', board_form=ManageBoardForm(),
sensor_list_forms=SensorListForms.generate_forms())
@main.route('/board/manage/<int:id>', methods=['GET', 'POST'])
......
......@@ -77,7 +77,7 @@ USAGE
'''
from .board import Board
from .sensor import Sensor
from .sensor import Sensor, Unity, Measurement, UnityMeasurement
from .rawsensordata import RawSensorData
from .users import User
......
......@@ -2,7 +2,7 @@ from .. import db
from .dbutils import DBUtils
import csv
from .sensor import Sensor
from .sensor import Sensor, Unity, Measurement, UnityMeasurement
from flask import abort
......@@ -35,11 +35,7 @@ class Board(db.Model, DBUtils):
Returns the newly created Board object.
'''
sensors = None
if 'sensors' in kwargs:
sensors = kwargs['sensors']
del kwargs['sensors']
sensors = kwargs.pop('sensors', None)
board = cls(**kwargs)
db.session.add(board)
......@@ -78,6 +74,16 @@ class Board(db.Model, DBUtils):
"You should confirm by calling db.session.commit()")
if 'board_id' in kwargs:
del kwargs['board_id']
if 'unity_label' in kwargs and 'measurement_name' in kwargs:
u = Unity.get(label=kwargs.pop('unity_label'))
m = Measurement.get(name=kwargs.pop('measurement_name'))
kwargs['unity_measurement_id'] = UnityMeasurement.get(
unity_id=u.id, measurement_id=m.id).id
else:
raise KeyError("Missing 'unity_label' and/or 'measurement_name' "
"keys")
sensor = Sensor(board_id=self.id, **kwargs)
db.session.add(sensor)
return sensor
......
from .. import db
from ..utils import ClassProperty
from flask import abort
class ClassProperty(property):
def __get__(self, cls, owner):
return self.fget.__get__(None, owner)()
class DBUtils:
'''
Implements several short-named methods and class properties for more
......@@ -12,14 +9,14 @@ class DBUtils:
'''
@classmethod
def get(cls, *args):
def get(cls, *args, **kwargs):
''' Retrives a specific record if args consists of an int primary key,
or perform a SQL Alchemy query filter with the given args.
'''
if len(args) == 1 and isinstance(args[0], int):
return cls.query.get_or_404(args[0])
else:
return cls.query.filter(*args).first_or_404()
return cls.query.filter_by(**kwargs).first_or_404()
@ClassProperty
@classmethod
......@@ -50,5 +47,7 @@ class DBUtils:
'''
return {field: getattr(self, field) for field in self.__fields__}
@classmethod
def import_from_json(cls, *args):
for json_data in args:
cls.add(**json_data)
......@@ -4,7 +4,7 @@ from .dbutils import DBUtils
from datetime import datetime
class RawSensorData(db.Model, DBUtils):
__tablename__ = 'rawsensordata'
id = db.Column(db.Integer, primary_key=True)
......
......@@ -4,6 +4,68 @@ from .rawsensordata import RawSensorData
import csv
class Unity(db.Model, DBUtils):
__tablename__ = 'unities'
id = db.Column(db.Integer, primary_key=True)
label = db.Column(db.String(10), unique=True, nullable=False)
latex_label = db.Column(db.String(150), unique=True)
unity_measurements = db.relationship('UnityMeasurement', backref='unity',
lazy='dynamic')
def __repr__(self):
return "<Unity id={} label='{}'>".format(self.id, self.label)
class Measurement(db.Model, DBUtils):
__tablename__ = 'measurements'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True, nullable=False)
label_br = db.Column(db.String(50), unique=True, nullable=False)
label_en = db.Column(db.String(50), unique=True, nullable=False)
unity_measurements = db.relationship('UnityMeasurement',
backref='measurement',
lazy='dynamic')
def __repr__(self):
return "<Measurement id={} name='{}' unities={}>".format(
self.id, self.name, str([um.unity.label for um in
self.unity_measurements.all()]))
@classmethod
def add(cls, **kwargs):
unities = kwargs.pop('unities', None)
measurement = cls(**kwargs)
db.session.add(measurement)
if unities:
db.session.flush()
for u in unities:
UnityMeasurement.add(measurement_id=measurement.id,
unity_id=Unity.get(label=u).id)
return measurement
class UnityMeasurement(db.Model, DBUtils):
__tablename__ = 'unitymeasurements'
id = db.Column(db.Integer, primary_key=True)
measurement_id = db.Column(db.Integer, db.ForeignKey('measurements.id'),
nullable=False)
unity_id = db.Column(db.Integer, db.ForeignKey('unities.id'),
nullable=False)
sensors = db.relationship('Sensor', backref='unity_measurement',
lazy='dynamic')
def __repr__(self):
return "<UnityMeasurement id={} measurement='{}' unity='{}'>".format(
self.id, self.measurement.name, self.unity.label)
class Sensor(db.Model, DBUtils):
__tablename__ = 'sensors'
......@@ -11,18 +73,20 @@ class Sensor(db.Model, DBUtils):
id = db.Column(db.Integer, primary_key=True)
board_id = db.Column(db.Integer, db.ForeignKey('boards.id'))
nickname = db.Column(db.String(20))
quantity = db.Column(db.String(30))
measured_object = db.Column(db.String(30))
data_format = db.Column(db.String(10))
unity = db.Column(db.String(20))
description = db.Column(db.String(300))
unity_measurement_id = db.Column(db.Integer,
db.ForeignKey('unitymeasurements.id'),
nullable=False)
rawdata = db.relationship('RawSensorData', backref='sensor',
lazy='dynamic')
def __repr__(self):
return "<Sensor id={} board_id={} nickname='{}' data_count={}>".format(
self.id, self.board_id, self.nickname, self.rawdata.count())
return "<Sensor id={} board_id={} nickname='{}' "\
"unity='{}' rawdata_count={}>".format(
self.id, self.board_id, self.nickname,
self.unity_measurement.unity.label,
self.rawdata.count())
def add_rawdata(self, datetime, value, avoid_duplication=False):
''' Add raw sensor data. RawSensorData fields are given by kwargs.
......@@ -62,15 +126,6 @@ class Sensor(db.Model, DBUtils):
setattr(self, field, json_data.get(field))
db.session.add(self)
@classmethod
def get_json_skel(cls, **kwargs):
d = {'id': None, 'board_id': None, 'nickname': '', 'quantity': '',
'unity': '', 'data_format': '', 'description': ''}
for k, v in kwargs.items():
if k in d:
d[k] = v
return d
def export_csv_file(self, filename, sep=','):
with open(filename, 'w') as f:
writer = csv.writer(f, delimiter=sep)
......
......@@ -42,10 +42,6 @@ class User(UserMixin, db.Model, DBUtils):
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
@property
def user_hash(self):
return generate_password_hash(self.password_hash + self.username)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id})
......
/*!
* 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;