diff --git a/.github/workflows/python-flake8.yml b/.github/workflows/python-flake8.yml index 397a26a17..0123278fb 100644 --- a/.github/workflows/python-flake8.yml +++ b/.github/workflows/python-flake8.yml @@ -23,10 +23,10 @@ jobs: timeout-minutes: 10 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.8 + - name: Set up Python 3.10 uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: "3.10" - name: Lint with flake8 run: | python -m pip install --upgrade pip diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 1fd243008..fbfcf9c61 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,5 +5,8 @@ build: tools: python: "mambaforge-4.10" +sphinx: + configuration: docs/conf.py + conda: environment: docs/environment.yml diff --git a/CHANGES.rst b/CHANGES.rst index 695fcbb07..e4bcda2ba 100755 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,23 @@ Changelog ========= +Version 8.3.1 +~~~~~~~~~~~~~ + +Bug fix release: + +All changes: +https://github.com/Open-MSS/MSS/milestone/99?closed=1 + +Version 8.3.0 +~~~~~~~~~~~~~ + +Bug fix release and minor enhacements: +We improved the KML docking widget feature + +All changes: +https://github.com/Open-MSS/MSS/milestone/98 + Version 8.2.0 ~~~~~~~~~~~~~ diff --git a/conftest.py b/conftest.py index c914efea6..055c1b5ae 100644 --- a/conftest.py +++ b/conftest.py @@ -125,6 +125,9 @@ def pytest_generate_tests(metafunc): # mscolab data directory MSCOLAB_DATA_DIR = fs.path.join(DATA_DIR, 'filedata') +# In the unit days when Operations get archived because not used +ARCHIVE_THRESHOLD = 30 + # To enable logging set to True or pass a logger object to use. SOCKETIO_LOGGER = True @@ -218,9 +221,8 @@ def _load_module(module_name, path): ''' @pytest.fixture(autouse=True) -def close_open_windows(): - """ - Closes all windows after every test +def fail_if_open_message_boxes_left(): + """Fail a test if there are any Qt message boxes left open at the end """ # Mock every MessageBox widget in the test suite to avoid unwanted freezes on unhandled error popups etc. with mock.patch("PyQt5.QtWidgets.QMessageBox.question") as q, \ @@ -231,7 +233,7 @@ def close_open_windows(): if any(box.call_count > 0 for box in [q, i, c, w]): summary = "\n".join([f"PyQt5.QtWidgets.QMessageBox.{box()._extract_mock_name()}: {box.mock_calls[:-1]}" for box in [q, i, c, w] if box.call_count > 0]) - warnings.warn(f"An unhandled message box popped up during your test!\n{summary}") + pytest.fail(f"An unhandled message box popped up during your test!\n{summary}") # Try to close all remaining widgets after each test diff --git a/docs/conf.py b/docs/conf.py index 3204f3761..edaea685c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -119,7 +119,7 @@ def get_tutorial_images(): # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = ['sphinx_rtd_theme'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -217,23 +217,19 @@ def get_tutorial_images(): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'sphinx_rtd_theme' +html_theme = 'sphinx_rtd_theme' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme - html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] html_style = 'css/mss.css' else: + htmls_static_path = ['_static'] + html_css_files = ['mss.css'] html_context = { 'display_github': False, # Add 'Edit on Bitbucket' link instead of 'View page source' 'last_updated': True, 'commit': False, - 'css_files': [ - 'https://media.readthedocs.org/css/sphinx_rtd_theme.css', - 'https://media.readthedocs.org/css/readthedocs-doc-embed.css', - '_static/css/mss.css', - ], } # Theme options are theme-specific and customize the look and feel of a theme diff --git a/docs/development.rst b/docs/development.rst index 329fce64e..dcee78896 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -449,15 +449,27 @@ As developer you should copy this directory and adjust the source path, build nu using a local meta.yaml recipe:: $ cd yourlocalbuild - $ conda build . - $ conda create -n mssbuildtest mamba - $ conda activate mssbuildtest - $ mamba install --use-local mss + $ mamba build . + $ mamba create -n mssbuildtest + $ mamba activate mssbuildtest + $ mamba install -c local mss Take care on removing alpha builds, or increase the build number for a new version. +Alternative local build by boa +------------------------------ + +`boa `_ is a new faster option to build conda packages. +We need first to convert the existing description to a recipe.yaml:: + + $ cd yourlocalbuild + $ boa convert meta.yaml > recipe.yaml + $ boa build . + $ mamba install -c local mss + + Creating a new release ---------------------- diff --git a/docs/publications.rst b/docs/publications.rst index 68aa927be..c563a6d88 100644 --- a/docs/publications.rst +++ b/docs/publications.rst @@ -24,9 +24,11 @@ Acknowledgement Please add the following acknowledgement and cite to your publications: + The authors gratefully acknowledge the use of the MSS flight planning - software (Rautenhaus, 2012; MSS development was partially funded by - the Deutsche Forschungsgemeinschaft (DFG, German Research Foundation) - project no. UN 311/3-1)“. + software (Bauer et al, 2022, Rautenhaus et al, 2012; + MSS development was partially funded by the Deutsche Forschungsgemeinschaft + (DFG, German Research Foundation) project no. UN 311/3-1 + and project no. SPP 1294 423229456) Thank you very much. diff --git a/docs/samples/config/mscolab/mscolab_settings.py.sample b/docs/samples/config/mscolab/mscolab_settings.py.sample index b521805f7..48bfc3951 100644 --- a/docs/samples/config/mscolab/mscolab_settings.py.sample +++ b/docs/samples/config/mscolab/mscolab_settings.py.sample @@ -26,6 +26,9 @@ """ import os +# In the unit days when Operations get archived because not used +ARCHIVE_THRESHOLD = 30 + # To enable logging set to True or pass a logger object to use. SOCKETIO_LOGGER = False diff --git a/localbuild/meta.yaml b/localbuild/meta.yaml index fca68048d..57fbd8317 100644 --- a/localbuild/meta.yaml +++ b/localbuild/meta.yaml @@ -30,6 +30,7 @@ requirements: - setuptools - pip - menuinst # [win] + - future run: - python - defusedxml @@ -64,7 +65,7 @@ requirements: - flask-httpauth - flask-mail - flask-migrate - - werkzeug >=2.2.3 + - werkzeug >=2.2.3, <3.0.0 - flask-socketio >=5.1.0 - flask-sqlalchemy >=3.0.0 - flask-cors diff --git a/mslib/index.py b/mslib/index.py index a819b78fa..9ba7ddcb3 100644 --- a/mslib/index.py +++ b/mslib/index.py @@ -64,12 +64,26 @@ def _xstatic(name): return None -def create_app(name=""): +def file_exists(filepath=None): + try: + return os.path.isfile(filepath) + except TypeError: + return False + + +def create_app(name="", imprint=None, gdpr=None): + imprint_file = imprint + gdpr_file = gdpr + if "mscolab.server" in name: from mslib.mscolab.app import APP else: from mslib.mswms.app import APP + APP.jinja_env.globals.update(file_exists=file_exists) + APP.jinja_env.globals["imprint"] = imprint_file + APP.jinja_env.globals["gdpr"] = gdpr_file + @APP.route('/xstatic//', defaults=dict(filename='')) @APP.route('/xstatic//') def files(name, filename): @@ -160,7 +174,7 @@ def plots(): "For further info on how to generate it, run the " \ "gallery --help command line parameter of mswms.
" \ "An example of the gallery can be seen " \ - "here" + "here" return render_template("/content.html", act="plots", content=content) @APP.route("/mss/code/") @@ -190,9 +204,19 @@ def help(): @APP.route("/mss/imprint") def imprint(): - _file = os.path.join(DOCS_SERVER_PATH, 'static', 'docs', 'imprint.md') - content = get_content(_file) - return render_template("/content.html", act="imprint", content=content) + if file_exists(imprint_file): + content = get_content(imprint_file) + return render_template("/content.html", act="imprint", content=content) + else: + return "" + + @APP.route("/mss/gpdr") + def gdpr(): + if file_exists(gdpr_file): + content = get_content(gdpr_file) + return render_template("/content.html", act="gdpr", content=content) + else: + return "" @APP.route('/mss/favicon.ico') def favicons(): diff --git a/mslib/mscolab/app/__init__.py b/mslib/mscolab/app/__init__.py index 5f9c6f06c..b034b3b27 100644 --- a/mslib/mscolab/app/__init__.py +++ b/mslib/mscolab/app/__init__.py @@ -25,10 +25,14 @@ """ import os + +from flask_migrate import Migrate + import mslib from flask import Flask from mslib.mscolab.conf import mscolab_settings +from flask_sqlalchemy import SQLAlchemy from mslib.mswms.gallery_builder import STATIC_LOCATION from mslib.utils import prefix_route @@ -58,3 +62,6 @@ APP.config['MAIL_PASSWORD'] = getattr(mscolab_settings, "MAIL_PASSWORD", None) APP.config['MAIL_USE_TLS'] = getattr(mscolab_settings, "MAIL_USE_TLS", None) APP.config['MAIL_USE_SSL'] = getattr(mscolab_settings, "MAIL_USE_SSL", None) + +db = SQLAlchemy(APP) +migrate = Migrate(APP, db, render_as_batch=True) diff --git a/mslib/mscolab/chat_manager.py b/mslib/mscolab/chat_manager.py index 926583672..da8555893 100644 --- a/mslib/mscolab/chat_manager.py +++ b/mslib/mscolab/chat_manager.py @@ -25,8 +25,11 @@ limitations under the License. """ import datetime +import os +import time import fs +from werkzeug.utils import secure_filename from mslib.mscolab.conf import mscolab_settings from mslib.mscolab.models import db, Message, MessageType @@ -93,3 +96,24 @@ def delete_message(self, message_id): upload_dir.remove(fs.path.join(str(message.op_id), file_name)) db.session.delete(message) db.session.commit() + + def add_attachment(self, op_id, upload_folder, file, file_token): + with fs.open_fs('/') as home_fs: + file_dir = fs.path.join(upload_folder, str(op_id)) + if '\\' not in file_dir: + if not home_fs.exists(file_dir): + home_fs.makedirs(file_dir) + else: + file_dir = file_dir.replace('\\', '/') + if not os.path.exists(file_dir): + os.makedirs(file_dir) + file_name, file_ext = file.filename.rsplit('.', 1) + file_name = f'{file_name}-{time.strftime("%Y%m%dT%H%M%S")}-{file_token}.{file_ext}' + file_name = secure_filename(file_name) + file_path = fs.path.join(file_dir, file_name) + file.save(file_path) + static_dir = fs.path.basename(upload_folder) + static_dir = static_dir.replace('\\', '/') + static_file_path = os.path.join(static_dir, str(op_id), file_name) + if os.path.exists(file_path): + return static_file_path diff --git a/mslib/mscolab/conf.py b/mslib/mscolab/conf.py index e0e50db56..8ffd5bb83 100644 --- a/mslib/mscolab/conf.py +++ b/mslib/mscolab/conf.py @@ -33,6 +33,9 @@ class default_mscolab_settings: # expire token in seconds # EXPIRATION = 86400 + # In the unit days when Operations get archived because not used + ARCHIVE_THRESHOLD = 30 + # To enable logging set to True or pass a logger object to use. SOCKETIO_LOGGER = False @@ -99,6 +102,10 @@ class default_mscolab_settings: # mail accounts # MAIL_DEFAULT_SENDER = 'MSS@localhost' + # filepath to md file with imprint + IMPRINT = None + # filepath to md file with gdpr + GDPR = None mscolab_settings = default_mscolab_settings() diff --git a/mslib/mscolab/file_manager.py b/mslib/mscolab/file_manager.py index f6d817b1f..b632e9287 100644 --- a/mslib/mscolab/file_manager.py +++ b/mslib/mscolab/file_manager.py @@ -104,10 +104,17 @@ def list_operations(self, user, skip_archived=False): operations = [] permissions = Permission.query.filter_by(u_id=user.id).all() for permission in permissions: + operation = Operation.query.filter_by(id=permission.op_id).first() + if operation.last_used is not None and ( + datetime.datetime.utcnow() - operation.last_used).days > mscolab_settings.ARCHIVE_THRESHOLD: + # outdated OPs get archived + self.update_operation(permission.op_id, "active", False, user) + # new query to get uptodate data if skip_archived: operation = Operation.query.filter_by(id=permission.op_id, active=skip_archived).first() else: operation = Operation.query.filter_by(id=permission.op_id).first() + if operation is not None: operations.append({ "op_id": permission.op_id, @@ -192,6 +199,35 @@ def auth_type(self, u_id, op_id): return False return perm.access_level + def modify_user(self, user, attribute=None, value=None, action=None): + if action == "create": + user_query = User.query.filter_by(emailid=str(user.emailid)).first() + if user_query is None: + db.session.add(user) + db.session.commit() + else: + return False + elif action == "delete": + user_query = User.query.filter_by(id=user.id).first() + if user_query is not None: + db.session.delete(user) + db.session.commit() + user_query = User.query.filter_by(id=user.id).first() + # on delete we return succesfull deleted + if user_query is None: + return True + user_query = User.query.filter_by(id=user.id).first() + if user_query is None: + return False + if None not in (attribute, value): + if attribute == "emailid": + user_query = User.query.filter_by(emailid=str(value)).first() + if user_query is not None: + return False + setattr(user, attribute, value) + db.session.commit() + return True + def update_operation(self, op_id, attribute, value, user): """ op_id: operation id @@ -226,12 +262,11 @@ def update_operation(self, op_id, attribute, value, user): db.session.commit() return True - def delete_file(self, op_id, user): + def delete_operation(self, op_id, user): """ op_id: operation id user: logged in user """ - # ToDo rename to delete_operation if self.auth_type(user.id, op_id) != "creator": return False Permission.query.filter_by(op_id=op_id).delete() @@ -341,14 +376,18 @@ def get_all_changes(self, op_id, user, named_version=False): 'created_at': change.created_at.strftime("%Y-%m-%d, %H:%M:%S") }, changes)) - def get_change_content(self, ch_id): + def get_change_content(self, ch_id, user): """ ch_id: change id user: user of this request Get change related to id """ - # ToDo refactor check user in op + ch = Change.query.filter_by(id=ch_id).first() + perm = Permission.query.filter_by(u_id=user.id, op_id=ch.op_id).first() + if perm is None: + return False + change = Change.query.filter_by(id=ch_id).first() if not change: return False @@ -368,13 +407,12 @@ def set_version_name(self, ch_id, op_id, u_id, version_name): db.session.commit() return True - def undo(self, ch_id, user): + def undo_changes(self, ch_id, user): """ ch_id: change-id user: user of this request Undo a change - # ToDo rename to undo_changes # ToDo add a revert option, which removes only that commit's change """ ch = Change.query.filter_by(id=ch_id).first() diff --git a/mslib/mscolab/models.py b/mslib/mscolab/models.py index 0ab82d5ca..6a87e8a7a 100644 --- a/mslib/mscolab/models.py +++ b/mslib/mscolab/models.py @@ -28,14 +28,10 @@ import datetime import logging import jwt - from passlib.apps import custom_app_context as pwd_context -from flask_sqlalchemy import SQLAlchemy -from mslib.mscolab.app import APP +from mslib.mscolab.app import db from mslib.mscolab.message_type import MessageType -db = SQLAlchemy(APP) - class User(db.Model): diff --git a/mslib/mscolab/server.py b/mslib/mscolab/server.py index 3a36f59cd..ee1b190ad 100644 --- a/mslib/mscolab/server.py +++ b/mslib/mscolab/server.py @@ -27,11 +27,9 @@ import functools import json import logging -import time import datetime import secrets import fs -import os import socketio import sqlalchemy.exc from itsdangerous import URLSafeTimedSerializer, BadSignature @@ -39,13 +37,11 @@ from flask import send_from_directory, abort, url_for from flask_mail import Mail, Message from flask_cors import CORS -from flask_migrate import Migrate from flask_httpauth import HTTPBasicAuth from validate_email import validate_email -from werkzeug.utils import secure_filename from mslib.mscolab.conf import mscolab_settings -from mslib.mscolab.models import Change, MessageType, User, Operation, db +from mslib.mscolab.models import Change, MessageType, User from mslib.mscolab.sockets_manager import setup_managers from mslib.mscolab.utils import create_files, get_message_dict from mslib.utils import conditional_decorator @@ -53,13 +49,11 @@ from mslib.mscolab.forms import ResetRequestForm, ResetPasswordForm -APP = create_app(__name__) +APP = create_app(__name__, imprint=mscolab_settings.IMPRINT, gdpr=mscolab_settings.GDPR) mail = Mail(APP) CORS(APP, origins=mscolab_settings.CORS_ORIGINS if hasattr(mscolab_settings, "CORS_ORIGINS") else ["*"]) -migrate = Migrate(APP, db, render_as_batch=True) auth = HTTPBasicAuth() -ARCHIVE_THRESHOLD = 30 try: from mscolab_auth import mscolab_auth @@ -160,18 +154,17 @@ def register_user(email, password, username): is_valid_username = True if username.find("@") == -1 else False is_valid_email = validate_email(email) if not is_valid_email: - return {"success": False, "message": "Oh no, your email ID is not valid!"} + return {"success": False, "message": "Your email ID is not valid!"} if not is_valid_username: - return {"success": False, "message": "Oh no, your username cannot contain @ symbol!"} + return {"success": False, "message": "Your username cannot contain @ symbol!"} user_exists = User.query.filter_by(emailid=str(email)).first() if user_exists: - return {"success": False, "message": "Oh no, this email ID is already taken!"} + return {"success": False, "message": "This email ID is already taken!"} user_exists = User.query.filter_by(username=str(username)).first() if user_exists: - return {"success": False, "message": "Oh no, this username is already registered"} - db.session.add(user) - db.session.commit() - return {"success": True} + return {"success": False, "message": "This username is already registered"} + result = fm.modify_user(user, action="create") + return {"success": result} def verify_user(func): @@ -291,10 +284,8 @@ def confirm_email(token): if user.confirmed: return render_template('user/confirmed.html', username=user.username) else: - user.confirmed = True - user.confirmed_on = datetime.datetime.now() - db.session.add(user) - db.session.commit() + fm.modify_user(user, attribute="confirmed_on", value=datetime.datetime.now()) + fm.modify_user(user, attribute="confirmed", value=True) return render_template('user/confirmed.html', username=user.username) @@ -304,24 +295,21 @@ def get_user(): return json.dumps({'user': {'id': g.user.id, 'username': g.user.username}}) -@APP.route("/delete_user", methods=["POST"]) +@APP.route("/delete_own_account", methods=["POST"]) @verify_user -def delete_user(): +def delete_own_account(): """ delete own account """ - # ToDo rename to delete_own_account user = g.user - db.session.delete(user) - db.session.commit() - return jsonify({"success": True}), 200 + result = fm.modify_user(user, action="delete") + return jsonify({"success": result}), 200 # Chat related routes @APP.route("/messages", methods=["GET"]) @verify_user def messages(): - # ToDo maybe move is_member part to file_manager user = g.user op_id = request.args.get("op_id", request.form.get("op_id", None)) if fm.is_member(user.id, op_id): @@ -341,32 +329,18 @@ def message_attachment(): file = request.files['file'] message_type = MessageType(int(request.form.get("message_type"))) user = g.user - # ToDo review users = fm.fetch_users_without_permission(int(op_id), user.id) if users is False: return jsonify({"success": False, "message": "Could not send message. No file uploaded."}) if file is not None: - with fs.open_fs('/') as home_fs: - file_dir = fs.path.join(APP.config['UPLOAD_FOLDER'], op_id) - if '\\' not in file_dir: - if not home_fs.exists(file_dir): - home_fs.makedirs(file_dir) - else: - file_dir = file_dir.replace('\\', '/') - if not os.path.exists(file_dir): - os.makedirs(file_dir) - file_name, file_ext = file.filename.rsplit('.', 1) - file_name = f'{file_name}-{time.strftime("%Y%m%dT%H%M%S")}-{file_token}.{file_ext}' - file_name = secure_filename(file_name) - file_path = fs.path.join(file_dir, file_name) - file.save(file_path) - static_dir = fs.path.basename(APP.config['UPLOAD_FOLDER']) - static_dir = static_dir.replace('\\', '/') - static_file_path = os.path.join(static_dir, op_id, file_name) - new_message = cm.add_message(user, static_file_path, op_id, message_type) - new_message_dict = get_message_dict(new_message) - sockio.emit('chat-message-client', json.dumps(new_message_dict)) - return jsonify({"success": True, "path": static_file_path}) + static_file_path = cm.add_attachment(op_id, APP.config['UPLOAD_FOLDER'], file, file_token) + if static_file_path is not None: + new_message = cm.add_message(user, static_file_path, op_id, message_type) + new_message_dict = get_message_dict(new_message) + sockio.emit('chat-message-client', json.dumps(new_message_dict)) + return jsonify({"success": True, "path": static_file_path}) + else: + return "False" return jsonify({"success": False, "message": "Could not send message. No file uploaded."}) # normal use case never gets to this return "False" @@ -435,9 +409,9 @@ def get_all_changes(): @APP.route('/get_change_content', methods=['GET']) @verify_user def get_change_content(): - # ToDo refactor see fm.get_change_content( ch_id = int(request.args.get('ch_id', request.form.get('ch_id', 0))) - result = fm.get_change_content(ch_id) + user = g.user + result = fm.get_change_content(ch_id, user) if result is False: return "False" return jsonify({"content": result}) @@ -477,7 +451,7 @@ def get_operations(): def delete_operation(): op_id = int(request.form.get('op_id', 0)) user = g.user - success = fm.delete_file(op_id, user) + success = fm.delete_operation(op_id, user) if success is False: return jsonify({"success": False, "message": "You don't have access for this operation!"}) @@ -514,48 +488,29 @@ def get_operation_details(): @APP.route('/set_last_used', methods=["POST"]) @verify_user def set_last_used(): - # ToDo refactor move to file_manager op_id = request.form.get('op_id', None) + user = g.user days_ago = int(request.form.get('days', 0)) - operation = Operation.query.filter_by(id=int(op_id)).first() - operation.last_used = datetime.datetime.utcnow() - datetime.timedelta(days=days_ago) - temp_operation_active = operation.active - if days_ago > ARCHIVE_THRESHOLD: - operation.active = False + fm.update_operation(int(op_id), 'last_used', + datetime.datetime.utcnow() - datetime.timedelta(days=days_ago), + user) + if days_ago > mscolab_settings.ARCHIVE_THRESHOLD: + fm.update_operation(int(op_id), "active", False, user) else: - operation.active = True - db.session.commit() - # Reload Operation List - if temp_operation_active != operation.active: + fm.update_operation(int(op_id), "active", True, user) token = request.args.get('token', request.form.get('token', False)) json_config = {"token": token} sockio.sm.update_operation_list(json_config) return jsonify({"success": True}), 200 -@APP.route('/update_last_used', methods=["POST"]) -@verify_user -def update_last_used(): - # ToDo refactor move to file_manager - operations = Operation.query.filter().all() - for operation in operations: - if operation.last_used is not None and \ - (datetime.datetime.utcnow() - operation.last_used).days > 30: - operation.active = False - else: - operation.active = True - db.session.commit() - return jsonify({"success": True}), 200 - - -@APP.route('/undo', methods=["POST"]) +@APP.route('/undo_changes', methods=["POST"]) @verify_user -def undo_ftml(): - # ToDo rename to undo_changes +def undo_changes(): ch_id = request.form.get('ch_id', -1) ch_id = int(ch_id) user = g.user - result = fm.undo(ch_id, user) + result = fm.undo_changes(ch_id, user) # get op_id from change ch = Change.query.filter_by(id=ch_id).first() if result is True: @@ -691,8 +646,7 @@ def reset_password(token): if form.validate_on_submit(): try: user.hash_password(form.confirm_password.data) - user.confirmed = True - db.session.commit() + fm.modify_user(user, "confirmed", True) flash('Password reset Success. Please login by the user interface.', 'category_success') return render_template('user/status.html') except IOError: diff --git a/mslib/msui/flighttrack.py b/mslib/msui/flighttrack.py index 0a3114ca2..85501cefd 100755 --- a/mslib/msui/flighttrack.py +++ b/mslib/msui/flighttrack.py @@ -693,15 +693,12 @@ def createEditor(self, parent, option, index): """ if index.column() == LOCATION: combobox = QtWidgets.QComboBox(parent) - locations = config_loader(dataset='locations') - adds = list(locations.keys()) + adds = set(config_loader(dataset='locations')) if self.parent() is not None: - for loc in [wp.location for wp in self.parent().waypoints_model.all_waypoint_data() if - wp.location != ""]: - if loc not in adds: - adds.append(loc) + for wp in self.parent().waypoints_model.all_waypoint_data(): + if wp.location != "": + adds.add(wp.location) combobox.addItems(sorted(adds)) - combobox.setEditable(True) return combobox else: @@ -709,14 +706,14 @@ def createEditor(self, parent, option, index): return QtWidgets.QItemDelegate.createEditor(self, parent, option, index) def setEditorData(self, editor, index): - text = index.model().data(index, QtCore.Qt.DisplayRole).value() + value = index.model().data(index, QtCore.Qt.DisplayRole).value() if index.column() in (LOCATION,): - i = editor.findText(text) + i = editor.findText(value) if i == -1: i = 0 editor.setCurrentIndex(i) else: - QtWidgets.QItemDelegate.setEditorData(self, editor, index) + editor.insert(str(value)) def setModelData(self, editor, model, index): """ diff --git a/mslib/msui/kmloverlay_dockwidget.py b/mslib/msui/kmloverlay_dockwidget.py index 9583f648d..b0f487b72 100644 --- a/mslib/msui/kmloverlay_dockwidget.py +++ b/mslib/msui/kmloverlay_dockwidget.py @@ -304,8 +304,6 @@ def __init__(self, parent=None, view=None): self.pushButton_color.clicked.connect(self.select_color) self.dsbx_linewidth.valueChanged.connect(self.select_linewidth) - self.listWidget.itemChanged.connect(self.flagop) # when item changes, flag operation happens. - self.settings_tag = "kmldock" settings = load_settings_qsettings( self.settings_tag, {"filename": "", "linewidth": 2, "colour": (0, 0, 0, 1), @@ -332,6 +330,8 @@ def __init__(self, parent=None, view=None): # When KMLoverlaywidget is opened, it ensures that the # color of individual KML files are already shown as icons. self.set_color_icons() + # must be connected here, lest the set_color_icons routine above causes many reloads + self.listWidget.itemChanged.connect(self.flagop) self.view.plot_kml(self) def update(self): @@ -459,6 +459,8 @@ def get_file(self): self, "Open KML File", os.path.dirname(str(self.directory_location)), "KML Files (*.kml)") if not filenames: return + + self.listWidget.itemChanged.disconnect(self.flagop) self.select_file(filenames) # set color icons according to linewidth to newly added KML files for filename in filenames: @@ -467,10 +469,12 @@ def get_file(self): for item in item_list: index = self.listWidget.row(item) self.listWidget.item(index).setIcon(self.show_color_icon(filename, self.set_color(filename))) + self.load_file() + self.listWidget.itemChanged.connect(self.flagop) def select_file(self, filenames): """ - Initializes selected file/ files + Initializes selected file/files """ for filename in filenames: if filename is None: @@ -488,7 +492,6 @@ def select_file(self, filenames): else: logging.info("%s file already added", text) self.labelStatusBar.setText("Status: KML Files added") - self.load_file() def create_list_item(self, text): """ @@ -511,8 +514,11 @@ def unselect_all(self): # unselects all files if self.listWidget.count() == 0: self.labelStatusBar.setText("Status: No Files to unselect. Click on Add KML Files to get started.") return + self.listWidget.itemChanged.disconnect(self.flagop) for index in range(self.listWidget.count()): self.listWidget.item(index).setCheckState(QtCore.Qt.Unchecked) + self.listWidget.itemChanged.connect(self.flagop) + self.load_file() self.labelStatusBar.setText("Status: All Files unselected") def remove_file(self): # removes checked files diff --git a/mslib/msui/mpl_map.py b/mslib/msui/mpl_map.py index 8f33c61ae..6c07477f7 100644 --- a/mslib/msui/mpl_map.py +++ b/mslib/msui/mpl_map.py @@ -147,17 +147,10 @@ def __init__(self, identifier=None, CRS=None, BBOX_UNITS=None, OPERATION_NAME=No self.image = None # Print project name and CRS identifier into figure. - crs_text = "" - if self.operation_name is not None: - crs_text += self.operation_name - if self.crs is not None: - if len(crs_text) > 0: - crs_text += "\n" - crs_text += self.crs - if hasattr(self, "crs_text"): # update existing textbox - self.crs_text.set_text(crs_text) - else: - self.crs_text = self.ax.figure.text(0, 0, crs_text) + if not hasattr(self, "_info_text"): + self._info_text = self.ax.figure.text(0, 0, "") + self._infos = [None] * 4 + self.update_info_text(name=self.operation_name, crs=self.crs) if self.appearance["draw_graticule"]: pass @@ -175,6 +168,18 @@ def __init__(self, identifier=None, CRS=None, BBOX_UNITS=None, OPERATION_NAME=No self.airspaces = None self.airspacetext = None + def update_info_text(self, openaip=None, ourairports=None, name=None, crs=None): + if openaip is not None: + self._infos[0] = openaip + if ourairports is not None: + self._infos[1] = ourairports + if name is not None: + self._infos[2] = name + if crs is not None: + self._infos[3] = crs + self._info_text.set_text( + "\n".join([_i for _i in self._infos if _i])) # both None and "" + def set_identifier(self, identifier): self.identifier = identifier @@ -350,8 +355,7 @@ def set_draw_airports(self, value, port_type=["small_airport"], reload=True): Sets airports to visible or not visible """ if (reload or not value or len(port_type) == 0) and self.airports: - if OURAIRPORTS_NOTICE in self.crs_text.get_text(): - self.crs_text.set_text(self.crs_text.get_text().replace(f"{OURAIRPORTS_NOTICE}\n", "")) + self.update_info_text(ourairports="") self.airports.remove() self.airtext.remove() self.airports = None @@ -365,8 +369,7 @@ def set_draw_airspaces(self, value, airspaces=[], range_km=None, reload=True): Sets airspaces to visible or not visible """ if (reload or not value or len(airspaces) == 0) and self.airspaces: - if OPENAIP_NOTICE in self.crs_text.get_text(): - self.crs_text.set_text(self.crs_text.get_text().replace(f"{OPENAIP_NOTICE}\n", "")) + self.update_info_text(openaip="") self.airspaces.remove() self.airspacetext.remove() self.airspaces = None @@ -396,9 +399,7 @@ def draw_airspaces(self, countries=[], range_km=None): if not airspaces: return - if OPENAIP_NOTICE not in self.crs_text.get_text(): - self.crs_text.set_text(f"{OPENAIP_NOTICE}\n" + self.crs_text.get_text()) - + self.update_info_text(openaip=OPENAIP_NOTICE) airspaces.sort(key=lambda x: (x["bottom"], x["top"] - x["bottom"])) max_height = max(airspaces[-1]["bottom"], 0.001) cmap = get_cmap("Blues") @@ -464,9 +465,7 @@ def draw_airports(self, port_type): if not airports: return - if OURAIRPORTS_NOTICE not in self.crs_text.get_text(): - self.crs_text.set_text(f"{OURAIRPORTS_NOTICE}\n" + self.crs_text.get_text()) - + self.update_info_text(ourairports=OURAIRPORTS_NOTICE) self.airports = self.ax.scatter(lons, lats, marker="o", color="r", linewidth=1, s=9, edgecolor="black", zorder=6) self.airports.set_pickradius(1) diff --git a/mslib/msui/mpl_pathinteractor.py b/mslib/msui/mpl_pathinteractor.py index 910607181..cabe3bd6a 100644 --- a/mslib/msui/mpl_pathinteractor.py +++ b/mslib/msui/mpl_pathinteractor.py @@ -977,6 +977,8 @@ def button_release_insert_callback(self, event): y = event.ydata wpm = self.waypoints_model flightlevel = float(pressure2flightlevel(y * units.Pa).magnitude) + # round flightlevel to the nearest multiple of five (legal level) + flightlevel = 5.0 * round(flightlevel / 5) [lat, lon], best_index = self.plotter.get_lat_lon(event, wpm.all_waypoint_data()) loc = find_location(lat, lon) # skipped tolerance which uses appropriate_epsilon_km if loc is not None: diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index 234f7a6e3..69cb36b03 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -151,6 +151,9 @@ def __init__(self, parent=None, mscolab=None): # connect login, adduser, connect buttons self.connectBtn.clicked.connect(self.connect_handler) + self.connectBtn.setFocus() + self.disconnectBtn.clicked.connect(self.disconnect_handler) + self.disconnectBtn.hide() self.loginBtn.clicked.connect(self.login_handler) self.addUserBtn.clicked.connect(lambda: self.stackedWidget.setCurrentWidget(self.newuserPage)) @@ -182,11 +185,6 @@ def page_switched(self, index): self.newPasswordLe.setText("") self.newConfirmPasswordLe.setText("") - if index == 1: - self.connectBtn.setEnabled(False) - else: - self.connectBtn.setEnabled(True) - def set_status(self, _type="Error", msg=""): if _type == "Error": msg = "⚠ " + msg @@ -253,14 +251,11 @@ def connect_handler(self): config_loader(dataset="MSS_auth").get(self.mscolab_server_url)) self.mscolab_login_changed(self.loginEmailLe.text()) self.enable_login_btn() + self.loginBtn.setFocus() # Change connect button text and connect disconnect handler - self.connectBtn.setText('Disconnect') - try: - self.connectBtn.clicked.disconnect(self.connect_handler) - except TypeError: - pass - self.connectBtn.clicked.connect(self.disconnect_handler) + self.connectBtn.hide() + self.disconnectBtn.show() else: logging.error("Error %s", r) self.set_status("Error", "Some unexpected error occurred. Please try again.") @@ -295,9 +290,9 @@ def disconnect_handler(self): self.mscolab_server_url = None self.auth = None - self.connectBtn.setText('Connect') - self.connectBtn.clicked.disconnect(self.disconnect_handler) - self.connectBtn.clicked.connect(self.connect_handler) + self.connectBtn.show() + self.connectBtn.setFocus() + self.disconnectBtn.hide() self.set_status("Info", 'Disconnected from server.') def login_handler(self): @@ -314,7 +309,7 @@ def login_handler(self): r = s.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) if r.status_code == 401: raise requests.exceptions.ConnectionError - except requests.exceptions.ConnectionError as ex: + except requests.exceptions.RequestException as ex: logging.error("unexpected error: %s %s %s", type(ex), url, ex) self.set_status( "Error", @@ -595,12 +590,6 @@ def after_login(self, emailid, url, r): "New Login required!") self.logout() else: - # Update Last Used - data = { - "token": self.token - } - r = requests.post(f"{self.mscolab_server_url}/update_last_used", data=data, - timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) self.conn.signal_operation_list_updated.connect(self.reload_operation_list) self.conn.signal_reload.connect(self.reload_window) self.conn.signal_new_permission.connect(self.render_new_permission) @@ -767,7 +756,7 @@ def delete_account(self): } try: - r = requests.post(self.mscolab_server_url + '/delete_user', data=data, + r = requests.post(self.mscolab_server_url + '/delete_own_account', data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) except requests.exceptions.RequestException as e: logging.error(e) @@ -874,8 +863,11 @@ def add_operation(self): self.logout() else: if r.text == "True": - self.error_dialog = QtWidgets.QErrorMessage() - self.error_dialog.showMessage('Your operation was created successfully') + QtWidgets.QMessageBox.information( + self.ui, + "Creation successful", + "Your operation was created successfully.", + ) op_id = self.get_recent_op_id() self.new_op_id = op_id self.conn.handle_new_operation(op_id) @@ -906,6 +898,7 @@ def get_recent_op_id(self): return op_id else: show_popup(self.ui, "Error", "Session expired, new login required") + self.signal_logout_mscolab() else: show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() @@ -1083,7 +1076,12 @@ def handle_leave_operation(self): "selected_userids": json.dumps([self.user["id"]]) } url = urljoin(self.mscolab_server_url, "delete_bulk_permissions") - res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + try: + res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as e: + logging.error(e) + show_popup(self.ui, "Error", "Some error occurred! Please reconnect.") + self.logout() if res.text != "False": res = res.json() if res["success"]: @@ -1130,15 +1128,24 @@ def change_category_handler(self): "value": entered_operation_category } url = urljoin(self.mscolab_server_url, 'update_operation') - r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text == "True": - self.active_operation_category = entered_operation_category - self.reload_operation_list() - self.error_dialog = QtWidgets.QErrorMessage() - self.error_dialog.showMessage("Description is updated successfully.") - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as e: + logging.error(e) + show_popup(self.ui, "Error", "Some error occurred! Please reconnect.") self.logout() + else: + if r.text == "True": + self.active_operation_category = entered_operation_category + self.reload_operation_list() + QtWidgets.QMessageBox.information( + self.ui, + "Update successful", + "Category is updated successfully.", + ) + else: + show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + self.logout() else: show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() @@ -1164,17 +1171,26 @@ def change_description_handler(self): } url = urljoin(self.mscolab_server_url, 'update_operation') - r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text == "True": - # Update active operation description label - self.set_operation_desc_label(entered_operation_desc) - - self.reload_operation_list() - self.error_dialog = QtWidgets.QErrorMessage() - self.error_dialog.showMessage("Description is updated successfully.") - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as e: + logging.error(e) + show_popup(self.ui, "Error", "Some error occurred! Please reconnect.") self.logout() + else: + if r.text == "True": + # Update active operation description label + self.set_operation_desc_label(entered_operation_desc) + + self.reload_operation_list() + QtWidgets.QMessageBox.information( + self.ui, + "Update successful", + "Description is updated successfully.", + ) + else: + show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + self.logout() else: show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() @@ -1199,23 +1215,32 @@ def rename_operation_handler(self): "value": entered_operation_name } url = urljoin(self.mscolab_server_url, 'update_operation') - r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text == "True": - # Update active operation name - self.active_operation_name = entered_operation_name - - # Update active operation description - self.set_operation_desc_label(self.active_operation_description) - self.reload_operation_list() - self.reload_windows_slot() - # Update other user's operation list - self.conn.signal_operation_list_updated.connect(self.reload_operation_list) - - self.error_dialog = QtWidgets.QErrorMessage() - self.error_dialog.showMessage("Operation is renamed successfully.") - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as e: + logging.error(e) + show_popup(self.ui, "Error", "Some error occurred! Please reconnect.") self.logout() + else: + if r.text == "True": + # Update active operation name + self.active_operation_name = entered_operation_name + + # Update active operation description + self.set_operation_desc_label(self.active_operation_description) + self.reload_operation_list() + self.reload_windows_slot() + # Update other user's operation list + self.conn.signal_operation_list_updated.connect(self.reload_operation_list) + + QtWidgets.QMessageBox.information( + self.ui, + "Rename successful", + "Operation is renamed successfully.", + ) + else: + show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") + self.logout() else: show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() @@ -1399,7 +1424,7 @@ def render_new_permission(self, op_id, u_id): operation_desc = f'{operation["path"]} - {operation["access_level"]}' widgetItem = QtWidgets.QListWidgetItem(operation_desc, parent=self.ui.listOperationsMSC) widgetItem.op_id = operation["op_id"] - widgetItem.catgegory = operation["category"] + widgetItem.operation_category = operation["category"] widgetItem.operation_path = operation["path"] widgetItem.access_level = operation["access_level"] widgetItem.active_operation_description = operation["description"] @@ -1509,15 +1534,19 @@ def show_categories_to_ui(self, ops=None): """ logging.debug('show_categories_to_ui') if verify_user_token(self.mscolab_server_url, self.token) or ops: + r = None if ops is not None: r = ops else: data = { "token": self.token } - r = requests.get(f'{self.mscolab_server_url}/operations', data=data, - timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text != "False": + try: + r = requests.get(f'{self.mscolab_server_url}/operations', data=data, + timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.MissingSchema: + show_popup(self.ui, "Error", "Session expired, new login required") + if r is not None and r.text != "False": _json = json.loads(r.text) operations = _json["operations"] self.ui.filterCategoryCb.currentIndexChanged.disconnect(self.operation_category_handler) @@ -1558,11 +1587,11 @@ def add_operations_to_ui(self): for operation in operations: operation_desc = f'{operation["path"]} - {operation["access_level"]}' widgetItem = QtWidgets.QListWidgetItem(operation_desc) - widgetItem.active_operation_description = operation["description"] widgetItem.op_id = operation["op_id"] - widgetItem.access_level = operation["access_level"] - widgetItem.operation_path = operation["path"] widgetItem.operation_category = operation["category"] + widgetItem.operation_path = operation["path"] + widgetItem.access_level = operation["access_level"] + widgetItem.active_operation_description = operation["description"] try: # compatibility to 7.x # a newer server can distinguish older operations and move those into inactive state @@ -1648,17 +1677,6 @@ def set_active_op_id(self, item): self.ui.workLocallyCheckbox.setChecked(False) self.ui.workLocallyCheckbox.blockSignals(False) - # Disable Activate Operation Button - # self.ui.actionUnarchiveOperation.setEnabled(False) - - # set last used date for operation - data = { - "token": self.token, - "op_id": item.op_id, - } - requests.post(f'{self.mscolab_server_url}/set_last_used', data=data, - timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - # set active_op_id here self.active_op_id = item.op_id self.access_level = item.access_level @@ -1933,6 +1951,12 @@ def logout(self): return self.ui.local_active = True self.ui.menu_handler() + + # disconnect socket + if self.conn is not None: + self.conn.disconnect() + self.conn = None + # close all hanging window self.close_external_windows() self.hide_operation_options() @@ -1957,6 +1981,7 @@ def logout(self): self.ui.usernameLabel.hide() self.ui.userOptionsTb.hide() self.ui.connectBtn.show() + self.ui.connectBtn.setFocus() self.ui.openOperationsGb.hide() self.ui.actionAddOperation.setEnabled(False) # hide operation description @@ -1965,10 +1990,6 @@ def logout(self): self.ui.activeOperationDesc.setText(self.ui.tr("Select Operation to View Description.")) # set usernameLabel back to default self.ui.usernameLabel.setText("User") - # disconnect socket - if self.conn is not None: - self.conn.disconnect() - self.conn = None # Turn off work locally toggle self.ui.workLocallyCheckbox.blockSignals(True) self.ui.workLocallyCheckbox.setChecked(False) @@ -1991,11 +2012,9 @@ def logout(self): self.operation_archive_browser.hide() - # Don't try to activate local flighttrack while testing - if "pytest" not in sys.modules: - # activate first local flighttrack after logging out - self.ui.listFlightTracks.setCurrentRow(0) - self.ui.activate_selected_flight_track() + # activate first local flighttrack after logging out + self.ui.listFlightTracks.setCurrentRow(0) + self.ui.activate_selected_flight_track() class MscolabMergeWaypointsDialog(QtWidgets.QDialog, merge_wp_ui.Ui_MergeWaypointsDialog): diff --git a/mslib/msui/mscolab_admin_window.py b/mslib/msui/mscolab_admin_window.py index 572e658ac..71e50182d 100644 --- a/mslib/msui/mscolab_admin_window.py +++ b/mslib/msui/mscolab_admin_window.py @@ -180,8 +180,8 @@ def set_label_text(self): if r.text != "False": _json = json.loads(r.text) creator_name = _json["username"] - self.operationNameLabel.setText(f"Operation: {self.operation_name}") - self.creatorNameLabel.setText(f"Creator: {creator_name}") + self.operationNameLabel.setText(f"Operation: {self.operation_name}") + self.creatorNameLabel.setText(f"Creator: {creator_name}") self.usernameLabel.setText(f"Logged In: {self.user['username']}") def load_import_operations(self): diff --git a/mslib/msui/mscolab_version_history.py b/mslib/msui/mscolab_version_history.py index 1d080cf0e..869ef2527 100644 --- a/mslib/msui/mscolab_version_history.py +++ b/mslib/msui/mscolab_version_history.py @@ -273,7 +273,7 @@ def handle_undo(self): "token": self.token, "ch_id": self.changes.currentItem().id } - url = urljoin(self.mscolab_server_url, 'undo') + url = urljoin(self.mscolab_server_url, 'undo_changes') r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) if r.text != "False": # reload windows diff --git a/mslib/msui/msui_mainwindow.py b/mslib/msui/msui_mainwindow.py index 3d9d9fb60..37659527f 100644 --- a/mslib/msui/msui_mainwindow.py +++ b/mslib/msui/msui_mainwindow.py @@ -48,7 +48,7 @@ from mslib.msui.updater import UpdaterUI from mslib.plugins.io.csv import load_from_csv, save_to_csv from mslib.msui.icons import icons, python_powered -from mslib.utils.qt import get_open_filenames, get_save_filename +from mslib.utils.qt import get_open_filenames, get_save_filename, show_popup from mslib.utils.config import read_config_file, config_loader from PyQt5 import QtGui, QtCore, QtWidgets @@ -824,8 +824,12 @@ def create_view_handler(self, _type): if self.local_active: self.create_view(_type, self.active_flight_track) else: - self.mscolab.waypoints_model.name = self.mscolab.active_operation_name - self.create_view(_type, self.mscolab.waypoints_model) + try: + self.mscolab.waypoints_model.name = self.mscolab.active_operation_name + self.create_view(_type, self.mscolab.waypoints_model) + except AttributeError: + # can happen, when the servers secret was changed + show_popup(self.mscolab.ui, "Error", "Session expired, new login required") def create_view(self, _type, model): """Method called when the user selects a new view to be opened. Creates @@ -836,7 +840,7 @@ def create_view(self, _type, model): view_window = None if _type == "topview": # Top view. - view_window = topview.MSUITopViewWindow(parent=self, model=model, + view_window = topview.MSUITopViewWindow(mainwindow=self, model=model, active_flighttrack=self.active_flight_track, mscolab_server_url=self.mscolab.mscolab_server_url, token=self.mscolab.token) diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index 08b494274..8c13cabfd 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -636,6 +636,9 @@ def _parse_vtimes(self): self.vtime_name = valid_time_names[0] values = self.extents[self.vtime_name]["values"] self.allowed_valid_times = sorted(self.parent.dock_widget.parse_time_extent(values)) + while len(self.allowed_valid_times) > 1000: + logging.warning("Too many valid times (%s). discarding 90%%.", len(self.allowed_valid_times)) + self.allowed_valid_times = self.allowed_valid_times[::10] self.vtimes = [_time.isoformat() + "Z" for _time in self.allowed_valid_times] if len(self.allowed_valid_times) == 0: logging.error("Cannot determine valid time format of %s for %s", self.header.text(0), self.text(0)) diff --git a/mslib/msui/multiple_flightpath_dockwidget.py b/mslib/msui/multiple_flightpath_dockwidget.py index df0d40669..b935a15cf 100644 --- a/mslib/msui/multiple_flightpath_dockwidget.py +++ b/mslib/msui/multiple_flightpath_dockwidget.py @@ -177,7 +177,8 @@ def __init__(self, parent=None, view=None, listFlightTracks=None, @QtCore.pyqtSlot() def logout(self): - self.operations.logout_mscolab() + if self.operations is not None: + self.operations.logout_mscolab() self.ui.signal_listFlighttrack_doubleClicked.disconnect() self.ui.signal_permission_revoked.disconnect() self.ui.signal_render_new_permission.disconnect() @@ -524,9 +525,6 @@ def __init__(self, parent, mscolab_server_url, token, list_operation_track, list self.operation_activated = False self.color_change = False - # Connect signals and slots - self.list_operation_track.itemChanged.connect(self.set_flag) - # Load operations from wps server server_operations = self.get_wps_from_server() sorted_server_operations = sorted(server_operations, key=lambda d: d["path"]) @@ -538,6 +536,10 @@ def __init__(self, parent, mscolab_server_url, token, list_operation_track, list wp_model.name = operations["path"] self.create_operation(op_id, wp_model) + # This needs to be done after operations are loaded + # Connect signals and slots + self.list_operation_track.itemChanged.connect(self.set_flag) + def set_flag(self): if self.operation_added: self.operation_added = False @@ -616,6 +618,8 @@ def activate_operation(self): """ Activate Mscolab Operation """ + # disconnect itemChanged during activation loop + self.list_operation_track.itemChanged.disconnect(self.set_flag) font = QtGui.QFont() for i in range(self.list_operation_track.count()): listItem = self.list_operation_track.item(i) @@ -634,6 +638,8 @@ def activate_operation(self): listItem.setFlags(listItem.flags() | QtCore.Qt.ItemIsUserCheckable) self.set_activate_flag() listItem.setFont(font) + # connect itemChanged after everything setup, otherwise it will be triggered on each entry + self.list_operation_track.itemChanged.connect(self.set_flag) def save_last_used_operation(self, op_id): if self.active_op_id is not None: @@ -680,6 +686,7 @@ def operationRemoved(self, op_id): """ Slot to remove operation. """ + self.list_operation_track.itemChanged.disconnect(self.set_flag) self.operation_removed = True for index in range(self.list_operation_track.count()): if self.list_operation_track.item(index).op_id == op_id: @@ -690,6 +697,7 @@ def operationRemoved(self, op_id): self.list_operation_track.takeItem(index) self.active_op_id = None break + self.list_operation_track.itemChanged.connect(self.set_flag) def set_activate_flag(self): if not self.operation_activated: diff --git a/mslib/msui/qt5/ui_mainwindow.py b/mslib/msui/qt5/ui_mainwindow.py index 890ae2112..2b8056f50 100644 --- a/mslib/msui/qt5/ui_mainwindow.py +++ b/mslib/msui/qt5/ui_mainwindow.py @@ -55,6 +55,7 @@ def setupUi(self, MSUIMainWindow): self.userOptionsTb.setObjectName("userOptionsTb") self.userOptionsHL.addWidget(self.userOptionsTb, 0, QtCore.Qt.AlignRight) self.connectBtn = QtWidgets.QPushButton(self.MSColabConnectGb) + self.connectBtn.setAutoDefault(True) self.connectBtn.setObjectName("connectBtn") self.userOptionsHL.addWidget(self.connectBtn) self.userOptionsHL.setStretch(0, 1) @@ -156,7 +157,7 @@ def setupUi(self, MSUIMainWindow): self.gridLayout.setColumnStretch(0, 1) MSUIMainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MSUIMainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 738, 20)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 738, 22)) self.menubar.setNativeMenuBar(False) self.menubar.setObjectName("menubar") self.menuFile = QtWidgets.QMenu(self.menubar) diff --git a/mslib/msui/qt5/ui_mscolab_connect_dialog.py b/mslib/msui/qt5/ui_mscolab_connect_dialog.py index 2f0061373..31c5ac0b4 100644 --- a/mslib/msui/qt5/ui_mscolab_connect_dialog.py +++ b/mslib/msui/qt5/ui_mscolab_connect_dialog.py @@ -14,7 +14,7 @@ class Ui_MSColabConnectDialog(object): def setupUi(self, MSColabConnectDialog): MSColabConnectDialog.setObjectName("MSColabConnectDialog") - MSColabConnectDialog.resize(478, 255) + MSColabConnectDialog.resize(478, 271) self.verticalLayout = QtWidgets.QVBoxLayout(MSColabConnectDialog) self.verticalLayout.setContentsMargins(12, 10, 10, 10) self.verticalLayout.setSpacing(5) @@ -29,9 +29,12 @@ def setupUi(self, MSColabConnectDialog): self.urlCb.setObjectName("urlCb") self.horizontalLayout_2.addWidget(self.urlCb) self.connectBtn = QtWidgets.QPushButton(MSColabConnectDialog) - self.connectBtn.setAutoDefault(False) + self.connectBtn.setAutoDefault(True) self.connectBtn.setObjectName("connectBtn") self.horizontalLayout_2.addWidget(self.connectBtn) + self.disconnectBtn = QtWidgets.QPushButton(MSColabConnectDialog) + self.disconnectBtn.setObjectName("disconnectBtn") + self.horizontalLayout_2.addWidget(self.disconnectBtn) self.horizontalLayout_2.setStretch(1, 1) self.verticalLayout.addLayout(self.horizontalLayout_2) self.line = QtWidgets.QFrame(MSColabConnectDialog) @@ -51,7 +54,7 @@ def setupUi(self, MSColabConnectDialog): self.addUserBtn.setObjectName("addUserBtn") self.gridLayout_3.addWidget(self.addUserBtn, 4, 1, 1, 1) self.loginBtn = QtWidgets.QPushButton(self.loginPage) - self.loginBtn.setAutoDefault(False) + self.loginBtn.setAutoDefault(True) self.loginBtn.setObjectName("loginBtn") self.gridLayout_3.addWidget(self.loginBtn, 3, 0, 1, 2) self.loginPasswordLe = QtWidgets.QLineEdit(self.loginPage) @@ -181,10 +184,13 @@ def retranslateUi(self, MSColabConnectDialog): self.urlCb.setToolTip(_translate("MSColabConnectDialog", "Enter Mscolab Server URL")) self.connectBtn.setToolTip(_translate("MSColabConnectDialog", "Connect to entered URL")) self.connectBtn.setText(_translate("MSColabConnectDialog", "Connect")) + self.connectBtn.setShortcut(_translate("MSColabConnectDialog", "Return")) + self.disconnectBtn.setText(_translate("MSColabConnectDialog", "Disconnect")) self.addUserBtn.setToolTip(_translate("MSColabConnectDialog", "Add new user to the server")) self.addUserBtn.setText(_translate("MSColabConnectDialog", "Add user")) self.loginBtn.setToolTip(_translate("MSColabConnectDialog", "Login using entered credentials")) self.loginBtn.setText(_translate("MSColabConnectDialog", "Login")) + self.loginBtn.setShortcut(_translate("MSColabConnectDialog", "Return")) self.loginPasswordLe.setPlaceholderText(_translate("MSColabConnectDialog", "Password")) self.loginEmailLe.setPlaceholderText(_translate("MSColabConnectDialog", "Email ID")) self.clickNewUserLabel.setText(_translate("MSColabConnectDialog", "Click here if new user")) diff --git a/mslib/msui/socket_control.py b/mslib/msui/socket_control.py index 7302b7a37..840081905 100644 --- a/mslib/msui/socket_control.py +++ b/mslib/msui/socket_control.py @@ -195,4 +195,24 @@ def save_file(self, token, op_id, content, comment=None): self.signal_reload.emit(op_id) def disconnect(self): + # Get all pyqtSignals defined in this class and disconnect them from all slots + allSignals = { + attr + for attr in dir(self.__class__) + if isinstance(getattr(self.__class__, attr), QtCore.pyqtSignal) + } + inheritedSignals = { + attr + for base_class in self.__class__.__bases__ + for attr in dir(base_class) + if isinstance(getattr(base_class, attr), QtCore.pyqtSignal) + } + signals = {getattr(self, signal) for signal in allSignals - inheritedSignals} + for signal in signals: + try: + signal.disconnect() + except TypeError: + # The disconnect call can fail if there are no connected slots, so catch that error here + pass + self.sio.disconnect() diff --git a/mslib/msui/topview.py b/mslib/msui/topview.py index 8682fd292..4cbf240d1 100644 --- a/mslib/msui/topview.py +++ b/mslib/msui/topview.py @@ -185,13 +185,27 @@ class MSUITopViewWindow(MSUIMplViewWindow, ui.Ui_TopViewWindow): signal_permission_revoked = QtCore.pyqtSignal(int) signal_render_new_permission = QtCore.pyqtSignal(int, str) - def __init__(self, parent=None, model=None, _id=None, active_flighttrack=None, mscolab_server_url=None, token=None): + def __init__(self, parent=None, mainwindow=None, model=None, _id=None, + active_flighttrack=None, mscolab_server_url=None, token=None): """ Set up user interface, connect signal/slots. """ super().__init__(parent, model, _id) logging.debug(_id) - self.ui = parent + self.mainwindow_signal_login_mscolab = mainwindow.signal_login_mscolab + self.mainwindow_signal_logout_mscolab = mainwindow.signal_logout_mscolab + self.mainwindow_signal_listFlighttrack_doubleClicked = mainwindow.signal_listFlighttrack_doubleClicked + self.mainwindow_signal_activate_operation = mainwindow.signal_activate_operation + self.mainwindow_signal_permission_revoked = mainwindow.signal_permission_revoked + self.mainwindow_signal_render_new_permission = mainwindow.signal_render_new_permission + self.mainwindow_signal_activate_flighttrack = mainwindow.signal_activate_flighttrack + self.mainwindow_signal_activate_operation = mainwindow.signal_activate_operation + self.mainwindow_signal_login_mscolab = mainwindow.signal_login_mscolab + self.mainwindow_signal_logout_mscolab = mainwindow.signal_logout_mscolab + self.mainwindow_listFlightTracks = mainwindow.listFlightTracks + self.mainwindow_filterCategoryCb = mainwindow.filterCategoryCb + self.mainwindow_listOperationsMSC = mainwindow.listOperationsMSC + self.setupUi(self) self.setWindowIcon(QtGui.QIcon(icons('64x64'))) @@ -230,15 +244,15 @@ def __init__(self, parent=None, model=None, _id=None, active_flighttrack=None, m # Tool opener. self.cbTools.currentIndexChanged.connect(self.openTool) - if parent is not None: + if mainwindow is not None: # Update flighttrack - self.ui.signal_activate_flighttrack.connect(self.update_active_flighttrack) - self.ui.signal_activate_operation.connect(self.update_active_operation) + self.mainwindow_signal_activate_flighttrack.connect(self.update_active_flighttrack) + self.mainwindow_signal_activate_operation.connect(self.update_active_operation) - self.ui.signal_operation_added.connect(self.add_operation_slot) - self.ui.signal_operation_removed.connect(self.remove_operation_slot) + self.signal_operation_added.connect(self.add_operation_slot) + self.signal_operation_removed.connect(self.remove_operation_slot) - self.ui.signal_login_mscolab.connect(self.login) + self.mainwindow_signal_login_mscolab.connect(self.login) def __del__(self): del self.mpl.canvas.waypoints_interactor @@ -339,18 +353,19 @@ def openTool(self, index): elif index == MULTIPLEFLIGHTPATH: title = "Multiple Flightpath" widget = mf.MultipleFlightpathControlWidget(parent=self, view=self.mpl.canvas, - listFlightTracks=self.ui.listFlightTracks, - listOperationsMSC=self.ui.listOperationsMSC, - category=self.ui.filterCategoryCb, + listFlightTracks=self.mainwindow_listFlightTracks, + listOperationsMSC=self.mainwindow_listOperationsMSC, + category=self.mainwindow_filterCategoryCb, activeFlightTrack=self.active_flighttrack, mscolab_server_url=self.mscolab_server_url, token=self.token) - self.ui.signal_logout_mscolab.connect(lambda: self.signal_logout_mscolab.emit()) - self.ui.signal_listFlighttrack_doubleClicked.connect( + self.mainwindow_signal_logout_mscolab.connect(lambda: self.signal_logout_mscolab.emit()) + self.mainwindow_signal_listFlighttrack_doubleClicked.connect( lambda: self.signal_listFlighttrack_doubleClicked.emit()) - self.ui.signal_permission_revoked.connect(lambda op_id: self.signal_permission_revoked.emit(op_id)) - self.ui.signal_render_new_permission.connect( + self.mainwindow_signal_permission_revoked.connect( + lambda op_id: self.signal_permission_revoked.emit(op_id)) + self.mainwindow_signal_render_new_permission.connect( lambda op_id, path: self.signal_render_new_permission.emit(op_id, path)) if self.active_op_id is not None: self.signal_activate_operation.emit(self.active_op_id) @@ -362,12 +377,12 @@ def openTool(self, index): self.createDockWidget(index, title, widget) def closed(self): - self.ui.signal_login_mscolab.disconnect() - self.ui.signal_logout_mscolab.disconnect() - self.ui.signal_listFlighttrack_doubleClicked.disconnect() - self.ui.signal_activate_operation.disconnect() - self.ui.signal_permission_revoked.disconnect() - self.ui.signal_render_new_permission.disconnect() + self.mainwindow_signal_login_mscolab.disconnect() + self.mainwindow_signal_logout_mscolab.disconnect() + self.mainwindow_signal_listFlighttrack_doubleClicked.disconnect() + self.mainwindow_signal_activate_operation.disconnect() + self.mainwindow_signal_permission_revoked.disconnect() + self.mainwindow_signal_render_new_permission.disconnect() @QtCore.pyqtSlot() def disable_cbs(self): diff --git a/mslib/msui/ui/ui_mainwindow.ui b/mslib/msui/ui/ui_mainwindow.ui index 46aec65a5..9f6209063 100644 --- a/mslib/msui/ui/ui_mainwindow.ui +++ b/mslib/msui/ui/ui_mainwindow.ui @@ -112,6 +112,9 @@ Connect + + true + @@ -353,7 +356,7 @@ Double click a operation to activate and view its description. 0 0 738 - 20 + 22 diff --git a/mslib/msui/ui/ui_mscolab_connect_dialog.ui b/mslib/msui/ui/ui_mscolab_connect_dialog.ui index 85d683571..e4fa62990 100644 --- a/mslib/msui/ui/ui_mscolab_connect_dialog.ui +++ b/mslib/msui/ui/ui_mscolab_connect_dialog.ui @@ -7,7 +7,7 @@ 0 0 478 - 255 + 271 @@ -30,7 +30,7 @@ 10 - + @@ -56,8 +56,18 @@ Connect + + Return + - false + true + + + + + + + Disconnect @@ -110,8 +120,11 @@ Login + + Return + - false + true diff --git a/mslib/msui/viewwindows.py b/mslib/msui/viewwindows.py index 2d20f2838..6a0b19503 100644 --- a/mslib/msui/viewwindows.py +++ b/mslib/msui/viewwindows.py @@ -234,10 +234,8 @@ def setFlightTrackModel(self, model): # Update Top View flighttrack name if hasattr(self.mpl.canvas, "map"): - text = self.mpl.canvas.map.crs_text.get_text() - old_name = self.mpl.canvas.map.operation_name self.mpl.canvas.map.operation_name = model.name - self.mpl.canvas.map.crs_text.set_text(text.replace(old_name, model.name)) + self.mpl.canvas.map.update_info_text(name=model.name) self.mpl.canvas.map.ax.figure.canvas.draw() def getView(self): diff --git a/mslib/msui/wms_control.py b/mslib/msui/wms_control.py index 2bd73f522..e793bc813 100644 --- a/mslib/msui/wms_control.py +++ b/mslib/msui/wms_control.py @@ -1285,7 +1285,7 @@ def normalize_crs(crs): if ret == QtWidgets.QMessageBox.Ignore: self.check_for_allowed_crs = False elif ret == QtWidgets.QMessageBox.No: - return + return [] # get...Time() will return None if the corresponding checkboxes are # disabled. objects passed to wms.getmap will not be included @@ -1478,24 +1478,21 @@ def append_multiple_images(self, imgs): """ images = [x for x in imgs if x] if images: - if hasattr(self.view, 'fig') is False: - QtWidgets.QMessageBox.warning(self, self.tr("Web Map Service"), "We have only fig implemented") - else: - # Add border around seperate legends - if len(images) > 1: - images = [ImageOps.expand(x, border=1, fill="black") for x in images] - max_height = int((self.view.fig.get_size_inches() * self.view.fig.get_dpi())[1] * 0.99) - width = max([image.width for image in images]) - height = sum([image.height for image in images]) - result = Image.new("RGBA", (width, height)) - current_height = 0 - for i, image in enumerate(images): - result.paste(image, (0, current_height - i)) - current_height += image.height - - if max_height < result.height: - result.thumbnail((result.width, max_height), Image.ANTIALIAS) - return result + # Add border around seperate legends + if len(images) > 1: + images = [ImageOps.expand(x, border=1, fill="black") for x in images] + max_height = int((self.view.plotter.fig.get_size_inches() * self.view.plotter.fig.get_dpi())[1] * 0.99) + width = max([image.width for image in images]) + height = sum([image.height for image in images]) + result = Image.new("RGBA", (width, height)) + current_height = 0 + for i, image in enumerate(images): + result.paste(image, (0, current_height - i)) + current_height += image.height + + if max_height < result.height: + result.thumbnail((result.width, max_height), Image.ANTIALIAS) + return result class VSecWMSControlWidget(WMSControlWidget): diff --git a/mslib/mswms/demodata.py b/mslib/mswms/demodata.py index 986c93802..ccb80d06c 100644 --- a/mslib/mswms/demodata.py +++ b/mslib/mswms/demodata.py @@ -969,6 +969,8 @@ def create_server_config(self, detailed_information=False): #service_country = "Germany" #service_fees = "none" #service_access_constraints = "This service is intended for research purposes only." +#imprint = "" +#gdpr = "" # diff --git a/mslib/mswms/wms.py b/mslib/mswms/wms.py index 6f96f95a7..fc06e19f4 100644 --- a/mslib/mswms/wms.py +++ b/mslib/mswms/wms.py @@ -69,12 +69,6 @@ # Flask basic auth's documentation # https://flask-basicauth.readthedocs.io/en/latest/#flask.ext.basicauth.BasicAuth.check_credentials -app = create_app(__name__) -auth = HTTPBasicAuth() - -realm = 'Mission Support Web Map Service' -app.config['realm'] = realm - class default_mswms_settings: base_dir = os.path.abspath(os.path.dirname(__file__)) @@ -97,6 +91,8 @@ class default_mswms_settings: register_horizontal_layers = [] register_vertical_layers = [] register_linear_layers = [] + imprint = "" + gdpr = "" data = {} enable_basic_http_authentication = False __file__ = None @@ -110,6 +106,12 @@ class default_mswms_settings: except ImportError as ex: logging.warning("Couldn't import mswms_settings (ImportError:'%s'), Using dummy config.", ex) +app = create_app(__name__, imprint=mswms_settings.imprint, gdpr=mswms_settings.gdpr) +auth = HTTPBasicAuth() + +realm = 'Mission Support Web Map Service' +app.config['realm'] = realm + try: import mswms_auth except ImportError as ex: diff --git a/mslib/static/templates/footer.html b/mslib/static/templates/footer.html index 713e1c37d..7bd2cae4d 100644 --- a/mslib/static/templates/footer.html +++ b/mslib/static/templates/footer.html @@ -16,9 +16,17 @@
- + {% if file_exists(imprint) %} + + {% endif %} + {% if file_exists(gdpr) %} + + + {% endif %}
diff --git a/mslib/utils/airdata.py b/mslib/utils/airdata.py index b7ae556d2..8e0a40157 100644 --- a/mslib/utils/airdata.py +++ b/mslib/utils/airdata.py @@ -102,7 +102,7 @@ def get_airports(force_download=False, url=None): _airports_mtime = Airspace().airports_mtime if url is None: - url = "https://ourairports.com/data/airports.csv" + url = "https://davidmegginson.github.io/ourairports-data/airports.csv" file_exists = os.path.exists(os.path.join(OSDIR, "downloads", "aip", "airports.csv")) @@ -252,7 +252,7 @@ def get_airspaces(countries=None): for data in airspace_data["polygon"].split(",")] _airspaces.append(airspace_data) _airspaces_mtime[file] = os.path.getmtime(os.path.join(OSDIR, "downloads", "aip", file)) - else: - QtWidgets.QMessageBox.information(None, "No Airspaces data in file:", f"{file}") + else: + QtWidgets.QMessageBox.information(None, "No Airspaces data in file:", f"{file}") return _airspaces diff --git a/mslib/utils/config.py b/mslib/utils/config.py index c740c220e..af9a918e5 100644 --- a/mslib/utils/config.py +++ b/mslib/utils/config.py @@ -111,8 +111,9 @@ class MSUIDefaultConfig: # URLs of default WMS servers. default_WMS = [ "http://localhost:8081/", - "http://eumetview.eumetsat.int/geoserver/wms", - "https://apps.ecmwf.int/wms/?token=public" + "https://view.eumetsat.int/geoserver/wms", + "http://eccharts.ecmwf.int/wms/?token=public", + "https://neo.gsfc.nasa.gov/wms/wms" ] default_VSEC_WMS = [ diff --git a/mslib/utils/coordinate.py b/mslib/utils/coordinate.py index b645fe5ec..54ecb2ff0 100644 --- a/mslib/utils/coordinate.py +++ b/mslib/utils/coordinate.py @@ -110,15 +110,13 @@ def rotate_point(point, angle, origin=(0, 0)): def get_projection_params(proj): proj = proj.lower() if proj.startswith("crs:"): - raise ValueError("CRS not supported") - projid = proj[4:] if projid == "84": proj_params = { "basemap": {"projection": "cyl"}, "bbox": "degree"} else: - raise ValueError("unsupported CRS code: '%s'", proj) + raise ValueError("Only CRS code 84 is supported: '%s' given", proj) elif proj.startswith("auto:"): raise ValueError("AUTO not supported") diff --git a/mslib/utils/time.py b/mslib/utils/time.py index 00d38f6a2..22b020013 100644 --- a/mslib/utils/time.py +++ b/mslib/utils/time.py @@ -43,7 +43,10 @@ def parse_iso_datetime(string): def parse_iso_duration(string): - return isodate.parse_duration(string) + try: + return isodate.parse_duration(string) + except isodate.ISO8601Error: + return datetime.timedelta(weeks=4) JSEC_START = datetime.datetime(2000, 1, 1) diff --git a/mslib/utils/units.py b/mslib/utils/units.py index 2a6105111..91bc39434 100644 --- a/mslib/utils/units.py +++ b/mslib/utils/units.py @@ -99,16 +99,16 @@ def convert_to(value, from_unit, to_unit, default=1.): value_unit = units.Quantity(value, from_unit) result = value_unit.to(to_unit).magnitude except pint.UndefinedUnitError: - logging.error("Error in unit conversion (undefined) '%s'/'%s'", from_unit, to_unit) + logging.warning("Error in unit conversion (undefined) '%s'/'%s'", from_unit, to_unit) result = value * default except pint.DimensionalityError: if units(to_unit).to_base_units().units == units.m: try: result = (value_unit / units.Quantity(9.81, "m s^-2")).to(to_unit).magnitude except pint.DimensionalityError: - logging.error("Error in unit conversion (dimensionality) %s/%s", from_unit, to_unit) + logging.warning("Error in unit conversion (dimensionality) '%s'/'%s'", from_unit, to_unit) result = value * default else: - logging.error("Error in unit conversion (dimensionality) %s/%s", from_unit, to_unit) + logging.warning("Error in unit conversion (dimensionality) '%s'/'%s'", from_unit, to_unit) result = value * default return result diff --git a/mslib/version.py b/mslib/version.py index b2dfa20e9..642cdf5d0 100644 --- a/mslib/version.py +++ b/mslib/version.py @@ -24,4 +24,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__ = u'8.2.0' +__version__ = u'8.3.1' diff --git a/pytest.ini b/pytest.ini index 3f0e6d3f9..33d560e1e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -6,7 +6,7 @@ log_file = pytest.log log_file_level = DEBUG log_file_format = %(asctime)s %(levelname)s %(message)s log_file_date_format = %Y-%m-%d %H:%M:%S -timeout = 60 +timeout = 30 filterwarnings = # These namespaces are declared in a way not conformant with PEP420. Not much we can do about that here, we should keep an eye on when this is fixed in our dependencies though. ignore:Deprecated call to `pkg_resources.declare_namespace\('(xstatic|xstatic\.pkg|mpl_toolkits|mpl_toolkits\.basemap_data|sphinxcontrib|zope|fs|fs\.opener)'\)`\.:DeprecationWarning diff --git a/requirements.d/development.txt b/requirements.d/development.txt index 4a5553df0..e57232b2b 100644 --- a/requirements.d/development.txt +++ b/requirements.d/development.txt @@ -23,3 +23,6 @@ pytest-reverse eventlet>0.30.2 dnspython>=2.0.0, <2.3.0 gsl==2.7.0 +boa +xmlschema<2.5.0 + diff --git a/tests/_test_mscolab/test_chat_manager.py b/tests/_test_mscolab/test_chat_manager.py index 30ceef682..abe1dd2b0 100644 --- a/tests/_test_mscolab/test_chat_manager.py +++ b/tests/_test_mscolab/test_chat_manager.py @@ -24,11 +24,14 @@ See the License for the specific language governing permissions and limitations under the License. """ +import os +import secrets from flask_testing import TestCase +from werkzeug.datastructures import FileStorage from mslib.mscolab.conf import mscolab_settings -from mslib.mscolab.models import Message, MessageType +from mslib.mscolab.models import Operation, Message, MessageType from mslib.mscolab.mscolab import handle_db_reset from mslib.mscolab.server import APP from mslib.mscolab.seed import add_user, get_user, add_operation, add_user_to_operation @@ -89,3 +92,17 @@ def test_delete_messages(self): self.cm.delete_message(message.id) message = Message.query.filter(Message.id == message.id).first() assert message is None + + def test_add_attachment(self): + sample_path = os.path.join(os.path.dirname(__file__), "..", "data") + filename = "example.csv" + name, ext = filename.split('.') + open_csv = os.path.join(sample_path, "example.csv") + operation = Operation.query.filter_by(path=self.operation_name).first() + token = secrets.token_urlsafe(16) + with open(open_csv, 'rb') as fp: + file = FileStorage(fp, filename=filename, content_type="text/csv") + static_path = self.cm.add_attachment(operation.id, mscolab_settings.UPLOAD_FOLDER, file, token) + assert name in static_path + assert static_path.endswith(ext) + assert token in static_path diff --git a/tests/_test_mscolab/test_file_manager.py b/tests/_test_mscolab/test_file_manager.py index af41ba37b..11852b27b 100644 --- a/tests/_test_mscolab/test_file_manager.py +++ b/tests/_test_mscolab/test_file_manager.py @@ -26,10 +26,11 @@ """ from flask_testing import TestCase import os +import datetime import pytest from mslib.mscolab.conf import mscolab_settings -from mslib.mscolab.models import Operation +from mslib.mscolab.models import Operation, User from mslib.mscolab.server import APP from mslib.mscolab.file_manager import FileManager from mslib.mscolab.seed import add_user, get_user @@ -80,7 +81,40 @@ def setUp(self): def tearDown(self): pass - @pytest.mark.skip(reason="needs review for py311") + def test_modify_user(self): + with self.app.test_client(): + user = User("user@example.com", "user", "password") + assert user.id is None + assert User.query.filter_by(emailid=user.emailid).first() is None + # creeat the user + self.fm.modify_user(user, action="create") + user_query = User.query.filter_by(emailid=user.emailid).first() + assert user_query.id is not None + assert user_query is not None + assert user_query.confirmed is False + # cannot create a user a second time + assert self.fm.modify_user(user, action="create") is False + # confirming the user + confirm_time = datetime.datetime.now() + datetime.timedelta(days=1) + self.fm.modify_user(user_query, attribute="confirmed_on", value=confirm_time) + self.fm.modify_user(user_query, attribute="confirmed", value=True) + user_query = User.query.filter_by(id=user.id).first() + assert user_query.confirmed is True + assert user_query.confirmed_on == confirm_time + assert user_query.confirmed_on > user_query.registered_on + # deleting the user + self.fm.modify_user(user_query, action="delete") + user_query = User.query.filter_by(id=user_query.id).first() + assert user_query is None + + def test_modify_user_special_cases(self): + user1 = User("user1@example.com", "user1", "password") + user2 = User("user2@example.com", "user2", "password") + self.fm.modify_user(user1, action="create") + self.fm.modify_user(user2, action="create") + user_query1 = User.query.filter_by(emailid=user1.emailid).first() + assert self.fm.modify_user(user_query1, "emailid", user2.emailid) is False + def test_fetch_operation_creator(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path="more_than_one") @@ -204,11 +238,10 @@ def test_update_operation(self): assert ren_operation.id == operation.id assert ren_operation.path == rename_to - def test_delete_file(self): - # Todo rename "file" to operation + def test_delete_operation(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path='operation4') - assert self.fm.delete_file(operation.id, self.user) + assert self.fm.delete_operation(operation.id, self.user) assert Operation.query.filter_by(path=flight_path).first() is None def test_get_authorized_users(self): @@ -245,7 +278,7 @@ def test_get_change_content(self): assert self.fm.save_file(operation.id, self.content1, self.user) assert self.fm.save_file(operation.id, self.content2, self.user) all_changes = self.fm.get_all_changes(operation.id, self.user) - assert self.fm.get_change_content(all_changes[1]["id"]) == self.content1 + assert self.fm.get_change_content(all_changes[1]["id"], self.user) == self.content1 def test_set_version_name(self): with self.app.test_client(): @@ -273,15 +306,15 @@ def test_undo(self): assert self.fm.save_file(operation.id, self.content2, self.user) all_changes = self.fm.get_all_changes(operation.id, self.user) # crestor - assert self.fm.undo(all_changes[1]["id"], self.user) + assert self.fm.undo_changes(all_changes[1]["id"], self.user) # check collaborator self.fm.add_bulk_permission(operation.id, self.user, [self.collaboratoruser.id], "collaborator") assert self.fm.is_collaborator(self.collaboratoruser.id, operation.id) - assert self.fm.undo(all_changes[1]["id"], self.collaboratoruser) + assert self.fm.undo_changes(all_changes[1]["id"], self.collaboratoruser) # check viewer self.fm.add_bulk_permission(operation.id, self.user, [self.vieweruser.id], "viewer") assert self.fm.is_viewer(self.vieweruser.id, operation.id) - assert self.fm.undo(all_changes[1]["id"], self.vieweruser) is False + assert self.fm.undo_changes(all_changes[1]["id"], self.vieweruser) is False def test_fetch_users_without_permission(self): with self.app.test_client(): diff --git a/tests/_test_mscolab/test_files.py b/tests/_test_mscolab/test_files.py index 6db498a21..df774e02f 100644 --- a/tests/_test_mscolab/test_files.py +++ b/tests/_test_mscolab/test_files.py @@ -129,7 +129,7 @@ def test_undo(self): changes = Change.query.filter_by(op_id=operation.id).all() assert changes is not None assert changes[0].id == 1 - assert self.fm.undo(changes[0].id, self.user) is True + assert self.fm.undo_changes(changes[0].id, self.user) is True assert len(self.fm.get_all_changes(operation.id, self.user)) == 3 assert "beta" in self.fm.get_file(operation.id, self.user) @@ -162,9 +162,9 @@ def test_delete_operation(self): with self.app.test_client(): self._create_operation(flight_path="f3") op_id = get_recent_op_id(self.fm, self.user) - assert self.fm.delete_file(op_id, self.user2) is False - assert self.fm.delete_file(op_id, self.user) is True - assert self.fm.delete_file(op_id, self.user) is False + assert self.fm.delete_operation(op_id, self.user2) is False + assert self.fm.delete_operation(op_id, self.user) is True + assert self.fm.delete_operation(op_id, self.user) is False permissions = Permission.query.filter_by(op_id=op_id).all() assert len(permissions) == 0 operations_db = Operation.query.filter_by(id=op_id).all() diff --git a/tests/_test_mscolab/test_files_api.py b/tests/_test_mscolab/test_files_api.py index 0aeb89473..1ac772f6f 100644 --- a/tests/_test_mscolab/test_files_api.py +++ b/tests/_test_mscolab/test_files_api.py @@ -182,12 +182,11 @@ def test_update_operation(self): operation = Operation.query.filter_by(path=new_flight_path).first() assert operation.description == new_description - def test_delete_file(self): - # ToDo rename to operation + def test_delete_operation(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path="V10") assert operation.path == flight_path - assert self.fm.delete_file(operation.id, self.user) + assert self.fm.delete_operation(operation.id, self.user) operation = Operation.query.filter_by(path=flight_path).first() assert operation is None @@ -206,9 +205,9 @@ def test_get_change_content(self): assert self.fm.save_file(operation.id, "content2", self.user) assert self.fm.save_file(operation.id, "content3", self.user) all_changes = self.fm.get_all_changes(operation.id, self.user) - previous_change = self.fm.get_change_content(all_changes[2]["id"]) + previous_change = self.fm.get_change_content(all_changes[2]["id"], self.user) assert previous_change == "content1" - previous_change = self.fm.get_change_content(all_changes[1]["id"]) + previous_change = self.fm.get_change_content(all_changes[1]["id"], self.user) assert previous_change == "content2" def test_set_version_name(self): diff --git a/tests/_test_mscolab/test_server.py b/tests/_test_mscolab/test_server.py index 1d6bc4882..c1bd4b460 100644 --- a/tests/_test_mscolab/test_server.py +++ b/tests/_test_mscolab/test_server.py @@ -88,13 +88,13 @@ def test_register_user(self): assert result["success"] is True result = register_user(self.userdata[0], self.userdata[1], self.userdata[2]) assert result["success"] is False - assert result["message"] == "Oh no, this email ID is already taken!" + assert result["message"] == "This email ID is already taken!" result = register_user("UV", self.userdata[1], self.userdata[2]) assert result["success"] is False - assert result["message"] == "Oh no, your email ID is not valid!" + assert result["message"] == "Your email ID is not valid!" result = register_user(self.userdata[0], self.userdata[1], self.userdata[0]) assert result["success"] is False - assert result["message"] == "Oh no, your username cannot contain @ symbol!" + assert result["message"] == "Your username cannot contain @ symbol!" def test_check_login(self): with self.app.test_client(): @@ -149,11 +149,11 @@ def test_delete_user(self): assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) with self.app.test_client() as test_client: token = self._get_token(test_client, self.userdata) - response = test_client.post('/delete_user', data={"token": token}) + response = test_client.post('/delete_own_account', data={"token": token}) assert response.status_code == 200 data = json.loads(response.data.decode('utf-8')) assert data["success"] is True - response = test_client.post('/delete_user', data={"token": "dsdsds"}) + response = test_client.post('/delete_own_account', data={"token": "dsdsds"}) assert response.status_code == 200 assert response.data.decode('utf-8') == "False" @@ -348,15 +348,6 @@ def test_set_last_used(self): data = json.loads(response.data.decode('utf-8')) assert data["success"] is True - def test_update_last_used(self): - assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) - with self.app.test_client() as test_client: - operation, token = self._create_operation(test_client, self.userdata) - response = test_client.post('/update_last_used', data={"token": token}) - assert response.status_code == 200 - data = json.loads(response.data.decode('utf-8')) - assert data["success"] is True - def test_get_users_without_permission(self): assert add_user(self.userdata[0], self.userdata[1], self.userdata[2]) unprevileged_user = 'UV20@uv20', 'UV20', 'uv20' diff --git a/tests/_test_msui/test_kmloverlay_dockwidget.py b/tests/_test_msui/test_kmloverlay_dockwidget.py index 130197436..d3eaf43a7 100644 --- a/tests/_test_msui/test_kmloverlay_dockwidget.py +++ b/tests/_test_msui/test_kmloverlay_dockwidget.py @@ -37,6 +37,8 @@ save_kml = os.path.join(ROOT_DIR, "merged_file123.kml") +# ToDo refactoring, extract helper methods into functions +# ToDo review needed helper functions class Test_KmlOverlayDockWidget(object): def setup_method(self): @@ -70,6 +72,7 @@ def select_file(self, file): # Utility function for single file path = fs.path.join(sample_path, file) filename = (path,) # converted to tuple self.window.select_file(filename) + self.window.load_file() QtWidgets.QApplication.processEvents() return path @@ -103,7 +106,7 @@ def test_select_file(self, mockbox): assert mockbox.critical.call_count == 0 assert self.window.listWidget.count() == index assert len(self.window.dict_files) == index - assert self.count_patches() > 0 + assert self.count_patches() == 9 self.window.select_all() self.window.remove_file() @@ -118,6 +121,7 @@ def test_select_file_error(self, mockbox): path = fs.path.join(sample_path, "satellite_predictor.txt") filename = (path,) # converted to tuple self.window.select_file(filename) + self.window.load_file() QtWidgets.QApplication.processEvents() assert mockbox.critical.call_count == 1 self.window.listWidget.clear() diff --git a/tests/_test_msui/test_mscolab.py b/tests/_test_msui/test_mscolab.py index 80b399ab6..6065c4475 100644 --- a/tests/_test_msui/test_mscolab.py +++ b/tests/_test_msui/test_mscolab.py @@ -38,7 +38,7 @@ from mslib.mscolab.models import Permission, User from mslib.msui.flighttrack import WaypointsTableModel from PyQt5 import QtCore, QtTest, QtWidgets -from mslib.utils.config import read_config_file, config_loader +from mslib.utils.config import read_config_file, config_loader, modify_config_file from tests.utils import mscolab_start_server, create_msui_settings_file, ExceptionMock from mslib.msui import msui from mslib.msui import mscolab @@ -64,6 +64,7 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.main_window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.main_window.create_new_flight_track() self.main_window.show() self.window = mscolab.MSColab_ConnectDialog(parent=self.main_window, mscolab=self.main_window.mscolab) self.window.urlCb.setEditText(self.url) @@ -115,15 +116,37 @@ def test_connect_success(self, mockbox, mockset): def test_disconnect(self): self._connect_to_mscolab() assert self.window.mscolab_server_url is not None - QtTest.QTest.mouseClick(self.window.connectBtn, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.window.disconnectBtn, QtCore.Qt.LeftButton) assert self.window.mscolab_server_url is None # set ui_name_winodw default assert self.main_window.usernameLabel.text() == 'User' def test_login(self): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) + self._login(self.userdata[0], self.userdata[2]) + QtWidgets.QApplication.processEvents() + # show logged in widgets + assert self.main_window.usernameLabel.text() == self.userdata[1] + assert self.main_window.connectBtn.isVisible() is False + assert self.main_window.mscolab.connect_window is None + assert self.main_window.local_active is True + # test operation listing visibility + assert self.main_window.listOperationsMSC.model().rowCount() == 1 + + @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) + def test_login_with_different_account_shows_update_credentials_popup(self, mockbox): + self._connect_to_mscolab() + connect_window = self.main_window.mscolab.connect_window self._login(self.userdata[0], self.userdata[2]) QtWidgets.QApplication.processEvents() + mockbox.assert_called_once_with( + connect_window, + "Update Credentials", + "You are using new credentials. Should your settings file be updated with the new credentials?", + mock.ANY, + mock.ANY, + ) # show logged in widgets assert self.main_window.usernameLabel.text() == self.userdata[1] assert self.main_window.connectBtn.isVisible() is False @@ -135,6 +158,7 @@ def test_login(self): def test_logout_action_trigger(self): # Login self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(self.userdata[0], self.userdata[2]) QtWidgets.QApplication.processEvents() assert self.main_window.usernameLabel.text() == self.userdata[1] @@ -149,6 +173,7 @@ def test_logout_action_trigger(self): def test_logout(self): # Login self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(self.userdata[0], self.userdata[2]) QtWidgets.QApplication.processEvents() assert self.main_window.usernameLabel.text() == self.userdata[1] @@ -163,6 +188,7 @@ def test_logout(self): @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) def test_add_user(self, mockmessage): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") assert config_loader(dataset="MSS_auth").get(self.url) == "something@something.org" assert mslib.utils.auth.get_password_from_keyring("MSCOLAB", @@ -197,6 +223,7 @@ def test_add_users_with_updating_credentials_in_config_file(self, mockmessage): assert config_loader(dataset="MSS_auth").get(self.url) == "something@something.org" self._connect_to_mscolab() assert self.window.mscolab_server_url is not None + modify_config_file({"MSS_auth": {self.url: "anand@something.org"}}) self._create_user("anand", "anand@something.org", "anand_pass") # check changed settings assert config_loader(dataset="MSS_auth").get(self.url) == "anand@something.org" @@ -246,8 +273,8 @@ class Test_Mscolab(object): sample_path = os.path.join(os.path.dirname(__file__), "..", "data") # import/export plugins import_plugins = { - "Text": ["txt", "mslib.plugins.io.text", "load_from_txt"], - "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"], + "TXT": ["txt", "mslib.plugins.io.text", "load_from_txt"], + "FliteStar": ["txt", "mslib.plugins.io.flitestar", "load_from_flitestar"], } export_plugins = { "Text": ["txt", "mslib.plugins.io.text", "save_to_txt"], @@ -277,6 +304,7 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.window.create_new_flight_track() self.window.show() def teardown_method(self): @@ -296,6 +324,7 @@ def teardown_method(self): def test_activate_operation(self): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(emailid=self.userdata[0], password=self.userdata[2]) # activate a operation self._activate_operation_at_index(0) @@ -305,6 +334,7 @@ def test_activate_operation(self): @mock.patch("PyQt5.QtWidgets.QMessageBox") def test_view_open(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(emailid=self.userdata[0], password=self.userdata[2]) # test after activating operation self._activate_operation_at_index(0) @@ -338,6 +368,7 @@ def test_view_open(self, mockbox): "Flight track (*.ftml)")) def test_handle_export(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(emailid=self.userdata[0], password=self.userdata[2]) self._activate_operation_at_index(0) self.window.actionExportFlightTrackFTML.trigger() @@ -349,50 +380,36 @@ def test_handle_export(self, mockbox): for i in range(wp_count): assert exported_waypoints.waypoint_data(i).lat == self.window.mscolab.waypoints_model.waypoint_data(i).lat - @pytest.mark.skip("fails on github with WebSocket transport is not available") - @pytest.mark.parametrize("ext", [".ftml", ".csv", ".txt"]) + @pytest.mark.parametrize("name", [("example.ftml", "actionImportFlightTrackFTML", 5), + ("example.csv", "actionImportFlightTrackCSV", 5), + ("example.txt", "actionImportFlightTrackTXT", 5), + ("flitestar.txt", "actionImportFlightTrackFliteStar", 10)]) @mock.patch("PyQt5.QtWidgets.QMessageBox") - def test_import_file(self, mockbox, ext): + def test_import_file(self, mockbox, name): self.window.remove_plugins() with mock.patch("mslib.msui.msui_mainwindow.config_loader", return_value=self.import_plugins): self.window.add_import_plugins("qt") - with mock.patch("mslib.msui.msui_mainwindow.config_loader", return_value=self.export_plugins): - self.window.add_export_plugins("qt") - file_path = fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, f'test_import{ext}') - with mock.patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName", return_value=(file_path, None)): - with mock.patch("PyQt5.QtWidgets.QFileDialog.getOpenFileName", return_value=(file_path, None)): - self._connect_to_mscolab() - self._login(emailid=self.userdata[0], password=self.userdata[2]) - self._activate_operation_at_index(0) - exported_wp = WaypointsTableModel(waypoints=self.window.mscolab.waypoints_model.waypoints) - full_name = f"actionExportFlightTrack{ext[1:]}" - for action in self.window.menuExportActiveFlightTrack.actions(): - if action.objectName() == full_name: - action.trigger() - break - assert os.path.exists(fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, f'test_import{ext}')) - QtWidgets.QApplication.processEvents() - self.window.mscolab.waypoints_model.invert_direction() - QtWidgets.QApplication.processEvents() - QtTest.QTest.qWait(100) - assert exported_wp.waypoint_data(0).lat != self.window.mscolab.waypoints_model.waypoint_data(0).lat - full_name = f"actionImportFlightTrack{ext[1:]}" - for action in self.window.menuImportFlightTrack.actions(): - if action.objectName() == full_name: - action.trigger() - break - QtWidgets.QApplication.processEvents() - QtTest.QTest.qWait(100) - assert len(self.window.mscolab.waypoints_model.waypoints) == 2 - imported_wp = self.window.mscolab.waypoints_model - wp_count = len(imported_wp.waypoints) - assert wp_count == 2 - for i in range(wp_count): - assert exported_wp.waypoint_data(i).lat == imported_wp.waypoint_data(i).lat + file_path = fs.path.join(self.sample_path, name[0]) + with mock.patch("mslib.msui.msui_mainwindow.get_open_filenames", return_value=[file_path]) as mockopen: + # with parametrize it is maybe too fast + QtTest.QTest.qWait(100) + self._connect_to_mscolab() + self._login(emailid=self.userdata[0], password=self.userdata[2]) + self._activate_operation_at_index(0) + wp = self.window.mscolab.waypoints_model + assert len(wp.waypoints) == 2 + for action in self.window.menuImportFlightTrack.actions(): + if action.objectName() == name[1]: + action.trigger() + break + assert mockopen.call_count == 1 + imported_wp = self.window.mscolab.waypoints_model + assert len(imported_wp.waypoints) == name[2] @pytest.mark.skip("Runs in a timeout locally > 60s") def test_work_locally_toggle(self): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(emailid=self.userdata[0], password=self.userdata[2]) self._activate_operation_at_index(0) self.window.workLocallyCheckbox.setChecked(True) @@ -413,6 +430,7 @@ def test_work_locally_toggle(self): @mock.patch("mslib.msui.mscolab.get_open_filename", return_value=os.path.join(sample_path, u"example.ftml")) def test_browse_add_operation(self, mockopen, mockmessage): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") assert self.window.listOperationsMSC.model().rowCount() == 0 self.window.actionAddOperation.trigger() @@ -436,59 +454,75 @@ def test_browse_add_operation(self, mockopen, mockmessage): assert item.operation_path == "example" assert item.access_level == "creator" - @mock.patch("PyQt5.QtWidgets.QErrorMessage") - def test_add_operation(self, mockbox): + def test_add_operation(self): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") assert self.window.usernameLabel.text() == 'something' assert self.window.connectBtn.isVisible() is False - self._create_operation("Alpha", "Description Alpha") - assert mockbox.return_value.showMessage.call_count == 1 - with mock.patch("PyQt5.QtWidgets.QLineEdit.text", return_value=None): + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation("Alpha", "Description Alpha") + m.assert_called_once_with( + self.window, + "Creation successful", + "Your operation was created successfully.", + ) + with (mock.patch("PyQt5.QtWidgets.QLineEdit.text", return_value=None), + mock.patch("PyQt5.QtWidgets.QErrorMessage.showMessage") as m): self._create_operation("Alpha2", "Description Alpha") - with mock.patch("PyQt5.QtWidgets.QTextEdit.toPlainText", return_value=None): + m.assert_called_once_with("Path can't be empty") + with (mock.patch("PyQt5.QtWidgets.QTextEdit.toPlainText", return_value=None), + mock.patch("PyQt5.QtWidgets.QErrorMessage.showMessage") as m): self._create_operation("Alpha3", "Description Alpha") - self._create_operation("/", "Description Alpha") - assert mockbox.return_value.showMessage.call_count == 4 + m.assert_called_once_with("Description can't be empty") + with mock.patch("PyQt5.QtWidgets.QErrorMessage.showMessage") as m: + self._create_operation("/", "Description Alpha") + m.assert_called_once_with("Path can't contain spaces or special characters") assert self.window.listOperationsMSC.model().rowCount() == 1 - self._create_operation("reproduce-test", "Description Test") + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation("reproduce-test", "Description Test") + m.assert_called_once() assert self.window.listOperationsMSC.model().rowCount() == 2 self._activate_operation_at_index(0) assert self.window.mscolab.active_operation_name == "Alpha" self._activate_operation_at_index(1) assert self.window.mscolab.active_operation_name == "reproduce-test" - @pytest.mark.skip(reason="needs review for py311") - @mock.patch("PyQt5.QtWidgets.QMessageBox.information") @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("flight7", True)) - def test_handle_delete_operation(self, mocktext, mockbox): + def test_handle_delete_operation(self, mocktext): + # pytest.skip('needs a review for the delete button pressed. Seems to delete a None operation') self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "berta@something.org"}}) self._create_user("berta", "berta@something.org", "something") assert self.window.usernameLabel.text() == 'berta' assert self.window.connectBtn.isVisible() is False assert self.window.listOperationsMSC.model().rowCount() == 0 operation_name = "flight7" - self._create_operation(operation_name, "Description flight7") + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation(operation_name, "Description flight7") + m.assert_called_once() # check for operation dir is created on server assert os.path.isdir(os.path.join(mscolab_settings.MSCOLAB_DATA_DIR, operation_name)) self._activate_operation_at_index(0) op_id = self.window.mscolab.get_recent_op_id() assert op_id is not None assert self.window.listOperationsMSC.model().rowCount() == 1 - self.window.actionDeleteOperation.trigger() - QtWidgets.QApplication.processEvents() + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self.window.actionDeleteOperation.trigger() + QtWidgets.QApplication.processEvents() + m.assert_called_once_with(self.window, "Success", 'Operation "flight7" was deleted!') op_id = self.window.mscolab.get_recent_op_id() assert op_id is None QtWidgets.QApplication.processEvents() QtTest.QTest.qWait(0) # check operation dir name removed assert os.path.isdir(os.path.join(mscolab_settings.MSCOLAB_DATA_DIR, operation_name)) is False - assert mockbox.call_count == 1 @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) def test_handle_leave_operation(self, mockmessage): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata3[0]}}) self._login(self.userdata3[0], self.userdata3[2]) QtWidgets.QApplication.processEvents() assert self.window.usernameLabel.text() == self.userdata3[1] @@ -514,56 +548,68 @@ def test_handle_leave_operation(self, mockmessage): assert self.window.listViews.count() == 0 assert self.window.listOperationsMSC.model().rowCount() == 0 - @mock.patch("PyQt5.QtWidgets.QMessageBox.information") @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_name", True)) - def test_handle_rename_operation(self, mockbox, mockpatch): + def test_handle_rename_operation(self, mocktext): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") - self._create_operation("flight1234", "Description flight1234") + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation("flight1234", "Description flight1234") + m.assert_called_once() assert self.window.listOperationsMSC.model().rowCount() == 1 self._activate_operation_at_index(0) assert self.window.mscolab.active_op_id is not None - self.window.actionRenameOperation.trigger() - QtWidgets.QApplication.processEvents() + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self.window.actionRenameOperation.trigger() + QtWidgets.QApplication.processEvents() + m.assert_called_once_with(self.window, "Rename successful", "Operation is renamed successfully.") QtTest.QTest.qWait(0) assert self.window.mscolab.active_op_id is not None assert self.window.mscolab.active_operation_name == "new_name" - @pytest.mark.skip(reason="needs review for py311") - @mock.patch("PyQt5.QtWidgets.QMessageBox.information") - @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_desciption", True)) - def test_update_description(self, mockbox, mockpatch): + @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_description", True)) + def test_update_description(self, mocktext): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") - self._create_operation("flight1234", "Description flight1234") + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation("flight1234", "Description flight1234") + m.assert_called_once() assert self.window.listOperationsMSC.model().rowCount() == 1 self._activate_operation_at_index(0) assert self.window.mscolab.active_op_id is not None - self.window.actionChangeDescription.trigger() - QtWidgets.QApplication.processEvents() + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self.window.actionChangeDescription.trigger() + QtWidgets.QApplication.processEvents() + m.assert_called_once_with(self.window, "Update successful", "Description is updated successfully.") QtTest.QTest.qWait(0) assert self.window.mscolab.active_op_id is not None - assert self.window.mscolab.active_operation_description == "new_desciption" + assert self.window.mscolab.active_operation_description == "new_description" - @mock.patch("PyQt5.QtWidgets.QMessageBox.information") @mock.patch("PyQt5.QtWidgets.QInputDialog.getText", return_value=("new_category", True)) - def test_update_category(self, mockbox, mockpatch): + def test_update_category(self, mocktext): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") - self._create_operation("flight1234", "Description flight1234") + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self._create_operation("flight1234", "Description flight1234") + m.assert_called_once() assert self.window.listOperationsMSC.model().rowCount() == 1 assert self.window.mscolab.active_operation_category == "example" self._activate_operation_at_index(0) assert self.window.mscolab.active_op_id is not None - self.window.actionChangeCategory.trigger() - QtWidgets.QApplication.processEvents() + with mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) as m: + self.window.actionChangeCategory.trigger() + QtWidgets.QApplication.processEvents() + m.assert_called_once_with(self.window, "Update successful", "Category is updated successfully.") QtTest.QTest.qWait(0) assert self.window.mscolab.active_op_id is not None assert self.window.mscolab.active_operation_category == "new_category" @mock.patch("PyQt5.QtWidgets.QMessageBox.information") - def test_any_special_category(self, mockpatch): + def test_any_special_category(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") self._create_operation("flight1234", "Description flight1234") QtTest.QTest.qWait(0) @@ -581,8 +627,10 @@ def test_any_special_category(self, mockpatch): range(self.window.mscolab.ui.listOperationsMSC.count())] assert ["flight5678"] == operation_pathes - def test_get_recent_op_id(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) + def test_get_recent_op_id(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "anton@something.org"}}) self._create_user("anton", "anton@something.org", "something") QtTest.QTest.qWait(100) assert self.window.usernameLabel.text() == 'anton' @@ -595,8 +643,10 @@ def test_get_recent_op_id(self): # ToDo fix number after cleanup initial data assert self.window.mscolab.get_recent_op_id() == current_op_id + 2 - def test_get_recent_operation(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) + def test_get_recent_operation(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "berta@something.org"}}) self._create_user("berta", "berta@something.org", "something") QtTest.QTest.qWait(100) assert self.window.usernameLabel.text() == 'berta' @@ -608,8 +658,10 @@ def test_get_recent_operation(self): assert operation["path"] == "flight1234" assert operation["access_level"] == "creator" - def test_open_chat_window(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) + def test_open_chat_window(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") self._create_operation("flight1234", "Description flight1234") assert self.window.listOperationsMSC.model().rowCount() == 1 @@ -620,9 +672,10 @@ def test_open_chat_window(self): QtTest.QTest.qWait(0) assert self.window.mscolab.chat_window is not None - @pytest.mark.skip(reason="needs review for py311") - def test_close_chat_window(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) + def test_close_chat_window(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") self._create_operation("flight1234", "Description flight1234") assert self.window.listOperationsMSC.model().rowCount() == 1 @@ -633,9 +686,10 @@ def test_close_chat_window(self): self.window.mscolab.close_chat_window() assert self.window.mscolab.chat_window is None - @pytest.mark.skip("py.311, needs review") - def test_delete_operation_from_list(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Ok) + def test_delete_operation_from_list(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "other@something.org"}}) self._create_user("other", "other@something.org", "something") assert self.window.usernameLabel.text() == 'other' assert self.window.connectBtn.isVisible() is False @@ -650,6 +704,7 @@ def test_delete_operation_from_list(self): @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) def test_user_delete(self, mockmessage): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") u_id = self.window.mscolab.user['id'] self.window.mscolab.open_profile_window() @@ -696,6 +751,7 @@ def test_create_dir_exceptions(self, mockexit, mockbox): @mock.patch("PyQt5.QtWidgets.QMessageBox") def test_profile_dialog(self, mockbox): self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: "something@something.org"}}) self._create_user("something", "something@something.org", "something") self.window.mscolab.profile_action.trigger() QtWidgets.QApplication.processEvents() @@ -743,8 +799,7 @@ def _reset_config_file(self): config_file = fs.path.combine(MSUI_CONFIG_PATH, "msui_settings.json") read_config_file(path=config_file) - @mock.patch("mslib.msui.mscolab.QtWidgets.QErrorMessage.showMessage") - def _create_operation(self, path, description, mockbox, category="example"): + def _create_operation(self, path, description, category="example"): self.window.actionAddOperation.trigger() QtWidgets.QApplication.processEvents() self.window.mscolab.add_proj_dialog.path.setText(str(path)) diff --git a/tests/_test_msui/test_mscolab_admin_window.py b/tests/_test_msui/test_mscolab_admin_window.py index 4427d3083..348c17814 100644 --- a/tests/_test_msui/test_mscolab_admin_window.py +++ b/tests/_test_msui/test_mscolab_admin_window.py @@ -25,6 +25,7 @@ limitations under the License. """ import os +import mock import pytest import sys @@ -35,6 +36,7 @@ from mslib.msui import msui from mslib.mscolab.mscolab import handle_db_reset from mslib.mscolab.seed import add_user, get_user, add_operation, add_user_to_operation +from mslib.utils.config import modify_config_file PORTS = list(range(24000, 24500)) @@ -68,9 +70,11 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.window.create_new_flight_track() self.window.show() # connect and login to mscolab self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(emailid=self.userdata[0], password=self.userdata[2]) # activate operation and open chat window self._activate_operation_at_index(0) @@ -86,6 +90,9 @@ def teardown_method(self): self.window.mscolab.admin_window.close() if self.window.mscolab.conn: self.window.mscolab.conn.disconnect() + with mock.patch("PyQt5.QtWidgets.QMessageBox.warning", return_value=QtWidgets.QMessageBox.Yes): + self.window.close() + QtWidgets.QApplication.processEvents() self.application.quit() QtWidgets.QApplication.processEvents() self.process.terminate() diff --git a/tests/_test_msui/test_mscolab_merge_waypoints.py b/tests/_test_msui/test_mscolab_merge_waypoints.py index 6ac974510..e8afee9fc 100644 --- a/tests/_test_msui/test_mscolab_merge_waypoints.py +++ b/tests/_test_msui/test_mscolab_merge_waypoints.py @@ -53,6 +53,7 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.window.create_new_flight_track() self.emailid = 'merge@alpha.org' def teardown_method(self): diff --git a/tests/_test_msui/test_mscolab_operation.py b/tests/_test_msui/test_mscolab_operation.py index 5ca3a48dd..ec2b769a9 100644 --- a/tests/_test_msui/test_mscolab_operation.py +++ b/tests/_test_msui/test_mscolab_operation.py @@ -36,6 +36,7 @@ from mslib.msui import msui from mslib.mscolab.mscolab import handle_db_reset from mslib.mscolab.seed import add_user, get_user, add_operation, add_user_to_operation +from mslib.utils.config import modify_config_file PORTS = list(range(22000, 22500)) @@ -63,9 +64,11 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.window.create_new_flight_track() self.window.show() # connect and login to mscolab self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(self.userdata[0], self.userdata[2]) # activate operation and open chat window self._activate_operation_at_index(0) diff --git a/tests/_test_msui/test_mscolab_version_history.py b/tests/_test_msui/test_mscolab_version_history.py index 01e48739f..7d4a33b4c 100644 --- a/tests/_test_msui/test_mscolab_version_history.py +++ b/tests/_test_msui/test_mscolab_version_history.py @@ -36,6 +36,7 @@ from mslib.msui import msui from mslib.mscolab.mscolab import handle_db_reset from mslib.mscolab.seed import add_user, get_user, add_operation, add_user_to_operation +from mslib.utils.config import modify_config_file PORTS = list(range(20000, 20500)) @@ -56,9 +57,11 @@ def setup_method(self): QtTest.QTest.qWait(500) self.application = QtWidgets.QApplication(sys.argv) self.window = msui.MSUIMainWindow(mscolab_data_dir=mscolab_settings.MSCOLAB_DATA_DIR) + self.window.create_new_flight_track() self.window.show() # connect and login to mscolab self._connect_to_mscolab() + modify_config_file({"MSS_auth": {self.url: self.userdata[0]}}) self._login(self.userdata[0], self.userdata[2]) # activate operation and open chat window self._activate_operation_at_index(0) @@ -113,7 +116,7 @@ def test_version_name_delete(self, mockbox): assert self.version_window.changes.currentItem().version_name is None @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) - def test_undo(self, mockbox): + def test_undo_changes(self, mockbox): self._change_version_filter(1) # make changes for i in range(2): diff --git a/tests/_test_msui/test_msui.py b/tests/_test_msui/test_msui.py index c423bbb20..d31556bce 100644 --- a/tests/_test_msui/test_msui.py +++ b/tests/_test_msui/test_msui.py @@ -29,6 +29,7 @@ import sys import mock import os +import fs import platform import argparse import pytest @@ -139,7 +140,7 @@ class Test_MSSSideViewWindow(object): save_txt = os.path.join(ROOT_DIR, "example.txt") # import/export plugins import_plugins = { - "Text": ["txt", "mslib.plugins.io.text", "load_from_txt"], + "TXT": ["txt", "mslib.plugins.io.text", "load_from_txt"], "FliteStar": ["txt", "mslib.plugins.io.flitestar", "load_from_flitestar"], } export_plugins = { @@ -258,24 +259,24 @@ def test_plugin_saveas(self, save_file): assert os.path.exists(save_file[0]) os.remove(save_file[0]) - @pytest.mark.parametrize( - "open_file", [(open_ftml, "actionImportFlightTrackFTML"), - (open_txt, "actionImportFlightTrackText"), (open_fls, "actionImportFlightTrackFliteStar")]) - def test_plugin_import(self, open_file): + @pytest.mark.parametrize("name", [("example.ftml", "actionImportFlightTrackFTML", 5), + ("example.csv", "actionImportFlightTrackCSV", 5), + ("example.txt", "actionImportFlightTrackTXT", 5), + ("flitestar.txt", "actionImportFlightTrackFliteStar", 10)]) + def test_plugin_import(self, name): with mock.patch("mslib.msui.msui_mainwindow.config_loader", return_value=self.import_plugins): self.window.add_import_plugins("qt") - with mock.patch("mslib.msui.msui_mainwindow.get_open_filenames", return_value=open_file) as mockopen: - assert self.window.listFlightTracks.count() == 1 - assert mockopen.call_count == 0 - self.window.last_save_directory = ROOT_DIR - obj_name = open_file[1] + assert self.window.listFlightTracks.count() == 1 + file_path = fs.path.join(self.sample_path, name[0]) + with mock.patch("mslib.msui.msui_mainwindow.get_open_filenames", return_value=[file_path]) as mockopen: for action in self.window.menuImportFlightTrack.actions(): - if obj_name == action.objectName(): + if action.objectName() == name[1]: action.trigger() break - QtWidgets.QApplication.processEvents() assert mockopen.call_count == 1 assert self.window.listFlightTracks.count() == 2 + assert self.window.active_flight_track.name == name[0].split(".")[0] + assert len(self.window.active_flight_track.waypoints) == name[2] @pytest.mark.parametrize("save_file", [[save_ftml, "actionExportFlightTrackFTML"], [save_txt, "actionExportFlightTrackText"]]) diff --git a/tests/_test_msui/test_multiple_flightpath_dockwidget.py b/tests/_test_msui/test_multiple_flightpath_dockwidget.py index aac22bb3f..9d824ced1 100644 --- a/tests/_test_msui/test_multiple_flightpath_dockwidget.py +++ b/tests/_test_msui/test_multiple_flightpath_dockwidget.py @@ -46,7 +46,8 @@ def setup_method(self): self.waypoints_model = ft.WaypointsTableModel("myname") self.waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.widget = tv.MSUITopViewWindow(parent=self.window, model=self.waypoints_model) + + self.widget = tv.MSUITopViewWindow(model=self.waypoints_model, mainwindow=self.window) self.window.show() QtWidgets.QApplication.processEvents() QtTest.QTest.qWaitForWindowExposed(self.window) @@ -59,5 +60,6 @@ def teardown_method(self): QtWidgets.QApplication.processEvents() def test_initialization(self): - widget = MultipleFlightpathControlWidget(parent=self.widget, listFlightTracks=self.widget.ui.listFlightTracks) + widget = MultipleFlightpathControlWidget(parent=self.widget, + listFlightTracks=self.window.listFlightTracks) assert widget.color == (0, 0, 1, 1) diff --git a/tests/_test_msui/test_satellite_dockwidget.py b/tests/_test_msui/test_satellite_dockwidget.py index e558fcd1c..8ebbf7c84 100644 --- a/tests/_test_msui/test_satellite_dockwidget.py +++ b/tests/_test_msui/test_satellite_dockwidget.py @@ -61,7 +61,13 @@ def test_load(self): assert self.view.plot_satellite_overpass.call_count == 2 self.view.reset_mock() - def test_load_no_file(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox.critical") + def test_load_no_file(self, mockbox): QtTest.QTest.mouseClick(self.window.btLoadFile, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert self.window.cbSatelliteOverpasses.count() == 0 + mockbox.assert_called_once_with( + self.window, + "Satellite Overpass Tool", + "ERROR:\n\npath '' should be a file", + ) diff --git a/tests/_test_msui/test_topview.py b/tests/_test_msui/test_topview.py index 2f1c3d84d..e0539e841 100644 --- a/tests/_test_msui/test_topview.py +++ b/tests/_test_msui/test_topview.py @@ -32,10 +32,11 @@ import sys import multiprocessing import tempfile +import mslib.msui.topview as tv from mslib.mswms.mswms import application from PyQt5 import QtWidgets, QtCore, QtTest from mslib.msui import flighttrack as ft -import mslib.msui.topview as tv +from mslib.msui.msui import MSUIMainWindow from mslib.msui.mpl_qtwidget import _DEFAULT_SETTINGS_TOPVIEW from tests.utils import wait_until_signal @@ -70,12 +71,13 @@ def test_get(self, mockcrit): class Test_MSSTopViewWindow(object): def setup_method(self): + mainwindow = MSUIMainWindow() self.application = QtWidgets.QApplication(sys.argv) initial_waypoints = [ft.Waypoint(40., 25., 0), ft.Waypoint(60., -10., 0), ft.Waypoint(40., 10, 0)] waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUITopViewWindow(model=waypoints_model) + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) self.window.show() QtWidgets.QApplication.processEvents() QtTest.QTest.qWaitForWindowExposed(self.window) @@ -312,7 +314,8 @@ def setup_method(self): waypoints_model.insertRows( 0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUITopViewWindow(model=waypoints_model) + mainwindow = MSUIMainWindow() + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) self.window.show() QtWidgets.QApplication.processEvents() QtTest.QTest.qWait(2000) @@ -364,7 +367,8 @@ def test_kwargs_update_does_not_harm(self): initial_waypoints = [ft.Waypoint(40., 25., 0), ft.Waypoint(60., -10., 0), ft.Waypoint(40., 10, 0)] waypoints_model = ft.WaypointsTableModel("") waypoints_model.insertRows(0, rows=len(initial_waypoints), waypoints=initial_waypoints) - self.window = tv.MSUITopViewWindow(model=waypoints_model) + mainwindow = MSUIMainWindow() + self.window = tv.MSUITopViewWindow(model=waypoints_model, mainwindow=mainwindow) # user_options is a global var from mslib.utils.config import user_options diff --git a/tests/_test_utils/test_airdata.py b/tests/_test_utils/test_airdata.py index 6da4a94fc..837ee250b 100644 --- a/tests/_test_utils/test_airdata.py +++ b/tests/_test_utils/test_airdata.py @@ -206,10 +206,12 @@ def test_get_airspaces(mockbox): @mock.patch("mslib.utils.airdata.download_progress", _download_incomplete_airspace) +@mock.patch("PyQt5.QtWidgets.QMessageBox.information") @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) -def test_get_airspaces_missing_data(mockbox): +def test_get_airspaces_missing_data(mockbox, infobox): """ We use a test file without the need for downloading to check handling """ # update_airspace would only update after 30 days _cleanup_test_files() airspaces = get_airspaces(countries=["bg"]) assert airspaces == [] + infobox.assert_called_once_with(None, 'No Airspaces data in file:', 'bg_asp.xml') diff --git a/tests/_test_utils/test_coordinate.py b/tests/_test_utils/test_coordinate.py index 5408ce48c..e38582aeb 100644 --- a/tests/_test_utils/test_coordinate.py +++ b/tests/_test_utils/test_coordinate.py @@ -60,7 +60,7 @@ def test_get_projection_params(self): with pytest.raises(ValueError): coordinate.get_projection_params('auto:42001') with pytest.raises(ValueError): - coordinate.get_projection_params('crs:84') + coordinate.get_projection_params('crs:83') class TestAngles(object): diff --git a/tests/utils.py b/tests/utils.py index 895dca650..d2a07be3c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -41,6 +41,7 @@ from mslib.mscolab.conf import mscolab_settings from mslib.mscolab.server import APP, initialize_managers, start_server from mslib.mscolab.mscolab import handle_db_init +from mslib.utils.config import modify_config_file def callback_ok_image(status, response_headers): @@ -106,7 +107,7 @@ def mscolab_delete_user(app, msc_url, email, password): response = mscolab_login(app, msc_url, email, password) if response.status == '200 OK': data = json.loads(response.get_data(as_text=True)) - url = urljoin(msc_url, 'delete_user') + url = urljoin(msc_url, 'delete_own_account') response = app.test_client().post(url, data=data) if response.status == '200 OK': data = json.loads(response.get_data(as_text=True)) @@ -198,6 +199,9 @@ def mscolab_start_server(all_ports, mscolab_settings=mscolab_settings, timeout=1 url = f"http://localhost:{port}" + # Update mscolab URL to avoid "Update Server List" message boxes + modify_config_file({"default_MSCOLAB": [url]}) + _app = APP _app.config['SQLALCHEMY_DATABASE_URI'] = mscolab_settings.SQLALCHEMY_DB_URI _app.config['MSCOLAB_DATA_DIR'] = mscolab_settings.MSCOLAB_DATA_DIR