Commit ea30cbd3 authored by Nelso Jost's avatar Nelso Jost

FIX: Insert board view; ADD: production MySQL setup

parent 34a68f7d
.venv/
.tag*
*.swp
*.autosave
*.sqlite
......
......@@ -79,6 +79,9 @@ fakelog:
tree:
tree . -I "${VENV}|__pycache__|*.pyc"
mysql-setup:
mysql -u root -p < production/MYSQL_SETUP
deploy:
@ echo "-------------------------------------------------------"
mkdir -p production/nginx-gunicorn/logs
......
......@@ -4,7 +4,9 @@
# Purpose: Provides the application factory and basic initializations.
#-------------------------------------------------------------------------------
'''
Instead of initializing the application by creating the Flask object globally, the goal here is to do it on-the-fly via the factory function create_app().
Instead of initializing the application by creating the Flask() object
globally, the approach used here is to do it on-the-fly via the factory
function create_app().
This approach has several advantages:
......
......@@ -7,17 +7,18 @@ import os
basedir = os.path.abspath(os.path.dirname(__file__))
# [flask] -- allow auto restart on file changes
DEBUG = False
DEBUG = True
host = '0.0.0.0'
SECRET_KEY = os.environ.get('FLASK_SECRET_KEY') or \
'hard to guess string'
# [flask-sqlalchemy] -- for sqlite version
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite')
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'db.sqlite')
# [flask-sqlalchemy] -- for mysql version
# SQLALCHEMY_DATABASE_URI = 'mysql://username:password@hostname/database'
# format: mysql://username:password@hostname/database
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://ctaemm:ctaemm@localhost/emmdb'
# [flask-sqlalchemy] -- assures a commit to prevent data loss
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
......@@ -29,7 +30,7 @@ class DATETIME:
# [app] -- include js client code to peform page reload periodically
# ATTENTION: may overload server connection
class AUTO_PAGE_RELOAD:
active = True
active = False
interval = 2000
class DB_ENUMS:
......
#-------------------------------------------------------------------------------
# Author: Nelso G. Jost <nelsojost@gmail.com> Copyright(C) 2015
# License: AGPL
# Purpose: Holds all database related functionality (models and operations).
#-------------------------------------------------------------------------------
'''
This project uses the SQL Alchemy ORM (Object-Relational-Mapper) to perform
all its database operations, integrated via Flask-SQLAlchemy extension.
DATABASE SCHEMA
---------------
+-----------------------+ +---------------------------+
| Board | | Sensor |
+-----------------------+ +---------------------------+
| id : int, PK |<(1)--+ | id : int, PK |<-(1)-+
| nickname : str | +--(n)>| board_id : int, FK | |
| latitude : str | | nickname : str | |
| longitude : str | | quantity : str | |
| description : str | | measured_object : str | |
+-----------------------+ | data_format : str | |
| unity : str | |
| description : str | |
+---------------------------+ |
|
+---------------------+ |
| RawSensorData | |
+---------------------+ |
| id : int, PK | |
| sensor_id : int, FK |<-(n)-------+
| datetime : str |
| value : str |
+---------------------+
OVERVIEW
---------
Tables are declared on this file through the concept of models -- a fancy name
for Python classes that inherit db.Model and specify table fields/columns with
class attributes of type db.Column. Relationships between tables are also
declared on the models with db.relationship class attributes. Finally, since
models are Python classes, operations can also be implemented.
Model Conventions for this project:
* All models inherits BaseModelUtils to provide a set of basic,
short-named, operations for easing of use on a Python shell.
* All tables have an integer field 'id' as primary key, that will be
auto-generated if not provided.
Convention is to NOT provide it. Thus, a call to db.session.flush()
or db.session.commit() must be made to generate this value.
* All models declare the __tablename__ attribute, which holds a nicer,
pural, name for the table.
* All models declare the __formfields__ attribute, which holds the names
of fields editable by the end user on HTML forms.
* Attribute declaration follow the pattern:
[fields]
[relationships]
__tablename__
__formfields__
[BaseModelUtils attributes]
USAGE
-----
The Board model is the entry point to explore meteorological data.
First,
'''
from .board import Board
from .sensor import Sensor
from .rawsensordata import RawSensorData
# === EXPORT VARIABLES SETUP (no need to update) =============================
tables = {v.__tablename__: v for k, v in globals().items()
if hasattr(v, '__tablename__')}
globals().update(**tables)
__all__ = ['tables'] + [k for k in tables.keys()] \
+ [k for k, v in globals().items()
if hasattr(v, '__tablename__')]
# ============================================================================
This diff is collapsed.
from .. import db
class ClassProperty(property):
def __get__(self, cls, owner):
return self.fget.__get__(None, owner)()
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
'''
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
'''
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
else:
raise ValueError("Invalid key type")
@classmethod
def get_key_field_name(cls, key):
''' Returns a string with the right field name for the given key type.
'''
if isinstance(key, int):
return 'id'
elif isinstance(key, str):
return cls.STR_KEY_FIELD
else:
return None
@ClassProperty
@classmethod
def ls(cls):
return cls.query.all()
@ClassProperty
@classmethod
def fields(cls):
return [k for k, v in cls.__dict__.items()
if 'InstrumentedAttribute' in v.__class__.__name__]
from .. import config, db
from .dbutils import DBUtils
from datetime import datetime
class RawSensorData(db.Model, DBUtils):
id = db.Column(db.Integer, primary_key=True)
sensor_id = db.Column(db.Integer, db.ForeignKey('sensors.id'))
datetime = db.Column(db.String(20))
value = db.Column(db.String(50))
__tablename__ = 'rawsensordata'
def __repr__(self):
return "<RawSensorData id={} sensor_id={} datetime='{}' value={}>"\
.format(self.id, self.sensor_id, self.datetime, self.value)
@property
def datetime_obj(self):
return datetime.strptime(self.datetime,
config.DATETIME.INTERNAL_FORMAT)
from .. import db
from .dbutils import DBUtils
from .rawsensordata import RawSensorData
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))
rawdata = db.relationship('RawSensorData', backref='sensor',
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(
self.id, self.board_id, self.nickname, self.rawdata.count())
def add_rawdata(self, **kwargs):
''' Add raw sensor data. RawSensorData fields are given by kwargs.
'''
if 'sensor_id' in 'kwargs':
del kwargs['sensor_id']
db.session.add(RawSensorData(sensor_id = self.id, **kwargs))
@classmethod
def delete(cls, key):
''' Recursively remove all data related to the sensor matching the
given key, incluing itself.
'''
sensor = cls.get(key)
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()}
for field in cls.FIELDS:
kw[field] = json_dict.get(field)
if board_id is not None:
kw['board_id'] = board_id
return cls(**kw)
@classmethod
def add_from_json(cls, json_dict, board_id=None):
sensor = cls.new_from_json(json_dict, board_id)
db.session.add(sensor)
return sensor
def update_from_json(self, json_data):
for field in self.FIELDS:
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
from .. import db
from .dbutils import DBUtils
class User(db.Model, DBUtils):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(30), unique=True, nullable=False)
email = db.Column(db.String(200), unique=True, nullable=False)
def __repr__(self):
return "<User id={} name={}>".format(self.id, self.name)
#-------------------------------------------------------------------------------
# Author: Nelso G. Jost <nelsojost@gmail.com> Copyright(C) 2015
# License: AGPL
# Purpose: Holds all database related functionality (models and operations).
#-------------------------------------------------------------------------------
'''
This project uses the SQL Alchemy ORM (Object-Relational-Mapper) to perform
all its database operations, integrated via Flask-SQLAlchemy extension.
DATABASE SCHEMA
---------------
+-----------------------+ +---------------------------+
| Board | | Sensor |
+-----------------------+ +---------------------------+
| id : int, PK |<(1)--+ | id : int, PK |<-(1)-+
| nickname : str | +--(n)>| board_id : int, FK | |
| latitude : str | | nickname : str | |
| longitude : str | | quantity : str | |
| description : str | | measured_object : str | |
+-----------------------+ | data_format : str | |
| unity : str | |
| description : str | |
+---------------------------+ |
|
+---------------------+ |
| RawSensorData | |
+---------------------+ |
| id : int, PK | |
| sensor_id : int, FK |<-(n)-------+
| datetime : str |
| value : str |
+---------------------+
OVERVIEW
---------
Tables are declared on this file through the concept of models -- a fancy name
for Python classes that inherit db.Model and specify table fields/columns with
class attributes of type db.Column. Relationships between tables are also
declared on the models with db.relationship class attributes. Finally, since
models are Python classes, operations can also be implemented.
Model Conventions for this project:
* All models inherits BaseModelUtils to provide a set of basic,
short-named, operations for easing of use on a Python shell.
* All tables have an integer field 'id' as primary key, that will be
auto-generated if not provided.
Convention is to NOT provide it. Thus, a call to db.session.flush()
or db.session.commit() must be made to generate this value.
* All models declare the __tablename__ attribute, which holds a nicer,
pural, name for the table.
* All models declare the __formfields__ attribute, which holds the names
of fields editable by the end user on HTML forms.
* Attribute declaration follow the pattern:
[fields]
[relationships]
__tablename__
__formfields__
[BaseModelUtils attributes]
USAGE
-----
The Board model is the entry point to explore meteorological data.
First,
'''
# __all__ = ['Board', 'Sensor', 'RawSensorData', 'list_models']
#
# from . import db
# from . import config
# from datetime import datetime
# import csv
# list_models = [Board, Sensor, RawSensorData]
# class PhysicalQuantity(db.Model):
# __tablename__ = 'physicalquantity'
#
# id = db.Column(db.Integer, primary_key=True)
# name = db.Column(db.String(20))
# class MeasuredTarget(db.Model):
# __tablename__ = 'measuredtarget'
#
# id = db.Column(db.Integer, primary_key=True)
# name = db.Column(db.String(20))
......@@ -46,6 +46,7 @@
<tr>
<th>Apelido</th>
<th>Grandeza Física</th>
<th>Objeto medido</th>
<th>Unidade</th>
<th>Formato de dados</th>
<th>Descrição</th>
......@@ -56,9 +57,10 @@
{% for f in sensor_forms %}
<tr>
<td>{{ f.nickname(class='form-control', size=10,
<td>{{ f.nickname(class='form-control', size=20,
autofocus=f.nickname.has_focus) }}</td>
<td>{{ f.quantity(class='form-control') }}</td>
<td>{{ f.measured_object(class='form-control') }}</td>
<td>{{ f.unity(class='form-control', size=5) }}</td>
<td>{{ f.data_format(class='form-control') }}</td>
<td>{{ f.description(class='form-control',
......@@ -75,6 +77,7 @@
</tbody>
</table>
<div class="panel-body">
<button type="submit" method="POST" name="btn_add_sensor"
class="btn btn-default">
......
......@@ -216,10 +216,11 @@ def view_insert_board():
return redirect(url_for('.index'))
if 'btn_save' in request.form:
board = Board.add_from_request(request.form)
update_session_sensors()
for sensor_json in session['sensors']:
Sensor.add_from_json(json_dict=sensor_json, board_id=board.id)
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)
db.session.commit()
return redirect(url_for('.view_board', id=board.id))
......
No preview for this file type
......@@ -58,14 +58,12 @@ Where [command] can be:
'''
from flask.ext.script import Manager, Shell
import app as ctaemmweb
from app import create_app, db
from app.config import DEFAULT_SENSORS
from app.models import *
# nice plural and lowercased names for better semantics on db operations
for m in list_models:
globals()[m.__tablename__] = m
# short nicknames for commom db.session operations
for n in ('commit', 'rollback', 'flush'):
globals()[n] = getattr(db.session, n)
......@@ -93,9 +91,9 @@ def initdb():
boards.get('Pezzi').import_csv_sensor_data(
filename=os.path.join('data', '26-Fev-2013_-_04-Mar-2013.log'),
columns=['datetime', 'DHT22_TEMP', 'DHT22_AH', '#',
'BMP085_PRESSURE', 'LDR'],
delimiter='\t')
cols=['datetime', 'DHT22_TEMP', 'DHT22_AH', '#',
'BMP085_PRESSURE', 'LDR'],
sep='\t')
db.session.commit()
......
DROP DATABASE emmdb;
CREATE DATABASE emmdb CHARACTER SET utf8 COLLATE utf8_bin;
DROP USER 'ctaemm'@'localhost';
CREATE USER 'ctaemm'@'localhost' identified by 'ctaemm';
GRANT ALL PRIVILEGES on emmdb.* to 'ctaemm'@'localhost';
FLUSH PRIVILEGES;
......@@ -10,6 +10,7 @@ itsdangerous==0.24
Jinja2==2.7.3
MarkupSafe==0.23
Pygments==2.0.2
PyMySQL==0.6.6
pyreadline==2.0
pyserial==2.7
PyYAML==3.11
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment