From 07d01304be029c223798c7a8d416769d0f4d4f57 Mon Sep 17 00:00:00 2001 From: Tobias Date: Fri, 26 Apr 2019 09:42:52 +0200 Subject: [PATCH] added room and recorder api and added length check (for sqlite) --- api/__init__.py | 10 +- api/group_api.py | 9 +- api/recorder_api.py | 255 +++++++++++++++++++++++++++++++++++++++ api/room_api.py | 97 +++++++++++++++ app.db | Bin 102400 -> 167936 bytes models/recorder_model.py | 28 +++-- models/room_model.py | 22 ++-- 7 files changed, 398 insertions(+), 23 deletions(-) create mode 100644 api/recorder_api.py create mode 100644 api/room_api.py diff --git a/api/__init__.py b/api/__init__.py index 8aea364..c2ecde2 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -27,10 +27,13 @@ api_v1 = Api(api_bp, prefix="/v1", version='0.1', title='Vue Test API', api_user = Namespace('user', description="User management namespace", authorizations=api_authorizations) api_group = Namespace('group', description="Group management namespace", authorizations=api_authorizations) +api_room = Namespace('room', description="Room management namespace", authorizations=api_authorizations) +api_recorder = Namespace('recorder', description="Recorder management namespace", authorizations=api_authorizations) api_v1.add_namespace(api_user) -api_v1.add_namespace(api_group - ) +api_v1.add_namespace(api_group) +api_v1.add_namespace(api_room) +api_v1.add_namespace(api_recorder) auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth') # user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user') @@ -40,6 +43,9 @@ from .example_api import * from .auth_api import * from .user_api import * from .group_api import * +from .room_api import * +from .recorder_api import * +#from .group_api import * @api_bp.route('/') diff --git a/api/group_api.py b/api/group_api.py index dc0c7b3..b3dc5f2 100644 --- a/api/group_api.py +++ b/api/group_api.py @@ -12,14 +12,16 @@ from backend import db from backend.api import api_group from backend.models.user_model import Group - group_model = api_group.model('Group', { 'id': fields.String(required=False, description='The group\'s identifier'), 'name': fields.String(required=True, description='The group\'s name'), 'description': fields.String(required=False, description='The group\'s description'), 'users': fields.List(fields.Nested(api_group.model('group_member', - {'id': fields.Integer(), 'first_name': fields.String(), 'last_name': fields.String()})), - required=False, description='Group members.') + {'id': fields.Integer(), 'nickname': fields.String(), + 'first_name': fields.String(), 'last_name': fields.String(), + 'email': fields.String(), 'registered_on': fields.DateTime(), + 'last_seen': fields.DateTime()})), + required=False, description='Group members.') }) @@ -61,6 +63,7 @@ class GroupResource(Resource): return group api_group.abort(404) + @api_group.route('') class GroupList(Resource): @jwt_required diff --git a/api/recorder_api.py b/api/recorder_api.py new file mode 100644 index 0000000..4ae3ac8 --- /dev/null +++ b/api/recorder_api.py @@ -0,0 +1,255 @@ +# Copyright (c) 2019. Tobias Kurze +""" +This module provides functions related to authentication through the API. +For example: listing of available auth providers or registration of users. + +Login through API does not start a new session, but instead returns JWT. +""" +from flask_jwt_extended import jwt_required +from flask_restplus import fields, Resource + +from backend import db +from backend.api import api_recorder +from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand + +recorder_model = api_recorder.model('Recorder', { + 'id': fields.String(required=False, description='The recorder\'s identifier'), + 'created_at': fields.DateTime(required=False, description='Creation date of the recorder'), + 'name': fields.String(required=True, description='The recorder\'s name'), + 'description': fields.String(required=False, description='The recorder\'s description'), + 'ip': fields.String(required=False, description='The recorder\'s IP address'), + 'network_name': fields.String(required=False, description='The recorder\'s network name'), + 'recorder_model': fields.Nested(api_recorder.model('recorder_model', + {'id': fields.Integer(), 'name': fields.String()}), + required=False, + description='Model of the recorder.'), + 'room': fields.Nested(api_recorder.model('recorder_room', + {'id': fields.Integer(), 'name': fields.String(), + 'number': fields.String(), 'alternate_name': fields.String()}), + required=False, + description='Room in which the recorder is located.') +}) + + +# == + +@api_recorder.route('/') +@api_recorder.response(404, 'Recorder not found') +@api_recorder.param('id', 'The recorder identifier') +class RecorderResource(Resource): + @jwt_required + @api_recorder.doc('get_recorder') + @api_recorder.marshal_with(recorder_model) + def get(self, id): + """Fetch a recorder given its identifier""" + recorder = Recorder.query.get(id) + if recorder is not None: + return recorder + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('delete_todo') + @api_recorder.response(204, 'Todo deleted') + def delete(self, id): + """Delete a recorder given its identifier""" + recorder = Recorder.query.get(id) + if recorder is not None: + db.session.delete(recorder) + db.session.commit() + return '', 204 + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('update_recorder') + @api_recorder.expect(recorder_model) + @api_recorder.marshal_with(recorder_model) + def put(self, id): + """Update a recorder given its identifier""" + num_rows_matched = Recorder.query.filter_by(id=id).update(api_recorder.payload) + if num_rows_matched < 1: + api_recorder.abort(404) + db.session.commit() + return "ok" + + + +@api_recorder.route('') +class RecorderList(Resource): + @jwt_required + @api_recorder.doc('recorders') + @api_recorder.marshal_list_with(recorder_model) + def get(self): + """ + List all recorders + :return: recorders + """ + return Recorder.get_all() + + @jwt_required + @api_recorder.doc('create_recorder') + @api_recorder.expect(recorder_model) + @api_recorder.marshal_with(recorder_model, code=201) + def post(self): + recorder = Recorder(**api_recorder.payload) + db.session.add(recorder) + db.session.commit() + return recorder + + +# == + + +recorder_model_model = api_recorder.model('Recorder Model', { + 'id': fields.String(required=False, description='The recorder model\'s identifier'), + 'name': fields.String(required=True, description='The recorder model\'s name'), + 'notes': fields.String(required=False, description='The recorder model\'s notes'), + + 'recorders': fields.List(fields.Nested(api_recorder.model('recorder_model', + {'id': fields.Integer(), 'name': fields.String(), + 'network_name': fields.String(), + 'ip': fields.String()})), required=False, + description='Model of the recorder.'), + 'commands': fields.List(fields.Nested(api_recorder.model('recorder_model_commands', + {'id': fields.Integer(), 'name': fields.String()})), + required=False, + description='Room in which the recorder is located.') +}) + + +@api_recorder.route('/model/') +@api_recorder.response(404, 'Recorder Model not found') +@api_recorder.param('id', 'The recorder model identifier') +class RecorderModelResource(Resource): + @jwt_required + @api_recorder.doc('get_recorder_model') + @api_recorder.marshal_with(recorder_model_model) + def get(self, id): + """Fetch a recorder model given its identifier""" + recorder_model = RecorderModel.query.get(id) + if recorder_model is not None: + return recorder_model + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('delete_recorder_model') + @api_recorder.response(204, 'Recorder model deleted') + def delete(self, id): + """Delete a recorder model given its identifier""" + recorder_model = RecorderModel.query.get(id) + if recorder_model is not None: + db.session.delete(recorder_model) + db.session.commit() + return '', 204 + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('update_recorder_model') + @api_recorder.expect(recorder_model_model) + @api_recorder.marshal_with(recorder_model_model) + def put(self, id): + """Update a recorder_model given its identifier""" + num_rows_matched = RecorderModel.query.filter_by(id=id).update(api_recorder.payload) + if num_rows_matched < 1: + api_recorder.abort(404) + db.session.commit() + return "ok" + + +@api_recorder.route('/model') +class RecorderModelList(Resource): + @jwt_required + @api_recorder.doc('recorders') + @api_recorder.marshal_list_with(recorder_model_model) + def get(self): + """ + List all recorder models + :return: recorder models + """ + return Recorder.get_all() + + @jwt_required + @api_recorder.doc('create_recorder') + @api_recorder.expect(recorder_model_model) + @api_recorder.marshal_with(recorder_model_model, code=201) + def post(self): + recorder = Recorder(**api_recorder.payload) + db.session.add(recorder) + db.session.commit() + return recorder + + +# == + + +recorder_command_model = api_recorder.model('Recorder Command', { + 'id': fields.String(required=False, description='The recorder command\'s identifier'), + 'name': fields.String(required=True, description='The recorder command\'s name'), + 'description': fields.String(required=False, description='The recorder command\'s description'), + 'command': fields.String(required=True, description='The recorder command\'s name'), + 'recorder_models': fields.List(fields.Nested(api_recorder.model('recorder_command_models', + {'id': fields.Integer(), 'name': fields.String()})), + required=False, + description='Recorder models associated with the command.'), +}) + + +@api_recorder.route('/command/') +@api_recorder.response(404, 'Recorder Command not found') +@api_recorder.param('id', 'The recorder command identifier') +class RecorderCommandResource(Resource): + @jwt_required + @api_recorder.doc('get_recorder_command') + @api_recorder.marshal_with(recorder_command_model) + def get(self, id): + """Fetch a recorder command given its identifier""" + recorder_command = RecorderCommand.query.get(id) + if recorder_command is not None: + return recorder_command + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('delete_recorder_command') + @api_recorder.response(204, 'Recorder_command deleted') + def delete(self, id): + """Delete a recorder command given its identifier""" + recorder_command = RecorderCommand.query.get(id) + if recorder_command is not None: + db.session.delete(recorder_command) + db.session.commit() + return '', 204 + api_recorder.abort(404) + + @jwt_required + @api_recorder.doc('update_recorder_command') + @api_recorder.expect(recorder_command_model) + @api_recorder.marshal_with(recorder_command_model) + def put(self, id): + """Update a recorder command given its identifier""" + num_rows_matched = RecorderCommand.query.filter_by(id=id).update(api_recorder.payload) + if num_rows_matched < 1: + api_recorder.abort(404) + db.session.commit() + return "ok" + + +@api_recorder.route('/command') +class RecorderCommandList(Resource): + @jwt_required + @api_recorder.doc('recorder_commands') + @api_recorder.marshal_list_with(recorder_command_model) + def get(self): + """ + List all recorders commands + :return: recorder commands + """ + return RecorderCommand.get_all() + + @jwt_required + @api_recorder.doc('create_recorder_commands') + @api_recorder.expect(recorder_command_model) + @api_recorder.marshal_with(recorder_command_model, code=201) + def post(self): + recorder_command = RecorderCommand(**api_recorder.payload) + db.session.add(recorder_command) + db.session.commit() + return recorder_command diff --git a/api/room_api.py b/api/room_api.py new file mode 100644 index 0000000..a67d77b --- /dev/null +++ b/api/room_api.py @@ -0,0 +1,97 @@ +# Copyright (c) 2019. Tobias Kurze +""" +This module provides functions related to authentication through the API. +For example: listing of available auth providers or registration of users. + +Login through API does not start a new session, but instead returns JWT. +""" +from flask_jwt_extended import jwt_required +from flask_restplus import fields, Resource +from sqlalchemy import exc + +from backend import db, app +from backend.api import api_room +from backend.models.room_model import Room + +room_model = api_room.model('Room', { + 'id': fields.String(required=False, description='The room\'s identifier'), + 'created_at': fields.DateTime(required=False, description='Creation date of the room info'), + 'name': fields.String(required=True, description='The room\'s name'), + 'alternate_name': fields.String(required=False, description='The room\'s alternate name'), + 'comment': fields.String(required=False, description='The room\'s comment'), + 'number': fields.String(required=True, description='The room\'s number'), + 'recorder': fields.List(fields.Nested(api_room.model('room_recorder', + {'id': fields.Integer(), 'name': fields.String(), + 'ip': fields.String(), 'network_name': fields.String()})), + required=False, description='Room members.') +}) + + +@api_room.route('/') +@api_room.response(404, 'Room not found') +@api_room.param('id', 'The room identifier') +class RoomResource(Resource): + @jwt_required + @api_room.doc('get_room') + @api_room.marshal_with(room_model) + def get(self, id): + """Fetch a user given its identifier""" + room = Room.query.get(id) + if room is not None: + return room + api_room.abort(404) + + @jwt_required + @api_room.doc('delete_todo') + @api_room.response(204, 'Todo deleted') + def delete(self, id): + '''Delete a task given its identifier''' + room = Room.query.get(id) + if room is not None: + db.session.delete(room) + db.session.commit() + return '', 204 + api_room.abort(404) + + @jwt_required + @api_room.doc('update_room') + @api_room.expect(room_model) + @api_room.marshal_with(room_model) + def put(self, id): + '''Update a task given its identifier''' + num_rows_matched = Room.query.filter_by(id=id).update(api_room.payload) + db.session.commit() + # if room is not None: + # app.logger.debug(room) + # room.update(api_room.payload) + # db.session.commit() + # return room + return "ok" + api_room.abort(404) + + +@api_room.route('') +class RoomList(Resource): + @jwt_required + @api_room.doc('rooms') + @api_room.marshal_list_with(room_model) + def get(self): + """ + List all rooms + :return: rooms + """ + return Room.get_all() + + @jwt_required + @api_room.doc('create_room') + @api_room.expect(room_model) + @api_room.marshal_with(room_model, code=201) + def post(self): + room = Room(**api_room.payload) + db.session.add(room) + try: + db.session.commit() + return room + except exc.IntegrityError as e: + db.session.rollback() + return str(e.detail), 400 diff --git a/app.db b/app.db index d09516d424765c29e9c666dd17af64f37ae46e59..c8e134227db492d42b469f86b02d7835808a7e2c 100644 GIT binary patch delta 2996 zcmeHJU2NM_6!vxe7blL>pTud}G+w*4vDU72+N`wu%_4W}Iub2SH>NSNtnnbwBqhz5 zCInkEn6!s6ru%@zBsTQ{Y1K4ABVOQP@B&gLG&T@>C`=PVJWTvlph=a65VjM$b=yh6 z%U+1$)D)f_A(a zxZp;-NkK2;O)!nSK|OpG_bcdSyx(0*axND*-%v~)8<$T^$_4eQD?h+@qj(*`ZE-2~ z`K+Rf)|ZYu;j>f+s41VP1^`||a3h_n`S8BAC0aW*<{-JwPEb0f=`t*?zQf8n8;MKd zgv2MpyY@-^N-5kE=z(ceT&*h8qBs^^EtgC>zR8|&fVGBcWF+ufB zvsw2t%d`z;lpe8IU&BalTNjvWgwHWv_yWM`e&1ckEm2>S~=uQ2Ks3+FEi3UUu&e`D_{$Q>4ba0M&>aHVBu8FT*WpFN84 zW$gY}Y)3G}i#-FOz5($mg~+|3&B0)|*x%XR+qpv09|}z`9G&K!1B`tRVaa*K@w+3& zeDg2a{s|!LbNEkBnC*QY`y3|z1HRK0zT4-(3_L<@{GagJt@NY_KMO+CE`&q&B93<) z2W*SBTeeqhZS-yW6m^Z-OXR5ra)JB;zk&UT)uQiNzDM?=S#&FEyA-Yuo!#C_ay}n8 z7c-^3o|J8Crn;Ks^jJYo73Fj?RfI9STX}c!nP4a+bnvvbkk4n6nKVBXO-Q??xC&(^ zj9W^qEXv8nllj6+N%clpk_G!J8cg>++=R8lGdAY5s%bD5mxgvn`C;i$pn8zN$E86j zE=42K2(My%Ad}Ygj7Ep{k4nmP!Zclewk9wsHJz=zhoRoD={JeBlW3u0a^*(FR zU6<_9M?%+z)zY9Y4Ro#7rEVQtx&_-yTi0E>!GrZ5-f9A%1^x_z*oLVX4_nYS&Ve|w{jHfU=_ z^|x86LzLS7EOj!cjSyGrp)sx2;ibqwrq%JDecD5E!^7ZQjVWUvv)s^MJua%Vw|ZWK zIm)_pZR7ZWlG~;w@HXXf8c2C&x?^<@^C?;1v1*->ZL5m7i!{n=h17p&-_xr$Rjn1rfb?^#xkcVP?MhW|;4rUv|$gOO>ck zM+ilD9fto&Je}21(#*)-Eu^DWr2JO0^0s{0yW?qlm)!fFvKzabu4_G;jvbfjm~{+^ zU&J}w5q=5%XchJkl@4%mEX-cizyQ6qS%Y2xoqelAN{lRILriNx48m-z0k1$~-y6^e zAtp6B3$oWu2=e8JCcG4`1lWT%9D^YJBxv-pV1S>l@gCqj4L)|V!xrS4Pb$R$-0(STf>&gX68_DrczutN?JMNizx z*^{}+T!|ykE-0di0?`A)fnX77(eCBu^wgAnw@7!(L)>K@k)LFbd?XuWoqXjk&qz|m z1MshhAp=)xLw<#2uPdF(<*=Ez7Xd8S6}D#7v$}o1@m*lxdA7 X%;CiU7t2ZwS525$nNWFFO~LpJwa}uF diff --git a/models/recorder_model.py b/models/recorder_model.py index 390d3da..94a3f11 100644 --- a/models/recorder_model.py +++ b/models/recorder_model.py @@ -16,9 +16,9 @@ metadata = MetaData() # This is the association table for the many-to-many relationship between # groups and permissions. -recorder_recorder_command_table = db.Table('recorder_recorder_command', - db.Column('recorder_id', db.Integer, - db.ForeignKey('recorder.id', +recorder_model_recorder_command_table = db.Table('recorder_model_recorder_command', + db.Column('recorder_model_id', db.Integer, + db.ForeignKey('recorder_model.id', onupdate="CASCADE", ondelete="CASCADE"), primary_key=True), @@ -29,13 +29,26 @@ recorder_recorder_command_table = db.Table('recorder_recorder_command', primary_key=True)) +class RecorderModel(db.Model): + id = db.Column(db.Integer, autoincrement=True, primary_key=True) + model_name = db.Column(db.Unicode(63), unique=True, nullable=False) + notes = db.Column(db.Unicode(255), unique=False, nullable=True, default=None) + recorder_commands = db.relationship('RecorderCommand', secondary=recorder_model_recorder_command_table, + back_populates='recorder_models') + recorders = db.relationship('Recorder', back_populates='recorder_model') + + class Recorder(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) name = db.Column(db.Unicode(63), unique=True, nullable=False) description = db.Column(db.Unicode(255), unique=False, nullable=True, default="") + room_id = db.Column(db.Integer, db.ForeignKey('room.id')) room = db.relationship('Room', uselist=False, back_populates='recorder') # one-to-one relation (uselist=False) - recorder_commands = db.relationship('RecorderCommand', secondary=recorder_recorder_command_table, back_populates='recorders') + ip = db.Column(db.String(15), unique=True, nullable=True, default=None) + network_name = db.Column(db.String(127), unique=True, nullable=True, default=None) + recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id')) + recorder_model = db.relationship('RecorderModel', back_populates='recorders') def __init__(self, **kwargs): super(Recorder, self).__init__(**kwargs) @@ -56,7 +69,7 @@ class Recorder(db.Model): Return all groups :return: """ - return Group.query.all() + return Recorder.query.all() def __str__(self): return self.name @@ -73,6 +86,7 @@ class RecorderCommand(db.Model): """Table containing permissions associated with groups.""" id = db.Column(db.Integer, autoincrement=True, primary_key=True) name = db.Column(db.Unicode(63), unique=True, nullable=False) - description = db.Column(db.Unicode(511)) - recorders = db.relationship(Recorder, secondary=recorder_recorder_command_table, + description = db.Column(db.Unicode(511), nullable=True, default=None) + command = db.Column(db.String(2047), nullable=False) + recorder_models = db.relationship('RecorderModel', secondary=recorder_model_recorder_command_table, back_populates='recorder_commands') diff --git a/models/room_model.py b/models/room_model.py index b1455fc..f855e53 100644 --- a/models/room_model.py +++ b/models/room_model.py @@ -4,12 +4,13 @@ Models for lecture recorder """ import json -from sqlalchemy import MetaData +from sqlalchemy import MetaData, CheckConstraint from backend import db, app, login_manager import re from sqlalchemy import or_ from datetime import datetime, timedelta +from backend.models.recorder_model import Recorder metadata = MetaData() @@ -19,13 +20,21 @@ class Room(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) name = db.Column(db.Unicode(127), unique=True, nullable=False) + alternate_name = db.Column(db.Unicode(127), unique=True, nullable=True, default=None) + comment = db.Column(db.Unicode(2047), unique=False, nullable=True, default="") number = db.Column(db.Unicode(63), unique=True, nullable=True) - description = db.Column(db.Unicode(255), unique=False, nullable=True, default="") + recorder = db.relationship('Recorder', uselist=False, back_populates='room') # one-to-one relation (uselist=False) + __table_args__ = ( + CheckConstraint('length(name) > 2', + name='name_min_length'), + ) + def __init__(self, **kwargs): super(Room, self).__init__(**kwargs) + @staticmethod def get_by_name(name): """ @@ -52,12 +61,3 @@ class Room(db.Model): def toJSON(self): return json.dumps(self.to_dict(), default=lambda o: o.__dict__, sort_keys=True, indent=4) - - -class Permission(db.Model): - """Table containing permissions associated with groups.""" - id = db.Column(db.Integer, autoincrement=True, primary_key=True) - name = db.Column(db.Unicode(63), unique=True, nullable=False) - description = db.Column(db.Unicode(511)) - groups = db.relationship(Room, secondary=room_permission_table, - back_populates='permissions')