diff --git a/Pipfile b/Pipfile index a505d45..15fca1e 100644 --- a/Pipfile +++ b/Pipfile @@ -23,6 +23,8 @@ flask-jwt-extended = "*" ssh2-python = "*" update = "*" flask-cors = "*" +html5lib = "*" +beautifulsoup4 = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 1583b6d..93e69c0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "79518b33814e3afda968f02d6de17856076a2d7a81183eb7ee1c219eccc114cc" + "sha256": "10fe25a82139020ea4ed6a7615b6f77965ebb4a96824b6f0d8989da081db892f" }, "pipfile-spec": 6, "requires": { @@ -63,6 +63,15 @@ ], "version": "==1.10.1" }, + "beautifulsoup4": { + "hashes": [ + "sha256:05668158c7b85b791c5abde53e50265e16f98ad601c402ba44d70f96c4159612", + "sha256:25288c9e176f354bf277c0a10aa96c782a6a18a17122dba2e8cec4a97e03343b", + "sha256:f040590be10520f2ea4c2ae8c3dae441c7cfff5308ec9d58a0ec0c1b8f81d469" + ], + "index": "pypi", + "version": "==4.8.0" + }, "certifi": { "hashes": [ "sha256:046832c04d4e752f37383b628bc601a7ea7211496b4638f6514d0e5b9acc4939", @@ -252,10 +261,10 @@ }, "flask-restplus": { "hashes": [ - "sha256:3fad697e1d91dfc13c078abcb86003f438a751c5a4ff41b84c9050199d2eab62", - "sha256:cdc27b5be63f12968a7f762eaa355e68228b0c904b4c96040a314ba7dc6d0e69" + "sha256:a15d251923a8feb09a5d805c2f4d188555910a42c64d58f7dd281b8cac095f1b", + "sha256:a66e442d0bca08f389fc3d07b4d808fc89961285d12fb8013f7cf15516fa9f5c" ], - "version": "==0.12.1" + "version": "==0.13.0" }, "flask-restplus-patched": { "hashes": [ @@ -292,6 +301,14 @@ ], "version": "==0.17.1" }, + "html5lib": { + "hashes": [ + "sha256:20b159aa3badc9d5ee8f5c647e5efd02ed2a66ab8d354930bd9ff139fc1dc0a3", + "sha256:66cb0dcfdbbc4f9c3ba1a63fdb511ffdbd4f513b2b6d81b80cd26ce6b3fb3736" + ], + "index": "pypi", + "version": "==1.0.1" + }, "idna": { "hashes": [ "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", @@ -361,10 +378,10 @@ }, "marshmallow": { "hashes": [ - "sha256:9cedfc5b6f568d57e8a2cf3d293fbd81b05e5ef557854008d03e25660a39ccfd", - "sha256:a4d99922116a76e5abd8f997ec0519086e24814b7e1e1344bebe2a312ba50235" + "sha256:43bef4d33b7adb1f66eba8074095208b38dec96bed51f7a9bf2e687750f226d8", + "sha256:b240f5e14bc641c257f4b7bda3951d7e71963ebf66bd519078267f1f961cbd15" ], - "version": "==2.19.5" + "version": "==2.20.1" }, "oic": { "hashes": [ @@ -396,7 +413,6 @@ }, "pycparser": { "hashes": [ - "sha256:50f84b9531cf541192a39303722ea4d69cab456fc1104ca47f66b9b8dd7be740", "sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3" ], "version": "==2.19" @@ -512,6 +528,13 @@ ], "version": "==1.12.0" }, + "soupsieve": { + "hashes": [ + "sha256:72b5f1aea9101cf720a36bb2327ede866fd6f1a07b1e87c92a1cc18113cbc946", + "sha256:e4e9c053d59795e440163733a7fec6c5972210e1790c507e4c7b051d6c5259de" + ], + "version": "==1.9.2" + }, "sqlalchemy": { "hashes": [ "sha256:217e7fc52199a05851eee9b6a0883190743c4fb9c8ac4313ccfceaffd852b0ff" @@ -595,6 +618,13 @@ ], "version": "==5.4.0" }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, "werkzeug": { "hashes": [ "sha256:87ae4e5b5366da2347eb3116c0e6c681a0e939a33b2805e2c0cbd282664932c4", diff --git a/__main__.py b/__main__.py index 0b67d80..ca2c74e 100644 --- a/__main__.py +++ b/__main__.py @@ -8,6 +8,8 @@ import ssl from jinja2.exceptions import TemplateNotFound from backend import app, db +from backend.models import pre_fill_table +from backend.tools.model_updater import update_recorder_models_database def main(): @@ -28,8 +30,11 @@ def main(): except Exception as e: logging.CRITICAL(e) + pre_fill_table() + update_recorder_models_database() - app.run(debug=True) + + app.run(debug=True, host="0.0.0.0") if __name__ == '__main__': diff --git a/api/__init__.py b/api/__init__.py index c2ecde2..439d94e 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -29,11 +29,17 @@ api_user = Namespace('user', description="User management namespace", authorizat 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_virtual_command = Namespace('virtual_command', description="Virtual command namespace", + authorizations=api_authorizations) +api_cron_job = Namespace('cron_job', description="Cron job namespace", + authorizations=api_authorizations) api_v1.add_namespace(api_user) api_v1.add_namespace(api_group) api_v1.add_namespace(api_room) api_v1.add_namespace(api_recorder) +api_v1.add_namespace(api_virtual_command) +api_v1.add_namespace(api_cron_job) auth_api_bp = Blueprint('auth_api', __name__, url_prefix='/api/auth') # user_api_bp = Blueprint('user_api', __name__, url_prefix='/api/user') @@ -45,7 +51,9 @@ from .user_api import * from .group_api import * from .room_api import * from .recorder_api import * -#from .group_api import * + + +# from .group_api import * @api_bp.route('/') diff --git a/api/example_api.py b/api/example_api.py index 1e00900..17c0192 100644 --- a/api/example_api.py +++ b/api/example_api.py @@ -1,3 +1,4 @@ + import datetime import ipaddress import json diff --git a/api/recorder_api.py b/api/recorder_api.py index 695efe6..5970c03 100644 --- a/api/recorder_api.py +++ b/api/recorder_api.py @@ -5,12 +5,11 @@ For example: listing of available auth providers or registration of users. Login through API does not start a new session, but instead returns JWT. """ -import inspect -import pkgutil +from datetime import datetime from pprint import pprint from flask_jwt_extended import jwt_required -from flask_restplus import fields, Resource +from flask_restplus import fields, Resource, inputs from backend import db, app from backend.api import api_recorder @@ -21,17 +20,23 @@ import backend.recorder_adapters as r_a 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'), + 'last_time_modified': fields.DateTime(required=False, description='Creation date of the recorder'), 'name': fields.String(min_length=3, required=True, description='The recorder\'s name'), 'description': fields.String(required=False, description='The recorder\'s description'), + 'locked': fields.Boolean(required=False, description='Indicates whether the recorder settings can be altered'), + 'lock_message': fields.String(required=False, description='Optional: message explaining lock state'), + 'offline': fields.Boolean(required=False, + description='Should be set when recorder is disconnected for maintenance, etc.'), 'ip': fields.String(required=False, description='The recorder\'s IP address'), 'network_name': fields.String(required=False, description='The recorder\'s network name'), 'ssh_port': fields.Integer(required=True, default=22, description='The recorder\'s SSH port number'), 'telnet_port': fields.Integer(required=True, default=23, description='The recorder\'s telnet port number'), - 'use_telnet_instead_ssh': fields.Boolean(required=False, default=False, - description='If this is set, telnet will be used instead of ssh. ' - 'This might require specific commands.'), + # 'use_telnet_instead_ssh': fields.Boolean(required=False, default=False, + # description='If this is set, telnet will be used instead of ssh. ' + # 'This might require specific commands.'), 'recorder_model': fields.Nested(api_recorder.model('recorder_model', - {'id': fields.Integer(), 'name': fields.String()}), + {'id': fields.Integer(), + 'name': fields.String(attribute="model_name", )}), required=False, allow_null=True, skip_none=False, @@ -39,30 +44,41 @@ recorder_model = api_recorder.model('Recorder', { 'room': fields.Nested(api_recorder.model('recorder_room', {'id': fields.Integer(), 'name': fields.String(), 'number': fields.String(), 'alternate_name': fields.String()}), - required=False, + r0equired=False, allow_null=True, skip_none=False, - description='Room in which the recorder is located.') + description='Room in which the recorder is located.'), + 'virtual_commands': fields.List(fields.Nested(api_recorder.model('recorder_virtual_commands', + {'id': fields.Integer(), + 'name': fields.String()}))) }) 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'), + 'alternative_name': fields.String(required=False, description='The recorder command\'s alternative name'), + 'disabled': fields.Boolean(required=False, description='Indicates if the recorder command is disabled'), + 'created_at': fields.DateTime(required=False, description='Creation date of the recorder'), 'last_time_modified': fields.DateTime(required=False), 'description': fields.String(required=False, description='The recorder command\'s description'), 'parameters': fields.Raw(required=True, description='The recorder parameters'), 'recorder_model': fields.Nested(api_recorder.model('recorder_command_models', - {'id': fields.Integer(), 'name': fields.String()})), + {'id': fields.Integer(), + 'name': fields.String(attribute="model_name", )})), }) recorder_model_model = api_recorder.model('Recorder Model', { 'id': fields.String(required=False, description='The recorder model\'s identifier'), 'name': fields.String(attribute="model_name", required=True, description='The recorder model\'s name'), + 'created_at': fields.DateTime(required=False, description='Creation date of the recorder'), 'last_time_modified': fields.DateTime(required=False), 'notes': fields.String(required=False, description='The recorder model\'s notes'), + 'requires_username': fields.Boolean(), + 'requires_password': fields.Boolean(), 'recorders': fields.List(fields.Nested(api_recorder.model('recorder_model', - {'id': fields.Integer(), 'name': fields.String(), + {'id': fields.Integer(), + 'name': fields.String(attribute="model_name", ), 'network_name': fields.String(), 'ip': fields.String()})), required=False, description='Model of the recorder.'), @@ -98,12 +114,36 @@ class RecorderResource(Resource): return '', 204 api_recorder.abort(404) + recorder_update_parser = api_recorder.parser() + recorder_update_parser.add_argument('name', type=str, required=False, nullable=False, store_missing=False) + recorder_update_parser.add_argument('network_name', type=inputs.regex(inputs.netloc_regex), required=False, store_missing=False) + recorder_update_parser.add_argument('ip', type=inputs.ipv4, required=False, store_missing=False) + recorder_update_parser.add_argument('ip6', type=inputs.ipv6, required=False, store_missing=False) + recorder_update_parser.add_argument('ssh_port', type=inputs.int_range(0,65535), required=False, default=22, store_missing=False) + recorder_update_parser.add_argument('telnet_port', type=inputs.int_range(0,65535), required=False, default=23, store_missing=False) + recorder_update_parser.add_argument('room_id', type=int, required=False, store_missing=False) + recorder_update_parser.add_argument('offline', type=inputs.boolean, required=False, default=False, store_missing=False) + recorder_update_parser.add_argument('locked', type=inputs.boolean, required=False, default=False, store_missing=False) + recorder_update_parser.add_argument('lock_message', type=str, required=False, nullable=True, default=None, + store_missing=False) + recorder_update_parser.add_argument('model_id', type=int, required=False, store_missing=False) + recorder_update_parser.add_argument('description', type=str, required=False, nullable=True, default=None, + store_missing=False) + recorder_update_parser.add_argument('virtual_command_ids', action='split', nullable=True, default=[], + required=False, store_missing=False) + @jwt_required @api_recorder.doc('update_recorder') @api_recorder.expect(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) + args = self.recorder_update_parser.parse_args(strict=True) + args['last_time_modified'] = datetime.utcnow() + pprint(args) + + num_rows_matched = Recorder.query.filter_by(id=id).update(args) + print(num_rows_matched) + if num_rows_matched < 1: api_recorder.abort(404) db.session.commit() @@ -205,21 +245,13 @@ class RecorderCommandResource(Resource): 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) + recorder_command_model_parser = api_recorder.parser() + recorder_command_model_parser.add_argument('description', type=str, required=False) + recorder_command_model_parser.add_argument('alternative_name', type=str, required=False) @jwt_required @api_recorder.doc('update_recorder_command') - @api_recorder.expect(recorder_command_model) + @api_recorder.expect(recorder_command_model_parser) @api_recorder.marshal_with(recorder_command_model) def put(self, id): """Update a recorder command given its identifier""" @@ -241,13 +273,3 @@ class RecorderCommandList(Resource): :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 index 9f29f11..ad93a45 100644 --- a/api/room_api.py +++ b/api/room_api.py @@ -21,6 +21,8 @@ room_model = api_room.model('Room', { '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'), + 'building_name': fields.String(required=False, description='The building\'s name'), + 'building_number': fields.String(required=False, description='The building\'s number'), 'recorder': fields.Nested(api_room.model('room_recorder', {'id': fields.Integer(), 'name': fields.String(), 'ip': fields.String(), 'network_name': fields.String()}), diff --git a/api/virtual_command_api.py b/api/virtual_command_api.py new file mode 100644 index 0000000..68f7698 --- /dev/null +++ b/api/virtual_command_api.py @@ -0,0 +1,127 @@ +# 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. +""" +import inspect +import pkgutil +from pprint import pprint + +from flask_jwt_extended import jwt_required +from flask_restplus import fields, Resource + +from backend import db, app +from backend.api import api_virtual_command +from backend.models.recorder_model import Recorder, RecorderModel, RecorderCommand +from backend.models.room_model import Room +import backend.recorder_adapters as r_a + +virtual_command_model = api_virtual_command.model('VirtualCommand', { + '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(min_length=3, required=True, description='The recorder\'s name'), + 'description': fields.String(required=False, description='The recorder\'s description'), + + 'parent_virtual_command': fields.Nested('virtual_command_model', + required=False, + allow_null=True, + skip_none=False, + description='Parent virtual command.'), + 'room': fields.Nested(api_virtual_command.model('recorder_room', + {'id': fields.Integer(), 'name': fields.String(), + 'number': fields.String(), 'alternate_name': fields.String()}), + r0equired=False, + allow_null=True, + skip_none=False, + description='Room in which the recorder is located.') +}) + + +# == + +@api_virtual_command.route('/') +@api_virtual_command.response(404, 'Recorder not found') +@api_virtual_command.param('id', 'The recorder identifier') +class VirtualCommandResource(Resource): + @jwt_required + @api_virtual_command.doc('get_recorder') + @api_virtual_command.marshal_with(virtual_command_model, skip_none=False) + def get(self, id): + """Fetch a recorder given its identifier""" + recorder = Recorder.query.get(id) + if recorder is not None: + return recorder + api_virtual_command.abort(404) + + @jwt_required + @api_virtual_command.doc('delete_todo') + @api_virtual_command.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_virtual_command.abort(404) + + virtual_command_model_parser = api_virtual_command.parser() + virtual_command_model_parser.add_argument('notes', type=str, required=True) + + @jwt_required + @api_virtual_command.doc('update_recorder') + @api_virtual_command.expect(virtual_command_model_parser) + def put(self, id): + """Update a recorder given its identifier""" + num_rows_matched = Recorder.query.filter_by(id=id).update(api_virtual_command.payload) + if num_rows_matched < 1: + api_virtual_command.abort(404) + db.session.commit() + return "ok" + + +@api_virtual_command.route('') +class RecorderList(Resource): + @jwt_required + @api_virtual_command.doc('recorders') + @api_virtual_command.marshal_list_with(virtual_command_model, skip_none=False) + def get(self): + """ + List all recorders + :return: recorders + """ + return Recorder.get_all() + + virtual_command_model_parser = api_virtual_command.parser() + virtual_command_model_parser.add_argument('notes', type=str, required=True) + + @jwt_required + @api_virtual_command.doc('create_recorder') + @api_virtual_command.expect(virtual_command_model_parser) + @api_virtual_command.marshal_with(virtual_command_model, skip_none=False, code=201) + def post(self): + if "room_id" in api_virtual_command.payload: + if api_virtual_command.payload["room_id"] is None: + api_virtual_command.payload["room"] = None + else: + room = Room.query.get(api_virtual_command.payload["room_id"]) + if room is not None: + api_virtual_command.payload["room"] = room + else: + return "specified room (id: {}) does not exist!".format(api_virtual_command.payload["room_id"]), 404 + if "recorder_model_id" in api_virtual_command.payload: + if api_virtual_command.payload["recorder_model_id"] is None: + api_virtual_command.payload["recorder_model"] = None + else: + rec_model = RecorderModel.query.get(api_virtual_command.payload["recorder_model_id"]) + if rec_model is not None: + api_virtual_command.payload["recorder_model"] = rec_model + else: + return "specified recorder model (id: {}) does not exist!".format( + api_virtual_command.payload["recorder_model_id"]), 404 + recorder = Recorder(**api_virtual_command.payload) + db.session.add(recorder) + db.session.commit() + return recorder diff --git a/app.db b/app.db deleted file mode 100644 index bd2019a..0000000 Binary files a/app.db and /dev/null differ diff --git a/manage.py b/manage.py index 1b36e0e..21ff429 100755 --- a/manage.py +++ b/manage.py @@ -1,14 +1,17 @@ #!/usr/bin/env python - +import os, sys +sys.path.append(os.path.join(os.path.dirname(__file__), os.path.pardir)) import os import unittest import coverage -from backend import app, db + from flask_script import Manager from flask_migrate import Migrate, MigrateCommand +from backend import app, db + COV = coverage.coverage( branch=True, include='app/*', diff --git a/models/__init__.py b/models/__init__.py index 4bfbabc..93e79ca 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -4,3 +4,6 @@ Import all models... from backend.models.example_model import * from backend.models.user_model import * from backend.models.post_model import * +from backend.models.recorder_model import * +from backend.models.room_model import * +from backend.models.virtual_command_model import * diff --git a/models/recorder_model.py b/models/recorder_model.py index 876121b..4c0fe07 100644 --- a/models/recorder_model.py +++ b/models/recorder_model.py @@ -6,6 +6,7 @@ import json from sqlalchemy import MetaData from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import validates from backend import db, app, login_manager import re @@ -19,6 +20,7 @@ metadata = MetaData() class RecorderModel(db.Model): id = db.Column(db.Integer, autoincrement=True, primary_key=True) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) last_time_modified = db.Column(db.DateTime, nullable=True, default=None) record_adapter_id = db.Column(db.Unicode(63), unique=True, nullable=False) model_name = db.Column(db.Unicode(63), unique=True, nullable=False) @@ -26,6 +28,8 @@ class RecorderModel(db.Model): recorder_commands = db.relationship('RecorderCommand', back_populates='recorder_model') recorders = db.relationship('Recorder', back_populates='recorder_model') checksum = db.Column(db.String(63), unique=True, nullable=False) + requires_user = db.Column(db.Boolean) + requires_password = db.Column(db.Boolean) @staticmethod def get_all(): @@ -49,13 +53,19 @@ class Recorder(db.Model): created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) last_time_modified = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) name = db.Column(db.Unicode(63), unique=True, nullable=False) + locked = db.Column(db.Boolean, default=False) + lock_message = db.Column(db.String, nullable=True, default=None) + offline = db.Column(db.Boolean, default=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) ip = db.Column(db.String(15), unique=True, nullable=True, default=None) + ip6 = db.Column(db.String(46), unique=True, nullable=True, default=None) network_name = db.Column(db.String(127), unique=True, nullable=True, default=None) telnet_port = db.Column(db.Integer, unique=False, nullable=False, default=23) ssh_port = db.Column(db.Integer, unique=False, nullable=False, default=22) + username = db.column(db.String(127)) + password = db.column(db.String(127)) recorder_model_id = db.Column(db.Integer, db.ForeignKey('recorder_model.id')) recorder_model = db.relationship('RecorderModel', back_populates='recorders') virtual_commands = db.relationship('VirtualCommand', secondary=virtual_command_recorder_table, back_populates='recorders') @@ -71,6 +81,11 @@ class Recorder(db.Model): def get_all(): return Recorder.query.all() + @validates('name') + def validate_name(self, key, value): + assert len(value) > 2 + return value + def __str__(self): return self.name @@ -85,8 +100,11 @@ class Recorder(db.Model): class RecorderCommand(db.Model): """Table containing permissions associated with groups.""" id = db.Column(db.Integer, autoincrement=True, primary_key=True) + created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow()) last_time_modified = db.Column(db.DateTime, nullable=True, default=None) name = db.Column(db.Unicode(63), unique=True, nullable=False) + alternative_name = db.Column(db.Unicode(63), unique=True, nullable=True, default=None) + disabled = db.Column(db.Boolean, default=False) description = db.Column(db.Unicode(511), nullable=True, default=None) parameters_string = db.Column(db.String(2047), nullable=True) recorder_model = db.relationship('RecorderModel', back_populates='recorder_commands') diff --git a/models/room_model.py b/models/room_model.py index f855e53..0469eeb 100644 --- a/models/room_model.py +++ b/models/room_model.py @@ -5,13 +5,12 @@ Models for lecture recorder import json from sqlalchemy import MetaData, CheckConstraint +from sqlalchemy.exc import IntegrityError + +from datetime import datetime, timedelta 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 - +from backend.tools.scrape_rooms import scrape_rooms metadata = MetaData() @@ -19,10 +18,12 @@ metadata = MetaData() 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) + name = db.Column(db.Unicode(127), unique=False, nullable=False) + alternate_name = db.Column(db.Unicode(127), unique=False, 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) + number = db.Column(db.Unicode(63), unique=False, nullable=True) + building_name = db.Column(db.Unicode(63), unique=False, nullable=True) + building_number = db.Column(db.Unicode(63), unique=False, nullable=True) recorder = db.relationship('Recorder', uselist=False, back_populates='room') # one-to-one relation (uselist=False) @@ -34,7 +35,6 @@ class Room(db.Model): def __init__(self, **kwargs): super(Room, self).__init__(**kwargs) - @staticmethod def get_by_name(name): """ @@ -47,7 +47,7 @@ class Room(db.Model): @staticmethod def get_all(): """ - Return all groups + Return all rooms :return: """ return Room.query.all() @@ -61,3 +61,16 @@ class Room(db.Model): def toJSON(self): return json.dumps(self.to_dict(), default=lambda o: o.__dict__, sort_keys=True, indent=4) + + +def pre_fill_table(): + rooms = scrape_rooms() + i_tunes_u_mappings = [Room(name=room['name'], number=room['room_number'], + building_name=room['building_name'], building_number=room['building_number']) for room in + rooms] + + try: + db.session.bulk_save_objects(i_tunes_u_mappings) + db.session.commit() + except IntegrityError as e: + db.session.rollback() diff --git a/recorder_adapters/__init__.py b/recorder_adapters/__init__.py index c297c35..769e8b3 100644 --- a/recorder_adapters/__init__.py +++ b/recorder_adapters/__init__.py @@ -110,6 +110,10 @@ def get_defined_recorder_adapters(): rec_model = {'id': f_p[1], 'name': f_p[1], 'commands': {}} if hasattr(rec_model_module, 'RECORDER_MODEL_NAME'): rec_model['name'] = rec_model_module.RECORDER_MODEL_NAME + if hasattr(rec_model_module, 'REQUIRES_USER'): + rec_model['requires_user'] = rec_model_module.REQUIRES_USER + if hasattr(rec_model_module, 'REQUIRES_PW'): + rec_model['requires_password'] = rec_model_module.REQUIRES_PW for name, obj in inspect.getmembers(rec_model_module, inspect.isclass): if issubclass(obj, RecorderAdapter): commands = {} diff --git a/recorder_adapters/extron_smp.py b/recorder_adapters/extron_smp.py index dd3bd95..1e44aab 100644 --- a/recorder_adapters/extron_smp.py +++ b/recorder_adapters/extron_smp.py @@ -2,6 +2,8 @@ from backend.recorder_adapters import telnetlib, TelnetAdapter, RecorderAdapter RECORDER_MODEL_NAME = "SMP 351 / 352" VERSION = "0.9.0" +REQUIRES_USER = False +REQUIRES_PW = True # HOST = "localhost" # HOST = "129.13.51.102" # Audimax SMP 351 @@ -13,9 +15,9 @@ PW = "123mzsmp" class SMP(TelnetAdapter, RecorderAdapter): - def __init__(self, address, admin_password): + def __init__(self, address, password, **kwargs): super().__init__(address) - self.admin_pw = admin_password + self.admin_pw = password def _login(self): self.tn = telnetlib.Telnet(HOST) diff --git a/tools/model_updater.py b/tools/model_updater.py index 2cc0fe6..e780f81 100644 --- a/tools/model_updater.py +++ b/tools/model_updater.py @@ -21,8 +21,9 @@ def calculate_md5_checksum(string_to_md5_sum: str): def create_recorder_commands_for_recorder_adapter(command_definitions: dict, recorder_model: RecorderModel): - existing_recorder_commands = RecorderCommand.query.filter(and_(RecorderCommand.name.in_(command_definitions.keys())), - RecorderCommand.recorder_model == recorder_model) + existing_recorder_commands = RecorderCommand.query.filter( + and_(RecorderCommand.name.in_(command_definitions.keys())), + RecorderCommand.recorder_model == recorder_model) existing_commands = set() for existing_command in existing_recorder_commands: existing_commands.add(existing_command.name) @@ -51,7 +52,9 @@ def update_recorder_models_database(): r_m = RecorderModel.get_by_adapter_id(r_a["id"]) model_checksum = calculate_md5_checksum(dumps(r_a["commands"])) if r_m is None: - r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum) + r_m = RecorderModel(record_adapter_id=r_a["id"], model_name=r_a["name"], checksum=model_checksum, + requires_user=r_a.get('requires_user', None), + requires_password=r_a.get('requires_password', None)) db.session.add(r_m) db.session.flush() db.session.refresh(r_m) diff --git a/tools/scrape_rooms.py b/tools/scrape_rooms.py new file mode 100644 index 0000000..322dcc3 --- /dev/null +++ b/tools/scrape_rooms.py @@ -0,0 +1,60 @@ +from pprint import pprint + +import re +import requests +from bs4 import BeautifulSoup + + +def scrape_rooms(): + headers = { + 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36'} + + room_url = "https://campus.kit.edu/live-stud/campus/all/roomgroup.asp?roomgroupcolumn1=H%F6r%2D%2FLehrsaal&tguid=0x1A35C3A1490748388EBEBA3943EFCDD5" + page = requests.get(room_url, headers=headers) + # soup = BeautifulSoup(page.content, 'html5lib') + soup = BeautifulSoup(page.content, 'html.parser') + + # pprint(page.content) + + # pprint(soup.prettify()) + + idx = 0 + + rooms = [] + + re_string = r"^(\d\d.\d\d)?\s(.*)" + re_exp = re.compile(re_string) + + for tr in soup.find_all('tr'): + idx += 1 + if idx == 1: # skip first row + continue + a_name = tr.find_all('a')[0].string + a_building = tr.find_all('a')[3].string + match = re_exp.match(a_name) + if match is not None: + building_number, name = re_exp.match(a_name).groups() + else: + name = a_name + building_number = None + + match = re_exp.match(a_building) + if match is not None: + building_number, building_name = re_exp.match(a_building).groups() + else: + building_name = a_name + building_number = None + + room = {'name': name, + 'room_number': tr.find_all('a')[1].string if tr.find_all('a')[0].string != "None" else tr.find_all('a')[ + 1].string, + 'building_name': building_name, + 'building_number': building_number} + + rooms.append(room) + + return rooms + + +if __name__ == '__main__': + scrape_rooms()