diff --git a/app/__init__.py b/app/__init__.py index 66c4caf..eed71a8 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -21,7 +21,7 @@ def create_app(config_name): root = CONFIG[config_name].APPLICATION_ROOT # flask migrate doesn't recognize the tables without this import - from app.models import Application, Genre, ApplicationType + from app.models import Application, Genre, ApplicationType, Session, Questionnaire # Set up extensions db.init_app(app) @@ -33,4 +33,10 @@ def create_app(config_name): from app.routes.v1 import application as application_blueprint app.register_blueprint(application_blueprint, url_prefix=root + '/application') + from app.routes.v1 import session as session_blueprint + app.register_blueprint(session_blueprint, url_prefix=root + '/session') + + from app.routes.v1 import questionnaire as questionnaire_blueprint + app.register_blueprint(questionnaire_blueprint, url_prefix=root + '/questionnaire') + return app diff --git a/app/models/__init__.py b/app/models/__init__.py index bdfa087..2316650 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -6,3 +6,5 @@ from .application import * # noqa from .application_type import * # noqa from .genre import * # noqa +from .session import * # noqa +from .questionnaire import * # noqa diff --git a/app/models/application.py b/app/models/application.py index 876b4c5..50b2754 100644 --- a/app/models/application.py +++ b/app/models/application.py @@ -1,4 +1,6 @@ from marshmallow import fields, validate +from .application_type import ApplicationTypeSchema +from .genre import GenreSchema from .. import db, ma @@ -13,9 +15,18 @@ class Application(db.Model): description = db.Column(db.String(250), nullable=False) creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False) genre_id = db.Column(db.Integer, db.ForeignKey('genre.id', use_alter=True, name='fk_genre_id'), nullable=False) + sessions = db.relationship('Session', backref='app', lazy='dynamic') + + def __init__(self, name, identifier, developer, type, description, genre): + self.name = name + self.identifier = identifier + self.developer = developer + self.type = type + self.description = description + self.genre = genre def __repr__(self): - return self.name + return '' % self.id class ApplicationSchema(ma.Schema): @@ -23,7 +34,7 @@ class ApplicationSchema(ma.Schema): name = fields.String(required=True, validate=validate.Length(1, 100)) identifier = fields.String() developer = fields.String(required=True, validate=validate.Length(1, 100)) - type = fields.String(required=True) + type = fields.Nested(ApplicationTypeSchema, dump_only=True) description = fields.String(required=True, validate=validate.Length(1, 250)) creation_date = fields.DateTime() - genre = fields.String(required=True) + genre = fields.Nested(GenreSchema, dump_only=True) diff --git a/app/models/application_type.py b/app/models/application_type.py index a161911..c93015a 100644 --- a/app/models/application_type.py +++ b/app/models/application_type.py @@ -15,6 +15,11 @@ class ApplicationType(db.Model): display_name_full = db.Column(db.String(250), nullable=False) applications = db.relationship('Application', backref='type', lazy='dynamic') + def __init__(self, name, display_name, display_name_full): + self.name = name + self.display_name = display_name + self.display_name_full = display_name_full + @classmethod def seed(cls): if cls.is_table_empty(cls): @@ -41,7 +46,7 @@ def is_table_empty(self): return False def __repr__(self): - return self.display_name_full + return '' % self.id class ApplicationTypeSchema(ma.Schema): diff --git a/app/models/genre.py b/app/models/genre.py index f32d315..89a0ecf 100644 --- a/app/models/genre.py +++ b/app/models/genre.py @@ -14,6 +14,10 @@ class Genre(db.Model): display_name = db.Column(db.String(250), nullable=False) applications = db.relationship('Application', backref='genre', lazy='dynamic') + def __init__(self, name, display_name): + self.name = name + self.display_name = display_name + @classmethod def seed(cls): if cls.is_table_empty(cls): @@ -40,7 +44,7 @@ def is_table_empty(self): return False def __repr__(self): - return self.display_name + return '' % self.id class GenreSchema(ma.Schema): diff --git a/app/models/questionnaire.py b/app/models/questionnaire.py new file mode 100644 index 0000000..3ae3522 --- /dev/null +++ b/app/models/questionnaire.py @@ -0,0 +1,32 @@ +from marshmallow import fields, validate +from .. import db, ma + + +class Questionnaire(db.Model): + __tablename__ = 'questionnaire' + + id = db.Column(db.Integer, primary_key=True) + pre = db.Column(db.JSON, nullable=False) + post = db.Column(db.JSON, nullable=False, default={}) + creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False) + session = db.relationship("Session", uselist=False, backref="questionnaire") + + def __init__(self, pre, post): + self.pre = pre + self.post = post + + def __repr__(self): + return '' % self.id + + +class SymptomSchema(ma.Schema): + name = fields.String(required=False) + display_name = fields.String(required=False) + score = fields.String(required=False) + + +class QuestionnaireSchema(ma.Schema): + id = fields.Integer(dump_only=True) + pre = fields.List(fields.Nested(SymptomSchema), dump_only=True) + post = fields.List(fields.Nested(SymptomSchema), dump_only=True) + creation_date = fields.DateTime() diff --git a/app/models/session.py b/app/models/session.py new file mode 100644 index 0000000..d71089b --- /dev/null +++ b/app/models/session.py @@ -0,0 +1,37 @@ +from marshmallow import fields, validate +from .. import db, ma +from .application import ApplicationSchema +from .questionnaire import QuestionnaireSchema + + +class Session(db.Model): + __tablename__ = 'session' + + id = db.Column(db.Integer, primary_key=True) + app_id = db.Column(db.Integer, db.ForeignKey('application.id', use_alter=True, name='fk_app_id'), nullable=False) + creation_date = db.Column(db.TIMESTAMP, server_default=db.func.current_timestamp(), nullable=False) + expected_emotions = db.Column(db.JSON, nullable=False) + questionnaire_id = db.Column(db.Integer, db.ForeignKey('questionnaire.id', use_alter=True, name='fk_questionnaire_id'), nullable=False) + cssi_score = db.Column(db.Float, nullable=False, default=0) + latency_scores = db.Column(db.JSON, nullable=False, default={}) + total_latency_score = db.Column(db.Float, nullable=False, default=0) + sentiment_scores = db.Column(db.JSON, nullable=False, default={}) + total_sentiment_score = db.Column(db.Float, nullable=False, default=0) + questionnaire_scores = db.Column(db.JSON, nullable=True, default={}) + total_questionnaire_score = db.Column(db.Float, nullable=False, default=0) + + def __init__(self, app, expected_emotions, questionnaire): + self.app = app + self.expected_emotions = expected_emotions + self.questionnaire = questionnaire + + def __repr__(self): + return '' % self.id + + +class SessionSchema(ma.Schema): + id = fields.Integer(dump_only=True) + creation_date = fields.DateTime() + expected_emotions = fields.List(fields.String(), dump_only=True) + app = fields.Nested(ApplicationSchema, dump_only=True) + questionnaire = fields.Nested(QuestionnaireSchema, dump_only=True) diff --git a/app/routes/v1/__init__.py b/app/routes/v1/__init__.py index 0d00b41..ebfd221 100644 --- a/app/routes/v1/__init__.py +++ b/app/routes/v1/__init__.py @@ -1,2 +1,4 @@ from app.routes.v1.application import application # noqa from app.routes.v1.main import main # noqa +from app.routes.v1.session import session # noqa +from app.routes.v1.questionnaire import questionnaire # noqa diff --git a/app/routes/v1/application.py b/app/routes/v1/application.py index 2cfeec4..f1f60dc 100644 --- a/app/routes/v1/application.py +++ b/app/routes/v1/application.py @@ -45,6 +45,7 @@ def get_application(id): @application.route('/', methods=['POST']) def create_application(): """Create a new Application""" + json_data = request.get_json(force=True) if not json_data: @@ -55,14 +56,13 @@ def create_application(): if errors: return jsonify({'status': 'error', 'message': 'Incorrect format of data provided.', 'data': errors}), 422 - name = data['name'] + name = request.json['name'] identifier = str(uuid.uuid4().hex) - developer = data['developer'] - type = ApplicationType.query.filter_by(name=data['type']).first() - description = data['description'] - genre = Genre.query.filter_by(name=data['genre']).first() + developer = request.json['developer'] + type = ApplicationType.query.filter_by(id=request.json['type']).first() + description = request.json['description'] + genre = Genre.query.filter_by(id=request.json['genre']).first() - print(identifier) # validate application type if not type: return {'status': 'error', 'message': 'Invalid Application Type'}, 400 diff --git a/app/routes/v1/questionnaire.py b/app/routes/v1/questionnaire.py new file mode 100644 index 0000000..4acb097 --- /dev/null +++ b/app/routes/v1/questionnaire.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright 2019 Brion Mario. +# (c) This file is part of the CSSI REST API and is made available under MIT license. +# (c) For more information, see https://github.com/brionmario/cssi-api/blob/master/LICENSE.txt +# (c) Please forward any queries to the given email address. email: brion@apareciumlabs.com + +"""Application routes module + +This modules contains all the different routes to interact with applications. + +Authors: + Brion Mario + +""" + +import uuid +from flask import Blueprint, jsonify, request +from app.models import Questionnaire, ApplicationType, QuestionnaireSchema, Genre +from app import db + +questionnaire = Blueprint('questionnaire', __name__) + +questionnaire_schema = QuestionnaireSchema(strict=True) +questionnaires_schema = QuestionnaireSchema(many=True, strict=True) + + +@questionnaire.route('/', methods=['GET']) +def get_questionnaire_list(): + """Get a list of all the Questionnaire""" + questionnaires = Questionnaire.query.all() + result = questionnaires_schema.dump(questionnaires).data + return jsonify({'status': 'success', 'message': None, 'data': result}), 200 + + +@questionnaire.route('/', methods=['GET']) +def get_questionnaire(id): + """Get questionnaire when an id is passed in""" + questionnaire = Questionnaire.query.get(id) + result = questionnaire_schema.dump(questionnaire).data + return jsonify({'status': 'success', 'message': None, 'data': result}), 200 + + +@questionnaire.route('/', methods=['POST']) +def create_questionnaire(): + """Create a new Questionnaire""" + pre = request.json['pre'] + post = request.json['post'] + + new_questionnaire = Questionnaire(pre=pre, post=post) + + db.session.add(new_questionnaire) + db.session.commit() + + result = questionnaire_schema.dump(new_questionnaire).data + + return jsonify({'status': 'success', 'message': 'Created new questionnaire', 'data': result}), 201 diff --git a/app/routes/v1/session.py b/app/routes/v1/session.py new file mode 100644 index 0000000..0930393 --- /dev/null +++ b/app/routes/v1/session.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# (c) Copyright 2019 Brion Mario. +# (c) This file is part of the CSSI REST API and is made available under MIT license. +# (c) For more information, see https://github.com/brionmario/cssi-api/blob/master/LICENSE.txt +# (c) Please forward any queries to the given email address. email: brion@apareciumlabs.com + +"""Session routes module + +This modules contains all the different routes to interact with Sessions. + +Authors: + Brion Mario + +""" + +from flask import Blueprint, jsonify, request +from app.models import Session, SessionSchema, Application, Questionnaire +from app import db + +session = Blueprint('session', __name__) + +session_schema = SessionSchema(strict=True) +sessions_schema = SessionSchema(many=True, strict=True) + + +@session.route('/', methods=['GET']) +def get_sessions_list(): + """Get a list of all the sessions""" + sessions = Session.query.all() + result = sessions_schema.dump(sessions).data + return jsonify({'status': 'success', 'message': None, 'data': result}), 200 + + +@session.route('/', methods=['GET']) +def get_session(id): + """Get info on a session when an id is passed in""" + session = Session.query.get(id) + result = sessions_schema.dump(session).data + return jsonify({'status': 'success', 'message': None, 'data': result}), 200 + + +@session.route('/', methods=['POST']) +def create_session(): + """Create a new Session""" + + app = Application.query.filter_by(id=request.json['app']).first() + questionnaire = Questionnaire.query.filter_by(id=request.json['questionnaire_id']).first() + expected_emotions = request.json['expected_emotions'] + + print(questionnaire) + + # validate application type + if not app: + return {'status': 'error', 'message': 'Invalid application.'}, 400 + + new_session = Session(app=app, expected_emotions=expected_emotions, questionnaire=questionnaire) + + db.session.add(new_session) + db.session.commit() + + result = session_schema.dump(new_session).data + + return jsonify({'status': 'success', 'message': 'Created new session for application with id of {}.'.format(request.json['app']), 'data': result}), 201 diff --git a/migrations/versions/89ee48680e79_.py b/migrations/versions/89ee48680e79_.py new file mode 100644 index 0000000..4f39656 --- /dev/null +++ b/migrations/versions/89ee48680e79_.py @@ -0,0 +1,42 @@ +"""empty message + +Revision ID: 89ee48680e79 +Revises: cb018c771a53 +Create Date: 2019-04-16 04:22:10.614121 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '89ee48680e79' +down_revision = 'cb018c771a53' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key('fk_type_id', 'application', 'application_type', ['type_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_genre_id', 'application', 'genre', ['genre_id'], ['id'], use_alter=True) + op.alter_column('questionnaire', 'post', + existing_type=mysql.TEXT(), + nullable=True) + op.drop_column('questionnaire', 'session_id') + op.create_foreign_key('fk_app_id', 'session', 'application', ['app_id'], ['id'], use_alter=True) + op.create_foreign_key('fk_questionnaire_id', 'session', 'questionnaire', ['questionnaire_id'], ['id'], use_alter=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('fk_questionnaire_id', 'session', type_='foreignkey') + op.drop_constraint('fk_app_id', 'session', type_='foreignkey') + op.add_column('questionnaire', sa.Column('session_id', mysql.INTEGER(display_width=11), autoincrement=False, nullable=False)) + op.alter_column('questionnaire', 'post', + existing_type=mysql.TEXT(), + nullable=False) + op.drop_constraint('fk_genre_id', 'application', type_='foreignkey') + op.drop_constraint('fk_type_id', 'application', type_='foreignkey') + # ### end Alembic commands ### diff --git a/migrations/versions/1ee028de452a_.py b/migrations/versions/cb018c771a53_.py similarity index 52% rename from migrations/versions/1ee028de452a_.py rename to migrations/versions/cb018c771a53_.py index 8c8f074..bd2e305 100644 --- a/migrations/versions/1ee028de452a_.py +++ b/migrations/versions/cb018c771a53_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 1ee028de452a +Revision ID: cb018c771a53 Revises: -Create Date: 2019-04-15 23:52:00.910648 +Create Date: 2019-04-16 01:05:44.197617 """ from alembic import op @@ -10,7 +10,7 @@ # revision identifiers, used by Alembic. -revision = '1ee028de452a' +revision = 'cb018c771a53' down_revision = None branch_labels = None depends_on = None @@ -44,11 +44,39 @@ def upgrade(): sa.Column('display_name', sa.String(length=250), nullable=False), sa.PrimaryKeyConstraint('id') ) + op.create_table('questionnaire', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('pre', sa.TEXT(), nullable=False), + sa.Column('post', sa.TEXT(), nullable=False), + sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('session_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['session_id'], ['session.id'], name='fk_session_id', use_alter=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('session', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('app_id', sa.Integer(), nullable=False), + sa.Column('creation_date', sa.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False), + sa.Column('expected_emotions', sa.TEXT(), nullable=False), + sa.Column('cssi_score', sa.Float(), nullable=False), + sa.Column('latency_scores', sa.TEXT(), nullable=False), + sa.Column('total_latency_score', sa.Float(), nullable=False), + sa.Column('sentiment_scores', sa.TEXT(), nullable=False), + sa.Column('total_sentiment_score', sa.Float(), nullable=False), + sa.Column('questionnaire_id', sa.Integer(), nullable=False), + sa.Column('questionnaire_scores', sa.TEXT(), nullable=True), + sa.Column('total_questionnaire_score', sa.Float(), nullable=False), + sa.ForeignKeyConstraint(['app_id'], ['application.id'], name='fk_app_id', use_alter=True), + sa.ForeignKeyConstraint(['questionnaire_id'], ['questionnaire.id'], name='fk_questionnaire_id', use_alter=True), + sa.PrimaryKeyConstraint('id') + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('session') + op.drop_table('questionnaire') op.drop_table('genre') op.drop_table('application_type') op.drop_table('application')