From e46b18f67474ccbb2a95b5a74eee304a95b0456c Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Sat, 1 Apr 2017 18:34:01 -0500 Subject: [PATCH] Add initial schema migration, with basic tempmon table clones plus expose new tables in the admin --- .../9a0317b74dee_initial_tempmon_tables.py | 78 ++++++++++++++ .../versions/e33a25dbaf07_initial_schema.py | 82 ++++++++++++++ hotcooler/admins.py | 70 +++++++++++- hotcooler/models.py | 101 +++++++++++++++++- 4 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 alembic/versions/9a0317b74dee_initial_tempmon_tables.py create mode 100644 alembic/versions/e33a25dbaf07_initial_schema.py diff --git a/alembic/versions/9a0317b74dee_initial_tempmon_tables.py b/alembic/versions/9a0317b74dee_initial_tempmon_tables.py new file mode 100644 index 0000000..94e3427 --- /dev/null +++ b/alembic/versions/9a0317b74dee_initial_tempmon_tables.py @@ -0,0 +1,78 @@ +"""initial tempmon tables + +Revision ID: 9a0317b74dee +Revises: e33a25dbaf07 +Create Date: 2017-04-01 15:58:27.513593 + +""" + +# revision identifiers, used by Alembic. +revision = '9a0317b74dee' +down_revision = 'e33a25dbaf07' +branch_labels = None +depends_on = None + +import datetime +import websauna.system.model.columns +from sqlalchemy.types import Text # Needed from proper creation of JSON fields as Alembic inserts astext_type=Text() row + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('client', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('config_key', sa.String(length=50), nullable=False), + sa.Column('hostname', sa.String(length=255), nullable=False), + sa.Column('location', sa.String(length=255), nullable=True), + sa.Column('delay', sa.Integer(), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('online', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_client')), + sa.UniqueConstraint('config_key', name=op.f('uq_client_config_key')) + ) + op.create_table('probe', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('config_key', sa.String(length=50), nullable=False), + sa.Column('appliance_type', sa.Integer(), nullable=False), + sa.Column('description', sa.String(length=255), nullable=False), + sa.Column('device_path', sa.String(length=255), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('good_temp_min', sa.Integer(), nullable=False), + sa.Column('good_temp_max', sa.Integer(), nullable=False), + sa.Column('critical_temp_min', sa.Integer(), nullable=False), + sa.Column('critical_temp_max', sa.Integer(), nullable=False), + sa.Column('therm_status_timeout', sa.Integer(), nullable=False), + sa.Column('status_alert_timeout', sa.Integer(), nullable=False), + sa.Column('status', sa.Integer(), nullable=True), + sa.Column('status_changed', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('status_alert_sent', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], name=op.f('fk_probe_client_id_client')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_probe')), + sa.UniqueConstraint('config_key', name=op.f('uq_probe_config_key')) + ) + op.create_table('reading', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('probe_id', sa.Integer(), nullable=True), + sa.Column('taken', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('degrees_f', sa.Numeric(precision=7, scale=4), nullable=False), + sa.ForeignKeyConstraint(['client_id'], ['client.id'], name=op.f('fk_reading_client_id_client')), + sa.ForeignKeyConstraint(['probe_id'], ['probe.id'], name=op.f('fk_reading_probe_id_probe')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_reading')) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('reading') + op.drop_table('probe') + op.drop_table('client') + # ### end Alembic commands ### diff --git a/alembic/versions/e33a25dbaf07_initial_schema.py b/alembic/versions/e33a25dbaf07_initial_schema.py new file mode 100644 index 0000000..2d11a48 --- /dev/null +++ b/alembic/versions/e33a25dbaf07_initial_schema.py @@ -0,0 +1,82 @@ +"""initial schema + +Revision ID: e33a25dbaf07 +Revises: +Create Date: 2017-04-01 15:27:22.495907 + +""" + +# revision identifiers, used by Alembic. +revision = 'e33a25dbaf07' +down_revision = None +branch_labels = None +depends_on = None + +import datetime +import websauna.system.model.columns +from sqlalchemy.types import Text # Needed from proper creation of JSON fields as Alembic inserts astext_type=Text() row + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('group', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', websauna.system.model.columns.UUID(), nullable=True), + sa.Column('name', sa.String(length=64), nullable=True), + sa.Column('description', sa.String(length=256), nullable=True), + sa.Column('created_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('updated_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('group_data', websauna.system.model.columns.JSONB(), nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('pk_group')), + sa.UniqueConstraint('name', name=op.f('uq_group_name')) + ) + op.create_table('user_activation', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('updated_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('expires_at', websauna.system.model.columns.UTCDateTime(), nullable=False), + sa.Column('code', sa.String(length=32), nullable=False), + sa.PrimaryKeyConstraint('id', name=op.f('pk_user_activation')), + sa.UniqueConstraint('code', name=op.f('uq_user_activation_code')) + ) + op.create_table('users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', websauna.system.model.columns.UUID(), nullable=True), + sa.Column('username', sa.String(length=256), nullable=True), + sa.Column('email', sa.String(length=256), nullable=True), + sa.Column('password', sa.String(length=256), nullable=True), + sa.Column('created_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('updated_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('activated_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('enabled', sa.Boolean(name='user_enabled_binary'), nullable=True), + sa.Column('last_login_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('last_login_ip', websauna.system.model.columns.INET(length=50), nullable=True), + sa.Column('user_data', websauna.system.model.columns.JSONB(), nullable=True), + sa.Column('last_auth_sensitive_operation_at', websauna.system.model.columns.UTCDateTime(), nullable=True), + sa.Column('activation_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['activation_id'], ['user_activation.id'], name=op.f('fk_users_activation_id_user_activation')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_users')), + sa.UniqueConstraint('email', name=op.f('uq_users_email')), + sa.UniqueConstraint('username', name=op.f('uq_users_username')) + ) + op.create_table('usergroup', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['group.id'], name=op.f('fk_usergroup_group_id_group')), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_usergroup_user_id_users')), + sa.PrimaryKeyConstraint('id', name=op.f('pk_usergroup')) + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('usergroup') + op.drop_table('users') + op.drop_table('user_activation') + op.drop_table('group') + # ### end Alembic commands ### diff --git a/hotcooler/admins.py b/hotcooler/admins.py index 66a3656..29cbc68 100644 --- a/hotcooler/admins.py +++ b/hotcooler/admins.py @@ -1 +1,69 @@ -"""Place your admin resources in this file.""" \ No newline at end of file +""" +Admin resources for hotcooler +""" + +from websauna.system.admin.modeladmin import model_admin +from websauna.system.admin.modeladmin import ModelAdmin + +# Import our models +from . import models + + +@model_admin(traverse_id='client') +class ClientAdmin(ModelAdmin): + """Admin resource for tempmon client model. + + This class declares a resource for client model admin root folder with listing and add views. + """ + + #: Label as shown in admin + title = "Clients" + + #: Used in admin listings etc. user visible messages + #: TODO: This mechanism will be phased out in the future versions with gettext or similar replacement for languages that have plulars one, two, many + singular_name = "client" + plural_name = "clients" + + #: Which models this model admin controls + model = models.Client + + class Resource(ModelAdmin.Resource): + """Declare resource for each individual client. + + View, edit and delete views are registered against this resource. + """ + + def get_title(self): + return str(self.get_object()) + + +@model_admin(traverse_id='probe') +class ProbeAdmin(ModelAdmin): + """Admin resource for probe model.""" + + title = "Probes" + + singular_name = "probe" + plural_name = "probes" + model = models.Probe + + class Resource(ModelAdmin.Resource): + + def get_title(self): + return str(self.get_object()) + + +@model_admin(traverse_id='reading') +class ReadingAdmin(ModelAdmin): + """Admin resource for reading model.""" + + title = "Readings" + + singular_name = "reading" + plural_name = "readings" + model = models.Reading + + class Resource(ModelAdmin.Resource): + + def get_title(self): + return str(self.get_object()) diff --git a/hotcooler/models.py b/hotcooler/models.py index b84d880..7255b29 100644 --- a/hotcooler/models.py +++ b/hotcooler/models.py @@ -1 +1,100 @@ -"""Place your SQLAlchemy models in this file.""" \ No newline at end of file +""" +SQLAlchemy models for hotcooler +""" + +from uuid import uuid4 + +import sqlalchemy as sa +from sqlalchemy import orm +from sqlalchemy.dialects.postgresql import UUID + +from websauna.system.model.meta import Base +from websauna.system.model.columns import UTCDateTime + + +class Client(Base): + """ + Represents a tempmon client + """ + __tablename__ = 'client' + + id = sa.Column(sa.Integer(), autoincrement=True, primary_key=True) + uuid = sa.Column(UUID(as_uuid=True), default=uuid4) + + config_key = sa.Column(sa.String(50), nullable=False, unique=True) + hostname = sa.Column(sa.String(255), nullable=False) + location = sa.Column(sa.String(255), nullable=True) + + delay = sa.Column(sa.Integer(), nullable=True, doc=""" + Number of seconds to delay between reading / recording temperatures. If + not set, a default of 60 seconds will be assumed. + """) + + enabled = sa.Column(sa.Boolean(), nullable=False, default=False) + online = sa.Column(sa.Boolean(), nullable=False, default=False) + + probes = orm.relationship('Probe', + back_populates='client', + cascade='all, delete-orphan', + single_parent=True) + + def __str__(self): + return '{} ({})'.format(self.config_key, self.hostname) + + def enabled_probes(self): + return [probe for probe in self.probes if probe.enabled] + + +class Probe(Base): + """ + Represents a probe connected to a tempmon client + """ + __tablename__ = 'probe' + + id = sa.Column(sa.Integer(), autoincrement=True, primary_key=True) + uuid = sa.Column(UUID(as_uuid=True), default=uuid4) + + client_id = sa.Column(sa.Integer(), sa.ForeignKey('client.id')) + client = orm.relationship(Client, back_populates='probes') + + config_key = sa.Column(sa.String(length=50), nullable=False, unique=True) + appliance_type = sa.Column(sa.Integer(), nullable=False) + description = sa.Column(sa.String(length=255), nullable=False) + device_path = sa.Column(sa.String(length=255), nullable=True) + enabled = sa.Column(sa.Boolean(), nullable=False, default=True) + + good_temp_min = sa.Column(sa.Integer(), nullable=False) + good_temp_max = sa.Column(sa.Integer(), nullable=False) + critical_temp_min = sa.Column(sa.Integer(), nullable=False) + critical_temp_max = sa.Column(sa.Integer(), nullable=False) + therm_status_timeout = sa.Column(sa.Integer(), nullable=False) + status_alert_timeout = sa.Column(sa.Integer(), nullable=False) + + status = sa.Column(sa.Integer(), nullable=True) + status_changed = sa.Column(UTCDateTime, nullable=True) + status_alert_sent = sa.Column(UTCDateTime, nullable=True) + + def __str__(self): + return self.description + + +class Reading(Base): + """ + Represents a single temperature reading from a tempmon probe + """ + __tablename__ = 'reading' + + id = sa.Column(sa.Integer(), autoincrement=True, primary_key=True) + uuid = sa.Column(UUID(as_uuid=True), default=uuid4) + + client_id = sa.Column(sa.Integer(), sa.ForeignKey('client.id')) + client = orm.relationship(Client) + + probe_id = sa.Column(sa.Integer(), sa.ForeignKey('probe.id')) + probe = orm.relationship(Probe) + + taken = sa.Column(UTCDateTime, default=None) + degrees_f = sa.Column(sa.Numeric(precision=7, scale=4), nullable=False) + + def __str__(self): + return str(self.degrees_f)