Commit db1fa7d9 authored by Nelso Jost's avatar Nelso Jost

FIX: what a mess

parent b1511dfb
PYBIN := python3
VENVDIR := $(shell pwd)/.venv
VENVPY := ${VENVDIR}/bin/python
<<<<<<< HEAD
PORT := 5000
=======
EMMPORT := 5000
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
all: help
......@@ -19,10 +15,7 @@ help:
@ echo " mysql-info return MySQL info: users, dbs and emmdb usage"
@ echo " mysql-chroot change MySQL root password"
@ echo " mysql-enter starts MySQL interactive session as root"
<<<<<<< HEAD
@ echo " mysql-schema shows the database schema"
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
@ echo ""
@ echo " dbinit recreate database from scratch (drop data) and populate it"
@ echo " shell start ipython3 interactively for high level DB admin"
......@@ -36,7 +29,6 @@ help:
@ echo " undeploy undo the previous operation"
debreq:
<<<<<<< HEAD
sudo apt-get install nginx python3 python3-pip supervisor mysql-server
sudo pip3 install virtualenv
sudo pip3 install --upgrade pip
......@@ -44,24 +36,12 @@ debreq:
venv:
virtualenv -v --python='${PYBIN}' ${VENVDIR}
${VENVDIR}/bin/pip3 install --upgrade pip
=======
sudo apt-get install nginx python3 python3-pip supervisor mysql
sudo pip3 install virtualenv
sudo pip3 install --upgrade pip
setup:
virtualenv -v --python='${PYBIN}' ${VENVDIR}
${VENVDIR}/bin/pip3 install --upgrade pip
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
${VENVDIR}/bin/pip3 install -r requirements.pip
@ echo "-------------------------------------------------------"
@ echo "Virtualenv successfully created at"
@ du -sh ${VENVDIR}
<<<<<<< HEAD
setup: debreq venv
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
clean-venv:
rm -rf ${VENVDIR}
......@@ -73,11 +53,7 @@ list-routes:
${VENVPY} manage.py list_routes
run:
<<<<<<< HEAD
${VENVPY} manage.py runserver --port ${PORT}
=======
${VENVPY} manage.py runserver --port ${EMMPORT}
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
shell:
${VENVDIR}/bin/ipython manage.py shell
......@@ -87,7 +63,6 @@ fakelog:
tree:
tree . -I "${VENVDIR}|__pycache__|*.pyc"
<<<<<<< HEAD
mysql-init:
${VENVPY} production/manage_mysql.py init
......@@ -109,23 +84,6 @@ mysql-enter:
gunicorn:
cd production/nginx-gunicorn && ${VENVDIR}/bin/gunicorn wsgi:app
=======
mysql-createall:
${VENVPY} production/manage_mysql.py createall
mysql-describe:
chmod +x production/mysql_describe.sh && ./production/mysql_describe.sh
mysql-useremm:
${VENVPY} production/manage_mysql.py useremm
mysql-chroot:
${VENVPY} production/manage_mysql.py chroot
mysql-enter:
mysql -u root -p
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
deploy:
sudo ${VENVPY} production/nginx-gunicorn/deploy.py
......@@ -139,18 +97,11 @@ reset-settings:
clean-logs:
rm -rf logs/*
<<<<<<< HEAD
clean: clean-venv reset-settings
sudo find . -type f -name "*.py[co]" -delete
sudo find . -type d -name "__pycache__" -delete
sudo rm -f app/db.sqlite
sudo rm -rf doc/_build
=======
clean-all: clean-venv
py3clean app
rm -f app/db.sqlite
rm -rf doc/_build
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
logtail-nginx-access:
tail production/nginx-gunicorn/logs/nginx-access.log
......
......@@ -36,25 +36,15 @@ login_manager = LoginManager()
login_manager.session_protection = 'strong'
login_manager.login_view = 'auth.login'
<<<<<<< HEAD
def create_app():
=======
def create_app(config_type):
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
''' Creates the Flask application object and register configurations,
blueprints and related global objects for it.
'''
app = Flask(__name__)
<<<<<<< HEAD
from .config import config_select
app.config.from_object(config_select[EMMCONFIG])
config_select[EMMCONFIG].init_app(app)
=======
from .config import config
app.config.from_object(config[config_type])
config[config_type].init_app(app)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
from .controllers import blueprints
......
......@@ -3,35 +3,24 @@
# License: AGPL
# Purpose: Hold application-wide settings.
#-------------------------------------------------------------------------------
<<<<<<< HEAD
from configparser import ConfigParser
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
import yaml
import os
basedir = os.path.abspath(os.path.dirname(__file__))
<<<<<<< HEAD
def path_here(filename):
return os.path.realpath(
os.path.join(os.path.abspath(os.path.dirname(__file__)), filename))
=======
def path_here(x):
return os.path.join(os.path.abspath(os.path.dirname(__file__)), x)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
with open(path_here('data/DB_INIT.yaml'), encoding='utf-8') as f:
DB_INIT = yaml.safe_load(f.read())
<<<<<<< HEAD
cp = ConfigParser()
cp.read(path_here('../settings.ini'))
EMMCONFIG = cp['app']['config']
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
class Config:
host = '0.0.0.0'
......@@ -40,8 +29,6 @@ class Config:
# wtforms secure submit transactions
SECRET_KEY = cp['app']['pass']
SERVER_NAME = 'dados.cta.if.ufrgs.br'
UPLOAD_FOLDER = 'upload'
# automatically commits at the end of a request
......@@ -55,13 +42,8 @@ class Config:
MAIL_USERNAME = cp['mail']['address'].split('@')[0].strip()
MAIL_PASSWORD = cp['mail'].get('pass') or cp['app']['pass']
MAIL_SUBJECT_PREFIX = '[CTA-EMM-WEB] '
<<<<<<< HEAD
MAIL_SENDER = 'CTA-EMM-WEB <{}>'.format(cp['mail']['address'])
MAIL_ADDRESS = cp['mail']['address']
=======
MAIL_SENDER = 'CTA-EMM-WEB <cta.emm.web@gmail.com>'
MAIL_ADDRESS = 'cta.emm.web@gmail.com'
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
DB_INIT = DB_INIT
......@@ -91,15 +73,10 @@ class ProductionConfig(Config):
DEBUG = False
# format: mysql+DRIVER://USER:PASSWORD@localhost/DATABASE
<<<<<<< HEAD
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{USER}:{PASS}@localhost/{DB}'\
.format(USER=cp['db']['user'],
PASS=cp['db'].get('pass') or cp['app']['pass'],
DB=cp['db']['name'])
=======
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://ctaemm:{}@localhost/{EMMDB}'\
.format(os.environ.get('EMMPASS'), os.environ.get('EMMDB'))
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
class TestingConfig(ProductionConfig):
......
......@@ -3,18 +3,12 @@ from ...models import *
from . import api_blueprint as api
<<<<<<< HEAD
from flask import jsonify, request, make_response, Response, \
stream_with_context
import csv
import io
import json
=======
from flask import jsonify, request
import json
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
from datetime import datetime
def JSONError(message):
......@@ -50,14 +44,9 @@ def post_rawsensordata():
if not board:
return JSONError("Invalid 'board_hash': board not found.")
<<<<<<< HEAD
for i, d in enumerate(js['data']):
print('validating', i, d)
=======
for i, d in enumerate(js['data']):
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
try:
dt = datetime.strptime(d['datetime']['value'],
d['datetime']['format'])
......@@ -69,7 +58,6 @@ def post_rawsensordata():
return JSONError("JSON missing or invalid 'sensors' attribute at "
"data line {}".format(i))
<<<<<<< HEAD
sensor_values = {}
for name, val in d['sensors'].items():
......@@ -152,21 +140,3 @@ def get_rawsensordata_all(board_id):
headers={'Content-Disposition':
'attachment; filename=emm-board-{}.csv'.format(board.id)})
=======
for sensor_name, sensor_value in d['sensors'].items():
try:
sensor = board.get_sensor_or_404(sensor_name)
except:
return JSONError("Board '{}' does not have a sensor named '{}'."
.format(board.nickname, sensor_name))
try:
sensor.add_rawdata(datetime=dt, value=sensor_value,
avoid_duplication=True)
except Exception as e:
return JSONError('{}'.format(e))
db.session.commit()
return jsonify({'success': '{} new points were saved on the board.'
.format(len(js['data']))})
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
......@@ -49,11 +49,7 @@ class BoardSelectForm(Form):
@classmethod
def handle_post(cls, board=None):
<<<<<<< HEAD
# print(request.form)
=======
print(request.form)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
if 'board_id' in request.form and request.form['board_id'] != '-1':
cls.save_session_current_board(int(request.form['board_id']))
......@@ -113,7 +109,6 @@ class BoardManageForm(Form):
self.board_exposition.data = board.exposition.name
self.board_longitude.data = board.longitude
self.board_latitude.data = board.latitude
<<<<<<< HEAD
else:
self.sensors = [Sensor.get(name=sensor_name).dump(
include=['measurement_name', 'unity_label'])
......@@ -121,24 +116,10 @@ class BoardManageForm(Form):
def validate_board_nickname(self, field):
if boards.query.filter_by(nickname=field.data).first():
=======
if request.method != 'POST':
self.sensors = [sensor.dump(
include=['unity_label', 'measurement_name'])
for sensor in board.sensors.all()]
else:
if request.method != 'POST':
self.sensors = config.DB_INIT['DEFAULT_SENSORS']
def validate_board_nickname(self, field):
b = boards.get(nickname=field.data)
if b and b._userhash != self.board_userhash.data:
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
raise ValidationError("Já existe uma estação cadastrada com esse"
" apelido!")
def validate_and_save(self):
<<<<<<< HEAD
try:
print('Trying to validate new board')
if self.validate():
......@@ -158,26 +139,6 @@ class BoardManageForm(Form):
except Exception as e:
print('validation exception:', e)
db.session.rollback()
=======
if self.validate():
b = boards.get(nickname=self.board_nickname.data)
r = request.form
print('new board', b)
if not b:
kwargs = {k.replace('board_', ''): v for k, v
in request.form.items()
if k != 'board_userhash' and k.startswith('board_')}
kwargs['sensors'] = self.sensors
b = Board.add(**kwargs)
else:
b.nickname = r['board_nickname']
b.description = r['board_description']
b.exposition = Exposition.get(name=r['board_exposition'])
b.longitude = r['board_longitude']
b.latitude = r['board_latitude']
db.session.commit()
return b
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
def gather_request_sensors(self):
sensors = {}
......@@ -189,11 +150,7 @@ class BoardManageForm(Form):
sensors[i] = {field: v}
else:
sensors[i][field] = v
<<<<<<< HEAD
return [sensors[i] for i in sorted(sensors.keys())]
=======
return [sensors[i] for i in range(len(sensors))]
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
class SensorListItemForm(Form):
......
......@@ -64,22 +64,14 @@ def sensor(id, sensor_nickname):
POST --> board.main(selected_id) | main.index;
'''
b = Board.query.get_or_404(id)
<<<<<<< HEAD
s = b.get_sensor(sensor_nickname).sensor
=======
sensor = b.get_sensor(sensor_nickname)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
if request.method == 'POST':
return BoardSelectForm.handle_post(b)
##TODO: improve loading system (data interpolation maybe?)
<<<<<<< HEAD
return render_template('board/sensor.html', board=b, the_sensor=s,
=======
return render_template('board/sensor.html', board=b, the_sensor=sensor,
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
board_select_form = BoardSelectForm(selected_id=b.id))
......@@ -115,23 +107,11 @@ def insert():
''' GET --> templates/insertboard.html
POST | btn_cancel --> main.index
| btn_save --> main.view_board (new board)
<<<<<<< HEAD
'''
board_manage_form = BoardManageForm()
if request.method == 'POST':
pprint(request.form)
=======
| 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?
board_manage_form = BoardManageForm()
# pprint(request.form)
if request.method == 'POST':
# pprint(request.form)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
# got any button click? if so, the btn will appear on the request.form
if 'btn_cancel' in request.form:
return redirect(url_for('main.index'))
......
......@@ -10,24 +10,12 @@ def index():
''' GET --> index.html
POST --> board.view_board()
'''
<<<<<<< HEAD
boards = Board.dump_all(include=['username', 'exposition_name',
'last_rawdata_json'],
exclude=['_userhash'])
=======
boards = Board.dump(include_fields=['username', 'exposition_name',
'last_rawdata_json'],
ignore_fields=['_userhash'])
print(boards)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
if request.method == 'POST':
return BoardSelectForm.handle_post()
return render_template('index.html', board_select_form=BoardSelectForm(),
<<<<<<< HEAD
boards=boards)
=======
boards=boards)
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
......@@ -27,84 +27,52 @@ MEASUREMENTS:
- name: ATMOSPHERIC_TEMPERATURE
label_en: Atmospheric Temperature
label_br: Temperatura atmosférica
<<<<<<< HEAD
short_br: TEMP
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
unities: ["ºC", "ºF", "K", "u.a."]
- name: AIR_RELATIVE_HUMIDITY
label_en: Relative Humidity of Air
label_br: Umidade relativa do ar
<<<<<<< HEAD
short_br: U.AR
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
unities: ["%", "u.a."]
- name: ATMOSPHERIC_PRESSURE
label_en: Atmospheric Pressure
label_br: Pressão atmosférica
<<<<<<< HEAD
short_br: PRESS
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
unities: ["Pa", "hPa", "mmHg", "atm", "u.a."]
- name: LUMINOSITY
label_en: Luminosity
label_br: Luminosidade ambiente
<<<<<<< HEAD
short_br: LUM
unities: ["u.a.", "lux"]
SENSORS:
- name: DHT22_TEMP
=======
unities: ["u.a.", "lux"]
DEFAULT_SENSORS:
- nickname: DHT22_TEMP
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
measurement_name: ATMOSPHERIC_TEMPERATURE
unity_label: "ºC"
description: |
https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf
<<<<<<< HEAD
- name: DHT22_AH
=======
- nickname: DHT22_AH
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
measurement_name: AIR_RELATIVE_HUMIDITY
unity_label: "%"
description: |
https://www.sparkfun.com/datasheets/Sensors/Temperature/DHT22.pdf
<<<<<<< HEAD
- name: BMP085_PRESSURE
=======
- nickname: BMP085_PRESSURE
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
measurement_name: ATMOSPHERIC_PRESSURE
unity_label: "Pa"
description: |
http://www.adafruit.com/datasheets/BMP085_DataSheet_Rev.1.0_01July2008.pdf
<<<<<<< HEAD
- name: LDR
=======
- nickname: LDR
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
measurement_name: LUMINOSITY
unity_label: "u.a."
description: |
http://www.biltek.tubitak.gov.tr/gelisim/elektronik/dosyalar/40/LDR_NSL19_M51.pdf
<<<<<<< HEAD
DEFAULT_SENSORS: [DHT22_TEMP, DHT22_AH, BMP085_PRESSURE, LDR]
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
......@@ -6,11 +6,6 @@ from flask.ext.script import Command
import json
import os
<<<<<<< HEAD
=======
__all__ = ['DBInit']
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
def path_here(x):
return os.path.join(os.path.abspath(os.path.dirname(__file__)), x)
......@@ -30,14 +25,9 @@ class DBInitCommand(Command):
print("\nPopulating with data/DB_INIT.yaml...")
<<<<<<< HEAD
user_admin = User.add(username='admin', confirmed=True,
email=self.app.config['MAIL_ADDRESS'],
password=self.app.config['SECRET_KEY'])
=======
user_admin = User.add(username='admin', email='cta.emm.web@gmail.com',
confirmed=True, password=os.environ['EMMPASS'])
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
db_init = self.app.config['DB_INIT']
......@@ -46,11 +36,8 @@ class DBInitCommand(Command):
Unity.import_from_json(*db_init['UNITIES'])
Measurement.import_from_json(*db_init['MEASUREMENTS'])
<<<<<<< HEAD
Sensor.import_from_json(*db_init['SENSORS'])
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
Board.add(nickname='Pezzi', user=user_admin,
longitude='-51.11948668956755',
latitude='-30.072849663041758',
......
......@@ -14,27 +14,16 @@ from .basemixin import BaseMixin, TableList
from .role import Role
from .exposition import Exposition
from .user import User
<<<<<<< HEAD
=======
from .board import Board
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
from .unity import Unity
from .measurement import Measurement
from .unitymeasurement import UnityMeasurement
from .sensor import Sensor
<<<<<<< HEAD
from .boardsensor import BoardSensor
from .rawsensordata import RawSensorData
from .board import Board
del role, exposition, user, board, unity, measurement, unitymeasurement, \
sensor, rawsensordata, boardsensor
=======
from .rawsensordata import RawSensorData
del role, exposition, user, board, unity, measurement, unitymeasurement, \
sensor, rawsensordata
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
# === EXPORT VARIABLES SETUP (no need to update) =============================
......
......@@ -12,25 +12,31 @@ class BaseMixin:
def json(self):
''' Returns a valid JSON dict with record values.
'''
return {col.name: getattr(self, col.name) for col in self.cols}
return {col.name: getattr(self, col.name + '_json')
if hasattr(self, col.name + '_json')
else getattr(self, col.name)
for col in self.cols}
@ClassProperty
@classmethod
def new_json(self):
return {col.name: None for col in self.cols}
def dump(self, include=None, exclude=None):
d = self.json
if exclude:
for field in exclude:
d.pop(field)
if include:
for field in include:
d[field] = getattr(self, field)
return d
@classmethod
def dump(cls, include_fields=None, ignore_fields=None):
def remove_fields(x):
j = x.json
if ignore_fields:
for field in ignore_fields:
j.pop(field)
if include_fields:
for field in include_fields:
j[field] = getattr(x, field)
return j
return [remove_fields(x) for x in cls.query.all()]
def dump_all(cls, include=None, exclude=None):
return [x.dump(include=include, exclude=exclude)
for x in cls.query]
@classmethod
def get(cls, *args, **kwargs):
......
......@@ -7,20 +7,14 @@ from .unity import Unity
from .measurement import Measurement
from .unitymeasurement import UnityMeasurement
from .sensor import Sensor
<<<<<<< HEAD
from .boardsensor import BoardSensor
from .rawsensordata import RawSensorData
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
from werkzeug.security import generate_password_hash,\
check_password_hash
<<<<<<< HEAD
from flask import url_for
=======
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
import csv
from datetime import datetime
......@@ -40,14 +34,10 @@ class Board(db.Model, BaseMixin):
_userhash = db.Column(db.String(128))
<<<<<<< HEAD
sensors = db.relationship('BoardSensor', backref='board',
lazy='dynamic')
rawdata = db.relationship('RawSensorData', backref='board',
lazy='dynamic')
=======
sensors = db.relationship('Sensor', backref='board', lazy='dynamic')
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
def __repr__(self):
return "<Board id={} user='{}' nickname='{}' expo='{}' "\
......@@ -68,7 +58,6 @@ class Board(db.Model, BaseMixin):
@property
def last_rawdata_json(self):
d = {}
<<<<<<< HEAD
last_data = self.rawdata.order_by(RawSensorData.datetime.desc())\
.first()
d['data'] = last_data.json if last_data else None
......@@ -81,16 +70,6 @@ class Board(db.Model, BaseMixin):
s['url'] = url_for('board.sensor',
id=self.id, sensor_nickname=bs.sensor.name)
d['sensors'].append(s)
=======
for sensor in self.sensors.all():
last_data = sensor.last_rawdata
if last_data:
d[sensor.measurement_name] = last_data.json
d[sensor.measurement_name]['unity_label'] = sensor.unity_label
d[sensor.measurement_name]['datetime'] = str(last_data.datetime)
else:
d[sensor.measurement_name] = None
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
return d
@classmethod
......@@ -109,11 +88,7 @@ class Board(db.Model, BaseMixin):
db.session.add(board)
db.session.flush() # assure ID fields without commiting
<<<<<<< HEAD
if sensors:
=======
if sensors and default_sensors is False:
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
board.add_sensors(sensors)
elif default_sensors:
board.include_default_sensors()
......@@ -126,41 +101,10 @@ class Board(db.Model, BaseMixin):
return board
<<<<<<< HEAD
def get_sensor(self, name):
''' Return a BoardSensor with given name on this board or 404. '''
return self.sensors.join(Sensor).filter(
Sensor.name == name).first_or_404()
=======
def get_sensor(self, nickname):
''' Return a sensor from this board with given nickname.
'''
return self.sensors.filter_by(nickname = nickname).first_or_404()
def add_sensor(self, **kwargs):
''' Add new sensor attached to this board and return it.
Kwargs are sensor columns. 'board_id' or 'board' are discarded.
'''
for attr in ('id', 'board', 'board_id'):
kwargs.pop(attr, None)
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' "
"attributes.")
sensor = Sensor(board=self, **kwargs)
db.session.add(sensor)
return sensor
>>>>>>> c672c539351a7585c9515c59d260609e93bb5b51
def delete_sensor(self, key):
''' Recursively remove all data related to the sensor mathing the
......@@ -169,7 +113,6 @@ class Board(db.Model, BaseMixin):
db.session.delete(self.get_sensor(