Commit ba8017e1 authored by Nelso Jost's avatar Nelso Jost

NEW: added user control system

parent ec60eb6f
......@@ -21,9 +21,16 @@ This approach has several advantages:
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.bootstrap import Bootstrap
from flask.ext.login import LoginManager
from flask.ext.mail import Mail
db = SQLAlchemy()
bootstrap = Bootstrap()
mail = Mail()
login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
def create_app():
''' Creates the Flask application object and register configurations,
......@@ -37,7 +44,12 @@ def create_app():
from .views import main as main_blueprint
app.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
db.init_app(app)
bootstrap.init_app(app)
login_manager.init_app(app)
mail.init_app(app)
return app
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
from flask.ext.wtf import Form
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import Required, Email, Length, Regexp, EqualTo
from wtforms import ValidationError
from ..models import *
from flask.ext.login import current_user
class MyStringField(StringField):
def __init__(self, label, **kwargs):
self._htmlattr = dict(kwargs.pop('htmlattr', {}))
super().__init__(label, **kwargs)
def __call__(self, **kwargs):
kwargs.update(**self._htmlattr)
return super().__call__(**kwargs)
class MyPasswordField(PasswordField):
def __init__(self, label, **kwargs):
self._htmlattr = dict(kwargs.pop('htmlattr', {}))
super().__init__(label, **kwargs)
def __call__(self, **kwargs):
kwargs.update(**self._htmlattr)
return super().__call__(**kwargs)
class LoginForm(Form):
email = MyStringField('Email', htmlattr=dict(autofocus=True),
validators=[Required(), Length(1, 64), Email()])
password = PasswordField('Senha', validators=[Required()])
remember_me = BooleanField('Mantenha-me conectado')
submit = SubmitField('Entrar')
class RegistrationForm(Form):
email = MyStringField('Email', htmlattr=dict(autofocus=True),
validators=[Required(), Length(1, 64), Email()])
username = StringField('Nome de usuário', validators=[
Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
"Nomes de usuário devem possuir apenas letras, números, "
"pontos ou sublinhados.")])
password = PasswordField('Senha', validators=[
Required(), EqualTo('password2', message='Senhas devem ser iguais!')])
password2 = PasswordField('Confirmar senha', validators=[Required()])
submit = SubmitField('Registrar')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email já cadastrado!')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Nome de usuário já existe!')
class ChangeEmailForm(Form):
current_email = MyStringField('Email atual',
htmlattr=dict(readonly=True))
new_email = MyStringField('Novo Email', htmlattr=dict(autofocus=True),
validators=[Required(), Length(1, 64), Email(),
EqualTo('confirm_new_email', message='Emails devem ser iguais!')])
confirm_new_email = StringField('Confirmar Email', validators=[
Required(), Length(1, 64), Email()])
submit = SubmitField('Enviar')
def __init__(self):
super().__init__()
if hasattr(current_user, 'email'):
self.current_email.data = current_user.email
def validate_new_email(self, field):
if User.query.filter_by(email=field.data).first():
raise ValidationError('Email já cadastrado!')
class ChangePasswordForm(Form):
new_password = MyPasswordField('Nova senha', validators=[Required(),
EqualTo('confirm_password', message='Senhas devem ser iguais!')],
htmlattr=dict(autofocus=True))
confirm_password = PasswordField('Confirmar senha',
validators=[Required()])
submit = SubmitField('Enviar')
class AskPasswordResetForm(Form):
email = MyStringField('Email', htmlattr=dict(autofocus=True))
submit = SubmitField('Enviar')
def validate_email(self, field):
if not User.query.filter_by(email=field.data).first():
raise ValidationError('Email não cadastrado!')
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 ..models import *
from .forms import *
from .. import db
from ..email import send_email
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
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):
login_user(user, form.remember_me.data)
if 'board_id' in session:
return redirect(url_for('main.view_board',
id=session['board_id']))
return redirect(request.args.get('next') or url_for('main.index'))
flash('Email ou senha inválidos!')
return render_template('auth/login.html', form=form)
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('Você encerrou a sua sessão de usuário!')
if 'board_id' in session:
return redirect(url_for('main.view_board', id=session['board_id']))
return redirect(request.args.get('next') or url_for('main.index'))
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data,
username=form.username.data,
password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirme sua Conta',
'mail/confirm', user=user, token=token)
flash("Um email de confirmação de conta foi enviado para {}. "
"Acesse a conta de email e clique no link enviado para ativar "
"sua conta.".format(user.email))
return redirect(url_for('main.index'))
return render_template('auth/register.html', form=form)
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
flash('Sua conta foi confirmada! Obrigado!')
else:
flash('O link de confirmação de conta expirou!')
return redirect(url_for('main.index'))
@auth.before_app_request
def before_request():
if current_user.is_authenticated() \
and not current_user.confirmed \
and request.endpoint[:5] != 'auth.':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous() or current_user.confirmed:
return redirect('main.index')
return render_template('auth/unconfirmed.html')
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(to=current_user.email, subject='Confirme sua Conta',
template='mail/confirm', user=current_user, token=token)
flash('Um novo email de confirmação de conta foi enviado para {}'.
format(current_user.email))
return redirect(url_for('main.index'))
@auth.route('/changemail', methods=['GET', 'POST'])
@login_required
def change_email():
form = ChangeEmailForm()
if form.validate_on_submit():
current_user.email = form.new_email.data
current_user.confirmed = False
db.session.add(current_user)
db.session.commit()
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirme sua Conta',
'mail/confirm', user=current_user, token=token)
flash("Um email de confirmação de conta foi enviado para {}. "
"Acesse este email e clique no link enviado para ativar "
"sua conta.".format(current_user.email))
return redirect(url_for('main.index'))
return render_template('auth/changemail.html', form=form)
@auth.route('/changepassword/', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
current_user.password = form.new_password.data
db.session.add(current_user)
db.session.commit()
flash("Sua senha foi alterada com sucesso!")
return redirect(url_for('main.index'))
return render_template('auth/changepassword.html', form=form)
@auth.route('/resetpassword/<token>', methods=['GET', 'POST'])
def reset_password(token):
user_id = User.validate_token(token)
if user_id is not None:
form = ChangePasswordForm()
if form.validate_on_submit():
user = User.get(user_id)
user.password = form.new_password.data
db.session.add(user)
db.session.commit()
flash("Sua senha foi alterada com sucesso!")
return redirect(url_for('auth.login'))
return render_template('auth/changepassword.html', form=form)
else:
flash('O link de confirmação de alteração de senha expirou!')
return redirect(url_for('main.index'))
@auth.route('/askpasswordreset', methods=['GET', 'POST'])
def ask_password_reset():
form = AskPasswordResetForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
token = user.generate_confirmation_token()
send_email(user.email, 'Alteração de senha',
'mail/resetpwd', user=user, token=token)
flash("Um email de confirmação foi enviado para {}. "
"Acesse a menssagem e clique no link enviado para prosseguir."
.format(user.email))
return redirect(url_for('auth.login'))
return render_template('auth/askpasswordreset.html', form=form)
......@@ -25,12 +25,23 @@ SQLALCHEMY_COMMIT_ON_TEARDOWN = True
UPLOAD_FOLDER = 'upload'
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 587
MAIL_USE_SSL = False
MAIL_USE_TLS = True
MAIL_USERNAME = 'cta.emm.web'
MAIL_PASSWORD = os.environ['EMM_ADMIN_PWD']
FLASK_MAIL_SUBJECT_PREFIX = '[CTA-EMM-WEB] '
FLASK_MAIL_SENDER = 'CTA-EMM-WEB <cta.emm.web@gmail.com>'
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
# ATTENTION: may overload server connection
# BEWARE: may overload server connection
class AUTO_PAGE_RELOAD:
active = False
interval = 2000
......@@ -70,6 +81,10 @@ DEFAULT_SENSORS = [
'quantity': 'Luminosidade',
'measured_object': 'ambiente',
'unity': '%',
'data_format': 'INTEGER',
'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!!!\
<br><br>Deseja mesmo prosseguir?"""
from . import config, mail
from flask.ext.mail import Message
from flask import render_template, current_app
from threading import Thread
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
msg = Message(config.FLASK_MAIL_SUBJECT_PREFIX + subject,
sender = config.FLASK_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
from flask.ext.wtf import Form
from wtforms import SelectField, FileField, SubmitField, StringField, \
TextAreaField
TextAreaField, BooleanField
from wtforms.widgets import FileInput
# from wtforms.validators import Required
from flask import session, request, redirect, url_for
from flask.ext.login import current_user
from .models import *
from . import config
class SelectBoardForm(Form):
board_choices = SelectField('Estação:', coerce=int)
__all__ = ['BoardSelectionForm', 'ManageBoardForm', 'SensorForm', 'MyFileField',
'ImportCSVForm', 'ExportCSVForm']
class BoardSelectionForm(Form):
board_id = SelectField('Estações', coerce=int)
only_user = BooleanField('apenas minhas')
def __init__(self, selected_id=None):
super().__init__()
self.only_user.data = session.get('board_choices_only_user', False)
self.init_choices()
if selected_id is not None:
self.save_session_current_board(selected_id)
self.board_id.data = selected_id
else:
session.pop('current_board', None)
def init_choices(self):
format_choice = lambda b:\
(b.id, 'id={id} usuário="{username}" estação="{nick}"'.format(
id=b.id, username=b.user.username if b.user else None,
nick=b.nickname))
if self.only_user.data:
choices = [format_choice(b) for b in Board.query.all()
if b.user == current_user]
else:
choices = [format_choice(b) for b in Board.query.all()]
self.board_id.choices = [(-1, '')] + choices
@classmethod
def handle_post(cls, board=None):
if 'board_id' in request.form and request.form['board_id'] != '-1':
cls.save_session_current_board(int(request.form['board_id']))
if 'only_user' in request.form:
session['board_choices_only_user'] = True
else:
session.pop('board_choices_only_user', None)
if 'current_board' in session:
if board and session['current_board']['id'] != board.id \
or board is None:
return redirect(url_for('main.view_board',
id=session['current_board']['id']))
return redirect(url_for('main.index'))
@classmethod
def save_session_current_board(cls, board_id):
board = Board.get(board_id)
session['current_board'] = board.to_json
session['current_board']['username'] = board.user.username
class ManageBoardForm(Form):
id = StringField('ID')
......@@ -16,6 +76,7 @@ class ManageBoardForm(Form):
longitude = StringField('Longitude')
description = TextAreaField('Descrição')
class SensorForm(Form):
nickname = StringField('Apelido')
quantity = SelectField('Grandeza Física',
......@@ -27,6 +88,7 @@ class SensorForm(Form):
unity = StringField('Unidade')
description = TextAreaField('Descrição')
class MyFileField(FileInput):
def __call__(self, field, **kwargs):
......@@ -39,6 +101,7 @@ class MyFileField(FileInput):
kwargs['data-buttonText'] = ' Escolher '
return super(MyTextInput, self).__call__(field, **kwargs)
class ImportCSVForm(Form):
arduino_choices = SelectField('Arduino:', coerce=int)
csv_file = FileField('Arquivo CSV:')
......@@ -50,6 +113,7 @@ class ImportCSVForm(Form):
(' ', 'espaço'),
('\t', r'\t (tabulação)')])
class ExportCSVForm(Form):
arduino_choices = SelectField('Arduino:', coerce=int)
btn_download = SubmitField('Download')
......
......@@ -79,6 +79,7 @@ USAGE
from .board import Board
from .sensor import Sensor
from .rawsensordata import RawSensorData
from .users import User
# === EXPORT VARIABLES SETUP (no need to update) =============================
......
......@@ -4,9 +4,13 @@ import csv
from .sensor import Sensor
from flask import abort
class Board(db.Model, DBUtils):
__tablename__ = 'boards'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
nickname = db.Column(db.String(30), unique=True, nullable=False)
latitude = db.Column(db.String(20))
longitude = db.Column(db.String(20))
......@@ -14,15 +18,10 @@ class Board(db.Model, DBUtils):
sensors = db.relationship('Sensor', backref='board', lazy='dynamic')
__tablename__ = 'boards'
__formfields__ = ['nickname', 'latitude', 'longitude', 'description']
STR_KEY_FIELD = 'nickname'
def __repr__(self):
return "<Board id={} nickname='{}' sensor_count={}>".format(
self.id, self.nickname, self.sensors.count())
return "<Board id={} user='{}' nickname='{}' sensor_count={}>".format(
self.id, self.user.username if self.user else None,
self.nickname, self.sensors.count())
@classmethod
def add(cls, **kwargs):
......@@ -60,6 +59,12 @@ class Board(db.Model, DBUtils):
.format(nickname))
return result
def get_sensor_or_404(self, nickname):
try:
return self.get_sensor(nickname)
except:
abort(404)
def add_sensor(self, **kwargs):
''' Add a new sensor related to this board. Field values are passed
as kwargs (no need for 'id' nor 'board_id').
......@@ -140,7 +145,7 @@ class Board(db.Model, DBUtils):
db.session.delete(board)
def send_to_form(self, form):
for field in self.__formfields__:
for field in self.__fields__:
getattr(form, field).rawdata = getattr(self, field)
@classmethod
......@@ -154,3 +159,9 @@ class Board(db.Model, DBUtils):
for field in self.__formfields__:
setattr(self, field, form.get(field, getattr(self, field)))
db.session.add(self)
# @property
# def to_full_json(self):
# d = {field: getattr(self, field) for field in self.__fields__}
# d.update(sensors={s.nickname: s.to_json for s in self.sensors.all()})
# return d
from .. import db
from flask import abort
class ClassProperty(property):
def __get__(self, cls, owner):
......@@ -6,75 +7,48 @@ class ClassProperty(property):
class DBUtils:
'''
Implements several short-named methods for accessing data more easily.
Attributes
----------
STR_KEY_FIELD (str|None)
Name of the str key field to be used by get()
__formfields__ (list of str)
List of all field names that will be available in user forms
Methods
-------
get() -- retrieve a record matching the given key for unique fields
ls() -- return a list of records matching the given filter
Implements several short-named methods and class properties for more
ease of use on the Python shell for database admin tasks.
'''
STR_KEY_FIELD = None # Model subclass can implement a str here
__formfields__ = [] # should exclude 'id', foreign keys and relations
@classmethod
def get(cls, key):
''' Return the Model object/record instance matching the unique
given key, or None if not found.
Type of given key can be of type:
int -- match 'id' field
str -- match cls.STR_KEY_FIELD, if != None
cls -- will just return the model object itself
def get(cls, *args):
''' Retrives a specific record if args consists of an int primary key,
or perform a SQL Alchemy query filter with the given args.
'''
if isinstance(key, int):
result = cls.query.filter(getattr(cls, 'id') == key).first()
if result is None:
raise KeyError("Table '{}' do not have a row with id == {}"
.format(cls.__tablename__, key))
return result
elif isinstance(key, str):
if cls.STR_KEY_FIELD is not None:
result = cls.query.filter(
getattr(cls, cls.STR_KEY_FIELD) == key).first()
if result is None:
raise KeyError(
"Table '{}' do not have a row with {} == '{}'"
.format(cls.__tablename__, cls.STR_KEY_FIELD, key))
return result
else:
raise TypeError("This model does not have a STR_KEY_FIELD.")
elif isinstance(key, cls):
return key
if len(args) == 1 and isinstance(args[0], int):
return cls.query.get_or_404(args[0])
else:
raise ValueError("Invalid key type")
return cls.query.filter(*args).first_or_404()
@ClassProperty
@classmethod
def get_key_field_name(cls, key):
''' Returns a string with the right field name for the given key type.
def __fields__(cls):
''' Returns a set of all the field names for this table.
'''
if isinstance(key, int):
return 'id'
elif isinstance(key, str):
return cls.STR_KEY_FIELD
else:
return None
return set(c.name for c in cls.__table__.columns)
@ClassProperty
@classmethod
def ls(cls):
''' Simply query all records inconditionally.
'''
return cls.query.all()
@ClassProperty
@classmethod
def fields(cls):
return [k for k, v in cls.__dict__.items()
if 'InstrumentedAttribute' in v.__class__.__name__]
def add(cls, **kwargs):
''' Add a new record to the table with given fields and returns it.
Does not commit automatically!
'''
r = cls(**kwargs)
db.session.add(r)
return r
@property
def to_json(self):
''' Returns a valid JSON dict with record values.
'''
return {field: getattr(self, field) for field in self.__fields__}
......@@ -19,10 +19,6 @@ class Sensor(db.Model, DBUtils):
lazy='dynamic')
__tablename__ = 'sensors'
STR_KEY_FIELD = 'nickname'
FIELDS = ['board_id', 'nickname', 'quantity', 'data_format', 'unity',
'description', 'measured_object']
def __repr__(self):
return "<Sensor id={} board_id={} nickname='{}' data_count={}>".format(
......@@ -44,12 +40,6 @@ class Sensor(db.Model, DBUtils):
sensor.rawdata.delete()
db.session.delete(sensor)
@property
def to_json(self):
d = {field: getattr(self, field) for field in self.FIELDS}
d['id'] = self.id
return d
@classmethod
def new_from_json(cls, json_dict, board_id=None):
kw = {'id': cls.get_new_id()}
......@@ -83,4 +73,18 @@ class Sensor(db.Model, DBUtils):
with open(filename, 'w') as f:
writer = csv.writer(f, delimiter=sep)
for d in self.rawdata.all():
writer.writerow([d.datetime_obj.strftime('%Y/%m/%d %H:%M:%S'), d.value])
writer.writerow([d.datetime_obj.strftime('%Y/%m/%d %H:%M:%S'),
d.value])
def get_dygraph_data(self):
''' Returns a string containing Javascript array code with values from
all the RawSensorData belonging to the given sensor.
'''
def row_generator(sensor):
for rawdata in sensor.rawdata.all():
dt = rawdata.datetime_obj
yield '[new Date({Y}, {M}, {D}, {h}, {m}, {s}), {y_value}]'\
.format(Y=dt.year, M=dt.month, D=dt.day, h=dt.hour,
m=dt.minute, s=dt.second, y_value=rawdata.value)
return '[{}]'.format(','.join(r for r in row_generator(self)))
from .. import db
from .. import db, login_manager
from .dbutils import DBUtils
class User(db.Model, DBUtils):
from flask import current_app
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer