diff --git a/.github/workflows/testing-all-oses.yml b/.github/workflows/testing-all-oses.yml index 86fb996fc..58df9ccdb 100644 --- a/.github/workflows/testing-all-oses.yml +++ b/.github/workflows/testing-all-oses.yml @@ -48,6 +48,4 @@ jobs: # I have no idea yet on why this happens and how to fix it. # Even a module level skip is not enough, they need to be completely ignored. # TODO: fix those tests and drop the ignores - run: micromamba run -n ci env QT_QPA_PLATFORM=offscreen pytest -v -n logical --durations=20 --cov=mslib - --ignore=tests/_test_msui/test_sideview.py --ignore=tests/_test_msui/test_topview.py --ignore=tests/_test_msui/test_wms_control.py - tests + run: micromamba run -n ci env QT_QPA_PLATFORM=offscreen pytest -v -n logical --durations=20 --cov=mslib tests diff --git a/CHANGES.rst b/CHANGES.rst index a92d20429..b1aef8422 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,16 @@ Changelog ========= +Version 9.2.0 +~~~~~~~~~~~~~ + +Bug fix release and minor enhancements: +We added a verification for xml data and changed the startup of the SocketsManager. + +All changes: +https://github.com/Open-MSS/MSS/milestone/106?closed=1 + + Version 9.1.0 ~~~~~~~~~~~~~ diff --git a/README.md b/README.md index f819688af..75ec70d0d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +**Chat:** +[![IRC: #mss-general on libera.chat](https://img.shields.io/badge/libera.chat-%23MSS_General-blue)](https://web.libera.chat/?channels=#mss-general) +[![IRC: #mss-gsoc on libera.chat](https://img.shields.io/badge/libera.chat-%23MSS_GSoC-brightgreen)](https://web.libera.chat/?channels=#mss-gsoc) + + Mission Support System Usage Guidelines ======================================= diff --git a/conftest.py b/conftest.py index b9bf7f543..13a0a5528 100644 --- a/conftest.py +++ b/conftest.py @@ -111,9 +111,9 @@ def generate_initial_config(): root_fs.makedir('colabTestData') BASE_DIR = ROOT_DIR DATA_DIR = fs.path.join(ROOT_DIR, 'colabTestData') -# mscolab data directory -MSCOLAB_DATA_DIR = fs.path.join(DATA_DIR, 'filedata') -MSCOLAB_SSO_DIR = fs.path.join(DATA_DIR, 'datasso') +# mscolab data directory for operation git repositories +OPERATIONS_DATA = fs.path.join(DATA_DIR, 'filedata') +SSO_DIR = fs.path.join(DATA_DIR, 'datasso') # In the unit days when Operations get archived because not used ARCHIVE_THRESHOLD = 30 diff --git a/docs/_templates/footer.html b/docs/_templates/footer.html new file mode 100644 index 000000000..eb5febe74 --- /dev/null +++ b/docs/_templates/footer.html @@ -0,0 +1,21 @@ +{% extends '!footer.html' %} + +{% block extrafooter %} +
" f"Category: {self.active_operation_category}
" @@ -653,14 +703,20 @@ def after_login(self, emailid, url, r): self.user = _json["user"] self.mscolab_server_url = url + if config_loader(dataset="MSCOLAB_skip_archived_operations"): + self.ui.pbOpenOperationArchive.setEnabled(False) + self.ui.pbOpenOperationArchive.setToolTip( + "This button is disabled to the config option 'MSCOLAB_skip_archived_operations'") + else: + self.ui.pbOpenOperationArchive.setEnabled(True) + self.ui.pbOpenOperationArchive.setToolTip("") + # create socket connection here try: self.conn = sc.ConnectionManager(self.token, user=self.user, mscolab_server_url=self.mscolab_server_url) except Exception as ex: - logging.debug("Couldn't create a socket connection: %s", ex) - show_popup(self.ui, "Error", "Couldn't create a socket connection. Maybe the MSColab server is too old. " - "New Login required!") - self.logout() + raise MSColabConnectionError("Couldn't create a socket connection. Maybe the MSColab server is too old." + f"({ex}). New Login required!") else: self.conn.signal_operation_list_updated.connect(self.reload_operation_list) self.conn.signal_reload.connect(self.reload_window) @@ -668,6 +724,7 @@ def after_login(self, emailid, url, r): self.conn.signal_update_permission.connect(self.handle_update_permission) self.conn.signal_revoke_permission.connect(self.handle_revoke_permission) self.conn.signal_operation_deleted.connect(self.handle_operation_deleted) + self.conn.signal_active_user_update.connect(self.update_active_user_label) self.ui.connectBtn.hide() self.ui.openOperationsGb.show() @@ -822,7 +879,7 @@ def open_profile_window(self): def on_context_menu(point): self.gravatar_menu.exec_(self.profile_dialog.gravatarLabel.mapToGlobal(point)) - self.prof_diag = QtWidgets.QDialog() + self.prof_diag = QDialog() self.profile_dialog = ui_profile.Ui_ProfileWindow() self.profile_dialog.setupUi(self.prof_diag) self.profile_dialog.buttonBox.accepted.connect(lambda: self.prof_diag.close()) @@ -852,7 +909,7 @@ def upload_image(self): try: # Resize the image and set profile image pixmap image = Image.open(file_name) - image = image.resize((64, 64), Image.ANTIALIAS) + image = image.resize((64, 64), Image.LANCZOS) img_byte_arr = io.BytesIO() image.save(img_byte_arr, format=file_format) img_byte_arr.seek(0) @@ -886,88 +943,80 @@ def upload_image(self): QMessageBox.critical(self.prof_diag, "Error", f'Cannot identify image file. Please check the file format. Error: {e}') - def delete_account(self): + @verify_user_token + def delete_account(self, _=None): # ToDo rename to delete_own_account - if verify_user_token(self.mscolab_server_url, self.token): - w = QtWidgets.QWidget() - qm = QtWidgets.QMessageBox - reply = qm.question(w, self.tr('Continue?'), - self.tr("You're about to delete your account. You cannot undo this operation!"), - qm.Yes, qm.No) - if reply == QtWidgets.QMessageBox.No: - return - data = { - "token": self.token - } + reply = QMessageBox.question( + self.ui, self.tr('Continue?'), + self.tr("You're about to delete your account. You cannot undo this operation!"), + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.No: + return + data = { + "token": self.token + } - try: - url = urljoin(self.mscolab_server_url, "delete_own_account") - 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.status_code == 200 and json.loads(r.text)["success"] is True: - self.logout() + try: + url = urljoin(self.mscolab_server_url, "delete_own_account") + r = requests.post(url, data=data, + timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") else: - show_popup(self, "Error", "Your Connection is expired. New Login required!") - self.logout() + if r.status_code == 200 and json.loads(r.text)["success"] is True: + self.logout() - def add_operation_handler(self): - if verify_user_token(self.mscolab_server_url, self.token): - def check_and_enable_operation_accept(): - if (self.add_proj_dialog.path.text() != "" and - self.add_proj_dialog.description.toPlainText() != "" and - self.add_proj_dialog.category.text() != ""): - self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True) + @verify_user_token + def add_operation_handler(self, _=None): + def check_and_enable_operation_accept(): + if (self.add_proj_dialog.path.text() != "" and + self.add_proj_dialog.description.toPlainText() != "" and + self.add_proj_dialog.category.text() != ""): + self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True) + else: + self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) + + def browse(): + import_type = self.add_proj_dialog.cb_ImportType.currentText() + file_type = ["Flight track (*.ftml)"] + if import_type != 'FTML': + file_type = [f"Flight track (*.{self.ui.import_plugins[import_type][1]})"] + + file_path = get_open_filename( + self.ui, "Open Flighttrack file", "", ';;'.join(file_type)) + if file_path is not None: + file_name = fs.path.basename(file_path) + if file_path.endswith('ftml'): + with open_fs(fs.path.dirname(file_path)) as file_dir: + file_content = file_dir.readtext(file_name) else: - self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) - - def browse(): - import_type = self.add_proj_dialog.cb_ImportType.currentText() - file_type = ["Flight track (*.ftml)"] - if import_type != 'FTML': - file_type = [f"Flight track (*.{self.ui.import_plugins[import_type][1]})"] - - file_path = get_open_filename( - self.ui, "Open Flighttrack file", "", ';;'.join(file_type)) - if file_path is not None: - file_name = fs.path.basename(file_path) - if file_path.endswith('ftml'): - with open_fs(fs.path.dirname(file_path)) as file_dir: - file_content = file_dir.readtext(file_name) - else: - function = self.ui.import_plugins[import_type][0] - ft_name, waypoints = function(file_path) - model = ft.WaypointsTableModel(waypoints=waypoints) - xml_doc = model.get_xml_doc() - file_content = xml_doc.toprettyxml(indent=" ", newl="\n") - self.add_proj_dialog.f_content = file_content - self.add_proj_dialog.selectedFile.setText(file_name) - - self.proj_diag = QtWidgets.QDialog() - self.add_proj_dialog = add_operation_ui.Ui_addOperationDialog() - self.add_proj_dialog.setupUi(self.proj_diag) - self.add_proj_dialog.f_content = None - self.add_proj_dialog.buttonBox.accepted.connect(self.add_operation) - self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) - self.add_proj_dialog.path.textChanged.connect(check_and_enable_operation_accept) - self.add_proj_dialog.description.textChanged.connect(check_and_enable_operation_accept) - self.add_proj_dialog.category.textChanged.connect(check_and_enable_operation_accept) - self.add_proj_dialog.browse.clicked.connect(browse) - self.add_proj_dialog.category.setText(config_loader(dataset="MSCOLAB_category")) - - # sets types from defined import menu - import_menu = self.ui.menuImportFlightTrack - for im_action in import_menu.actions(): - self.add_proj_dialog.cb_ImportType.addItem(im_action.text()) - self.proj_diag.show() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() - + function = self.ui.import_plugins[import_type][0] + ft_name, waypoints = function(file_path) + model = ft.WaypointsTableModel(waypoints=waypoints) + xml_doc = model.get_xml_doc() + file_content = xml_doc.toprettyxml(indent=" ", newl="\n") + self.add_proj_dialog.f_content = file_content + self.add_proj_dialog.selectedFile.setText(file_name) + + self.proj_diag = QDialog() + self.add_proj_dialog = add_operation_ui.Ui_addOperationDialog() + self.add_proj_dialog.setupUi(self.proj_diag) + self.add_proj_dialog.f_content = None + self.add_proj_dialog.buttonBox.accepted.connect(self.add_operation) + self.add_proj_dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) + self.add_proj_dialog.path.textChanged.connect(check_and_enable_operation_accept) + self.add_proj_dialog.description.textChanged.connect(check_and_enable_operation_accept) + self.add_proj_dialog.category.textChanged.connect(check_and_enable_operation_accept) + self.add_proj_dialog.browse.clicked.connect(browse) + self.add_proj_dialog.category.setText(config_loader(dataset="MSCOLAB_category")) + + # sets types from defined import menu + import_menu = self.ui.menuImportFlightTrack + for im_action in import_menu.actions(): + self.add_proj_dialog.cb_ImportType.addItem(im_action.text()) + self.proj_diag.show() + + @verify_user_token def add_operation(self): logging.debug("add_operation") path = self.add_proj_dialog.path.text() @@ -1004,52 +1053,43 @@ def add_operation(self): url = urljoin(self.mscolab_server_url, "create_operation") 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() + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") + if r.text == "True": + 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) + self.signal_operation_added.emit(op_id, path) else: - if r.text == "True": - 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) - self.signal_operation_added.emit(op_id, path) - else: - self.error_dialog = QtWidgets.QErrorMessage() - self.error_dialog.showMessage('The path already exists') + self.error_dialog = QtWidgets.QErrorMessage() + self.error_dialog.showMessage('The path already exists') + @verify_user_token def get_recent_op_id(self): + """ + get most recent operation's op_id + """ logging.debug('get_recent_op_id') - if verify_user_token(self.mscolab_server_url, self.token): - """ - get most recent operation's op_id - """ - skip_archived = config_loader(dataset="MSCOLAB_skip_archived_operations") - data = { - "token": self.token, - "skip_archived": skip_archived - } - url = urljoin(self.mscolab_server_url, "operations") - r = requests.get(url, data=data) - if r.text != "False": - _json = json.loads(r.text) - operations = _json["operations"] - op_id = None - if operations: - op_id = operations[-1]["op_id"] - logging.debug("recent op_id %s", op_id) - 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() + skip_archived = config_loader(dataset="MSCOLAB_skip_archived_operations") + data = { + "token": self.token, + "skip_archived": skip_archived + } + url = urljoin(self.mscolab_server_url, "operations") + r = requests.get(url, data=data) + if r.text == "False": + raise MSColabConnectionError("Session expired, new login required") + _json = json.loads(r.text) + operations = _json["operations"] + op_id = None + if operations: + op_id = operations[-1]["op_id"] + logging.debug("recent op_id %s", op_id) + return op_id def operation_options_handler(self): if self.sender() == self.ui.actionChat: @@ -1063,86 +1103,77 @@ def operation_options_handler(self): elif self.sender() == self.ui.actionLeaveOperation: self.handle_leave_operation() + @verify_user_token def open_chat_window(self): - if verify_user_token(self.mscolab_server_url, self.token): - if self.active_op_id is None: - return + if self.active_op_id is None: + return - if self.chat_window is not None: - self.chat_window.activateWindow() - return + if self.chat_window is not None: + self.chat_window.activateWindow() + return - self.chat_window = mc.MSColabChatWindow( - self.token, - self.active_op_id, - self.user, - self.active_operation_name, - self.access_level, - self.conn, - mscolab_server_url=self.mscolab_server_url, - ) - self.chat_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.chat_window.viewCloses.connect(self.close_chat_window) - self.chat_window.reloadWindows.connect(self.reload_windows_slot) - self.chat_window.show() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.chat_window = mc.MSColabChatWindow( + self.token, + self.active_op_id, + self.user, + self.active_operation_name, + self.access_level, + self.conn, + mscolab_server_url=self.mscolab_server_url, + ) + self.chat_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.chat_window.viewCloses.connect(self.close_chat_window) + self.chat_window.reloadWindows.connect(self.reload_windows_slot) + self.chat_window.show() def close_chat_window(self): self.chat_window.close() self.chat_window = None + @verify_user_token def open_admin_window(self): - if verify_user_token(self.mscolab_server_url, self.token): - if self.active_op_id is None: - return + if self.active_op_id is None: + return - if self.admin_window is not None: - self.admin_window.activateWindow() - return + if self.admin_window is not None: + self.admin_window.activateWindow() + return - operations = [operation for operation in self.operations if operation["active"] is True] + operations = [operation for operation in self.operations if operation["active"] is True] - self.admin_window = maw.MSColabAdminWindow( - self.token, - self.active_op_id, - self.user, - self.active_operation_name, - operations, - self.conn, - mscolab_server_url=self.mscolab_server_url, - ) - self.admin_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.admin_window.viewCloses.connect(self.close_admin_window) - self.admin_window.show() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.admin_window = maw.MSColabAdminWindow( + self.token, + self.active_op_id, + self.user, + self.active_operation_name, + operations, + self.conn, + mscolab_server_url=self.mscolab_server_url, + ) + self.admin_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.admin_window.viewCloses.connect(self.close_admin_window) + self.admin_window.show() def close_admin_window(self): self.admin_window.close() self.admin_window = None + @verify_user_token def open_version_history_window(self): - if verify_user_token(self.mscolab_server_url, self.token): - if self.active_op_id is None: - return + if self.active_op_id is None: + return - if self.version_window is not None: - self.version_window.activateWindow() - return + if self.version_window is not None: + self.version_window.activateWindow() + return - self.version_window = mvh.MSColabVersionHistory(self.token, self.active_op_id, self.user, - self.active_operation_name, self.conn, - mscolab_server_url=self.mscolab_server_url) - self.version_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.version_window.viewCloses.connect(self.close_version_history_window) - self.version_window.reloadWindows.connect(self.reload_windows_slot) - self.version_window.show() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.version_window = mvh.MSColabVersionHistory(self.token, self.active_op_id, self.user, + self.active_operation_name, self.conn, + mscolab_server_url=self.mscolab_server_url) + self.version_window.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.version_window.viewCloses.connect(self.close_version_history_window) + self.version_window.reloadWindows.connect(self.reload_windows_slot) + self.version_window.show() def close_version_history_window(self): self.version_window.close() @@ -1156,6 +1187,7 @@ def update_views(self): initial_waypoints = [ft.Waypoint(location=locations[0]), ft.Waypoint(location=locations[1])] waypoints_model = ft.WaypointsTableModel(name="", waypoints=initial_waypoints) self.waypoints_model = waypoints_model + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) self.reload_view_windows() def close_external_windows(self): @@ -1172,78 +1204,62 @@ def close_external_windows(self): self.version_window.close() self.version_window = None + @verify_user_token def handle_delete_operation(self): logging.debug("handle_delete_operation") - if verify_user_token(self.mscolab_server_url, self.token): - entered_operation_name, ok = QtWidgets.QInputDialog.getText( - self.ui, - self.ui.tr("Delete Operation"), - self.ui.tr( - f"You're about to delete the operation - '{self.active_operation_name}'. " - f"Enter the operation name to confirm: " - ), - ) - if ok: - if entered_operation_name == self.active_operation_name: - data = { - "token": self.token, - "op_id": self.active_op_id - } - url = urljoin(self.mscolab_server_url, 'delete_operation') - try: - res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - except requests.exceptions.RequestException as e: - logging.debug(e) - show_popup(self.ui, "Error", "Some error occurred! Please reconnect.") - self.logout() - else: - res.raise_for_status() - self.reload_operations() - self.signal_operation_removed.emit(self.active_op_id) - logging.debug("activate local") - self.ui.listFlightTracks.setCurrentRow(0) - self.ui.activate_selected_flight_track() - else: - show_popup(self.ui, "Error", "Entered operation name did not match!") - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() - - def handle_leave_operation(self): - logging.debug("handle_leave_operation") - w = QtWidgets.QWidget() - qm = QtWidgets.QMessageBox - reply = qm.question(w, self.tr('Mission Support System'), - self.tr("Do you want to leave this operation?"), - qm.Yes, qm.No) - if reply == QtWidgets.QMessageBox.Yes: - if verify_user_token(self.mscolab_server_url, self.token): + entered_operation_name, ok = QtWidgets.QInputDialog.getText( + self.ui, + self.ui.tr("Delete Operation"), + self.ui.tr( + f"You're about to delete the operation - '{self.active_operation_name}'. " + f"Enter the operation name to confirm: " + ), + ) + if ok: + if entered_operation_name == self.active_operation_name: data = { "token": self.token, - "op_id": self.active_op_id, - "selected_userids": json.dumps([self.user["id"]]) + "op_id": self.active_op_id } - url = urljoin(self.mscolab_server_url, "delete_bulk_permissions") + url = urljoin(self.mscolab_server_url, 'delete_operation') 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"]: - for window in self.ui.get_active_views(): - window.handle_force_close() - self.reload_operations() - else: - show_popup(self.ui, "Error", "Some error occurred! Could not leave operation.") + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + res.raise_for_status() + self.reload_operations() + self.signal_operation_removed.emit(self.active_op_id) else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + show_popup(self.ui, "Error", "Entered operation name did not match!") + + @verify_user_token + def handle_leave_operation(self): + logging.debug("handle_leave_operation") + reply = QMessageBox.question( + self.ui, self.tr('Mission Support System'), + self.tr("Do you want to leave this operation?"), + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if reply == QMessageBox.Yes: + data = { + "token": self.token, + "op_id": self.active_op_id, + "selected_userids": json.dumps([self.user["id"]]) + } + url = urljoin(self.mscolab_server_url, "delete_bulk_permissions") + try: + res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") + if res.text == "False": + raise MSColabConnectionError("Your Connection is expired. New Login required!") + res = res.json() + if res["success"]: + for window in self.ui.get_active_views(): + window.handle_force_close() + self.reload_operations() + else: + show_popup(self.ui, "Error", "Some error occurred! Could not leave operation.") def set_operation_desc_label(self, op_desc): self.active_operation_description = op_desc @@ -1256,181 +1272,156 @@ def set_operation_desc_label(self, op_desc): "Description is too long to show here, for long descriptions go " "to operations menu.") - def change_category_handler(self): + @verify_user_token + def change_category_handler(self, _=None): + logging.debug('change_category_handler') # only after login - if verify_user_token(self.mscolab_server_url, self.token): - entered_operation_category, ok = QtWidgets.QInputDialog.getText( + entered_operation_category, ok = QtWidgets.QInputDialog.getText( + self.ui, + self.ui.tr(f"{self.active_operation_name} - Change Category"), + self.ui.tr( + "You're about to change the operation category\n" + "Enter new operation category: " + ), + text=self.active_operation_category + ) + if ok: + data = { + "token": self.token, + "op_id": self.active_op_id, + "attribute": 'category', + "value": entered_operation_category + } + url = urljoin(self.mscolab_server_url, 'update_operation') + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") + if r.text == "False": + raise MSColabConnectionError("Your Connection is expired. New Login required!") + self.active_operation_category = entered_operation_category + self.reload_operation_list() + QMessageBox.information( self.ui, - self.ui.tr(f"{self.active_operation_name} - Change Category"), - self.ui.tr( - "You're about to change the operation category\n" - "Enter new operation category: " - ), - text=self.active_operation_category + "Update successful", + "Category is updated successfully.", ) - if ok: - data = { - "token": self.token, - "op_id": self.active_op_id, - "attribute": 'category', - "value": entered_operation_category - } - url = urljoin(self.mscolab_server_url, 'update_operation') - 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() - def change_description_handler(self): + @verify_user_token + def change_description_handler(self, _=None): + logging.debug('change_description_handler') # only after login - if verify_user_token(self.mscolab_server_url, self.token): - entered_operation_desc, ok = QtWidgets.QInputDialog.getText( + entered_operation_desc, ok = QtWidgets.QInputDialog.getText( + self.ui, + self.ui.tr(f"{self.active_operation_name} - Change Description"), + self.ui.tr( + "You're about to change the operation description\n" + "Enter new operation description: " + ), + text=self.active_operation_description + ) + if ok: + data = { + "token": self.token, + "op_id": self.active_op_id, + "attribute": 'description', + "value": entered_operation_desc + } + url = urljoin(self.mscolab_server_url, 'update_operation') + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") + if r.text == "False": + raise MSColabConnectionError("Your Connection is expired. New Login required!") + # Update active operation description label + self.set_operation_desc_label(entered_operation_desc) + + self.reload_operation_list() + QMessageBox.information( self.ui, - self.ui.tr(f"{self.active_operation_name} - Change Description"), - self.ui.tr( - "You're about to change the operation description\n" - "Enter new operation description: " - ), - text=self.active_operation_description + "Update successful", + "Description is updated successfully.", ) - if ok: - data = { - "token": self.token, - "op_id": self.active_op_id, - "attribute": 'description', - "value": entered_operation_desc - } - - url = urljoin(self.mscolab_server_url, 'update_operation') - 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() - def rename_operation_handler(self): + @verify_user_token + def rename_operation_handler(self, _=None): + logging.debug('rename_operation_handler') # only after login - if verify_user_token(self.mscolab_server_url, self.token): - entered_operation_name, ok = QtWidgets.QInputDialog.getText( + entered_operation_name, ok = QtWidgets.QInputDialog.getText( + self.ui, + self.ui.tr("Rename Operation"), + self.ui.tr( + f"You're about to rename the operation - '{self.active_operation_name}' " + f"Enter new operation name: " + ), + text=f"{self.active_operation_name}", + ) + if ok: + data = { + "token": self.token, + "op_id": self.active_op_id, + "attribute": 'path', + "value": entered_operation_name + } + url = urljoin(self.mscolab_server_url, 'update_operation') + try: + r = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Please reconnect.") + if r.text == "False": + raise MSColabConnectionError("Your Connection is expired. New Login required!") + # 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) + + QMessageBox.information( self.ui, - self.ui.tr("Rename Operation"), - self.ui.tr( - f"You're about to rename the operation - '{self.active_operation_name}' " - f"Enter new operation name: " - ), - text=f"{self.active_operation_name}", + "Rename successful", + "Operation is renamed successfully.", ) - if ok: - data = { - "token": self.token, - "op_id": self.active_op_id, - "attribute": 'path', - "value": entered_operation_name - } - url = urljoin(self.mscolab_server_url, 'update_operation') - 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() - def handle_work_locally_toggle(self): - if verify_user_token(self.mscolab_server_url, self.token): - if self.ui.workLocallyCheckbox.isChecked(): - if self.version_window is not None: - self.version_window.close() - self.create_local_operation_file() - self.local_ftml_file = fs.path.combine( - self.data_dir, - fs.path.join( - "local_mscolab_data", self.user["username"], - self.active_operation_name, "mscolab_operation.ftml"), - ) - self.ui.workingStatusLabel.setText( - self.ui.tr( - "Working Asynchronously.\nYour changes are only available to you. " - "Use the 'Server Options' drop-down menu below to Save to or Fetch from the server.") - ) - self.ui.serverOptionsCb.show() - self.reload_local_wp() - else: - self.local_ftml_file = None - self.ui.workingStatusLabel.setText( - self.ui.tr( - "Working Online.\nAll your changes will be shared with everyone. " - "You can work on the operation asynchronously by checking the 'Work Asynchronously' box.") - ) - self.ui.serverOptionsCb.hide() - self.waypoints_model = None - self.load_wps_from_server() - self.show_operation_options() - self.reload_view_windows() + @verify_user_token + def handle_work_locally_toggle(self, _=None): + if self.ui.workLocallyCheckbox.isChecked(): + if self.version_window is not None: + self.version_window.close() + self.create_local_operation_file() + self.local_ftml_file = fs.path.combine( + self.data_dir, + fs.path.join( + "local_colabdata", self.user["username"], + self.active_operation_name, "mscolab_operation.ftml"), + ) + self.ui.workingStatusLabel.setText( + self.ui.tr( + "Working Asynchronously.\nYour changes are only available to you. " + "Use the 'Server Options' drop-down menu below to Save to or Fetch from the server.") + ) + self.ui.serverOptionsCb.show() + self.reload_local_wp() else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.local_ftml_file = None + self.ui.workingStatusLabel.setText( + self.ui.tr( + "Working Online.\nAll your changes will be shared with everyone. " + "You can work on the operation asynchronously by checking the 'Work Asynchronously' box.") + ) + self.ui.serverOptionsCb.hide() + self.waypoints_model = None + self.load_wps_from_server() + self.show_operation_options() + self.reload_view_windows() def create_local_operation_file(self): with open_fs(self.data_dir) as mss_dir: - rel_file_path = fs.path.join('local_mscolab_data', self.user['username'], + rel_file_path = fs.path.join('local_colabdata', self.user['username'], self.active_operation_name, 'mscolab_operation.ftml') if mss_dir.exists(rel_file_path) is True: return @@ -1440,10 +1431,12 @@ def create_local_operation_file(self): def reload_local_wp(self): self.waypoints_model = ft.WaypointsTableModel(filename=self.local_ftml_file, data_dir=self.data_dir) + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() def operation_category_handler(self, update_operations=True): + logging.debug('operation_category_handler') # only after_login if self.mscolab_server_url is not None: self.selected_category = self.ui.filterCategoryCb.currentText() @@ -1471,71 +1464,63 @@ def server_options_handler(self, index): elif selected_option == "Save To Server": self.save_wp_mscolab() + @verify_user_token def fetch_wp_mscolab(self): - if verify_user_token(self.mscolab_server_url, self.token): - server_xml = self.request_wps_from_server() - server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) - self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, server_waypoints_model, True, self.ui) - self.merge_dialog.saveBtn.setDisabled(True) - if self.merge_dialog.exec_(): - xml_content = self.merge_dialog.get_values() - if xml_content is not None: - self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) - self.waypoints_model.save_to_ftml(self.local_ftml_file) - self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) - self.reload_view_windows() - show_popup(self.ui, "Success", "New Waypoints Fetched To Local File!", icon=1) - self.merge_dialog.close() - self.merge_dialog = None - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + server_xml = self.request_wps_from_server() + server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) + self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, server_waypoints_model, True, self.ui) + self.merge_dialog.saveBtn.setDisabled(True) + if self.merge_dialog.exec_(): + xml_content = self.merge_dialog.get_values() + if xml_content is not None: + self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) + self.waypoints_model.save_to_ftml(self.local_ftml_file) + self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) + self.reload_view_windows() + show_popup(self.ui, "Success", "New Waypoints Fetched To Local File!", icon=1) + self.merge_dialog.close() + self.merge_dialog = None + @verify_user_token def save_wp_mscolab(self, comment=None): - if verify_user_token(self.mscolab_server_url, self.token): - server_xml = self.request_wps_from_server() - server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) - self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, - server_waypoints_model, parent=self.ui) - self.merge_dialog.saveBtn.setDisabled(True) - if self.merge_dialog.exec_(): - xml_content = self.merge_dialog.get_values() - if xml_content is not None: - self.conn.save_file(self.token, self.active_op_id, xml_content, comment=comment) - self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) - self.waypoints_model.save_to_ftml(self.local_ftml_file) - self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) - self.reload_view_windows() - show_popup(self.ui, "Success", "New Waypoints Saved To Server!", icon=1) - self.merge_dialog.close() - self.merge_dialog = None - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + server_xml = self.request_wps_from_server() + server_waypoints_model = ft.WaypointsTableModel(xml_content=server_xml) + self.merge_dialog = MscolabMergeWaypointsDialog(self.waypoints_model, + server_waypoints_model, parent=self.ui) + self.merge_dialog.saveBtn.setDisabled(True) + if self.merge_dialog.exec_(): + xml_content = self.merge_dialog.get_values() + if xml_content is not None: + self.conn.save_file(self.token, self.active_op_id, xml_content, comment=comment) + self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) + self.waypoints_model.save_to_ftml(self.local_ftml_file) + self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) + self.reload_view_windows() + show_popup(self.ui, "Success", "New Waypoints Saved To Server!", icon=1) + self.merge_dialog.close() + self.merge_dialog = None + @verify_user_token def get_recent_operation(self): """ get most recent operation """ logging.debug('get_recent_operation') - if verify_user_token(self.mscolab_server_url, self.token): - data = { - "token": self.token - } - url = urljoin(self.mscolab_server_url, "operations") - r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text != "False": - _json = json.loads(r.text) - operations = _json["operations"] - recent_operation = None - if operations: - recent_operation = operations[-1] - return recent_operation - else: - show_popup(self.ui, "Error", "Session expired, new login required") - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + data = { + "token": self.token + } + url = urljoin(self.mscolab_server_url, "operations") + r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + if r.text == "False": + raise MSColabConnectionError("Session expired, new login required") + _json = json.loads(r.text) + operations = _json["operations"] + recent_operation = None + if operations: + recent_operation = operations[-1] + return recent_operation @QtCore.pyqtSlot() def reload_operation_list(self): @@ -1560,6 +1545,7 @@ def render_new_permission(self, op_id, u_id): to render new permission if added """ + logging.debug('render_new_permission') data = { 'token': self.token } @@ -1593,6 +1579,7 @@ def handle_update_permission(self, op_id, u_id, access_level): function updates existing permissions and related control availability """ + logging.debug('handle_update_permission') if u_id == self.user["id"]: # update table of operations operation_name = None @@ -1652,9 +1639,13 @@ def delete_operation_from_list(self, op_id): @QtCore.pyqtSlot(int, int) def handle_revoke_permission(self, op_id, u_id): + logging.debug('handle_revoke_permission') if u_id == self.user["id"]: + revoked_operation_currently_active = True if self.active_op_id == op_id else False operation_name = self.delete_operation_from_list(op_id) if operation_name is not None: + if revoked_operation_currently_active: + self.ui.userCountLabel.hide() show_popup(self.ui, "Permission Revoked", f'Your access to operation - "{operation_name}" was revoked!', icon=1) # on import permissions revoked name can not taken from the operation list, @@ -1663,238 +1654,225 @@ def handle_revoke_permission(self, op_id, u_id): self.signal_permission_revoked.emit(op_id) if self.active_op_id == op_id: - self.ui.listFlightTracks.setCurrentRow(0) - self.ui.activate_selected_flight_track() + self._activate_first_local_flighttrack() @QtCore.pyqtSlot(int) def handle_operation_deleted(self, op_id): - logging.debug('handle_operation_deleted') + logging.debug('handle_operation_deleted %s %s', op_id, self.active_op_id) old_operation_name = self.active_operation_name old_active_id = self.active_op_id operation_name = self.delete_operation_from_list(op_id) - if op_id == old_active_id and operation_name is None: - operation_name = old_operation_name - show_popup(self.ui, "Success", f'Operation "{operation_name}" was deleted!', icon=1) + if op_id == old_active_id: + if operation_name is None: + operation_name = old_operation_name + show_popup(self.ui, "Information", f'Active operation "{operation_name}" is inaccessible!', icon=1) + @QtCore.pyqtSlot(int, int) + def update_active_user_label(self, op_id, count): + # Update UI component which displays the number of active users + if self.active_op_id == op_id: + self.ui.userCountLabel.setText(f"Active Users: {count}") + + @QtCore.pyqtSlot(str) + def handle_change_message(self, message): + self.lastChangeMessage = message + + @verify_user_token def show_categories_to_ui(self, ops=None): """ adds the list of operation categories to the UI """ 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 - } - url = urljoin(self.mscolab_server_url, "operations") - try: - r = requests.get(url, 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) - self.ui.filterCategoryCb.clear() - categories = set(["*ANY*"]) - for operation in operations: - categories.add(operation["category"]) - categories.remove("*ANY*") - categories = ["*ANY*"] + sorted(categories) - category = config_loader(dataset="MSCOLAB_category") - self.ui.filterCategoryCb.addItems(categories) - if category in categories: - index = categories.index(category) - self.ui.filterCategoryCb.setCurrentIndex(index) - self.operation_category_handler(update_operations=False) - self.ui.filterCategoryCb.currentIndexChanged.connect(self.operation_category_handler) - - def add_operations_to_ui(self): - logging.debug('add_operations_to_ui') r = None - if verify_user_token(self.mscolab_server_url, self.token): - skip_archived = config_loader(dataset="MSCOLAB_skip_archived_operations") + if ops is not None: + r = ops + else: data = { - "token": self.token, - "skip_archived": skip_archived + "token": self.token } url = urljoin(self.mscolab_server_url, "operations") - r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text != "False": - _json = json.loads(r.text) - self.operations = _json["operations"] - logging.debug("adding operations to ui") - operations = sorted(self.operations, key=lambda k: k["path"].lower()) - self.ui.listOperationsMSC.clear() - self.operation_archive_browser.listArchivedOperations.clear() - new_operation = None - active_operation = None - for operation in operations: - operation_desc = f'{operation["path"]} - {operation["access_level"]}' - widgetItem = QtWidgets.QListWidgetItem(operation_desc) - widgetItem.op_id = operation["op_id"] - 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 - widgetItem.active = operation["active"] - except KeyError: - widgetItem.active = True - if widgetItem.active: - self.ui.listOperationsMSC.addItem(widgetItem) - if widgetItem.op_id == self.active_op_id: - active_operation = widgetItem - if widgetItem.op_id == self.new_op_id: - new_operation = widgetItem - else: - self.operation_archive_browser.listArchivedOperations.addItem(widgetItem) - if new_operation is not None: - logging.debug("%s %s %s", new_operation, self.new_op_id, self.active_op_id) - self.ui.listOperationsMSC.itemActivated.emit(new_operation) - elif active_operation is not None: - logging.debug("%s %s %s", new_operation, self.new_op_id, self.active_op_id) - self.ui.listOperationsMSC.itemActivated.emit(active_operation) - self.ui.listOperationsMSC.itemActivated.connect(self.set_active_op_id) - self.new_op_id = None + try: + r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.MissingSchema: + raise MSColabConnectionError("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) + self.ui.filterCategoryCb.clear() + categories = set(["*ANY*"]) + for operation in operations: + categories.add(operation["category"]) + categories.remove("*ANY*") + categories = ["*ANY*"] + sorted(categories) + category = config_loader(dataset="MSCOLAB_category") + self.ui.filterCategoryCb.addItems(categories) + if category in categories: + index = categories.index(category) + self.ui.filterCategoryCb.setCurrentIndex(index) + self.operation_category_handler(update_operations=False) + self.ui.filterCategoryCb.currentIndexChanged.connect(self.operation_category_handler) + + @verify_user_token + def add_operations_to_ui(self): + logging.debug('add_operations_to_ui') + r = None + skip_archived = config_loader(dataset="MSCOLAB_skip_archived_operations") + data = { + "token": self.token, + "skip_archived": skip_archived + } + url = urljoin(self.mscolab_server_url, "operations") + r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + if r.text == "False": + raise MSColabConnectionError("Session expired, new login required") + + _json = json.loads(r.text) + self.operations = _json["operations"] + operations = sorted(self.operations, key=lambda k: k["path"].lower()) + self.ui.listOperationsMSC.clear() + self.operation_archive_browser.listArchivedOperations.clear() + new_operation = None + active_operation = None + for operation in operations: + operation_desc = f'{operation["path"]} - {operation["access_level"]}' + widgetItem = QtWidgets.QListWidgetItem(operation_desc) + widgetItem.op_id = operation["op_id"] + 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 + widgetItem.active = operation["active"] + except KeyError: + widgetItem.active = True + if widgetItem.active: + self.ui.listOperationsMSC.addItem(widgetItem) + if widgetItem.op_id == self.active_op_id: + active_operation = widgetItem + if widgetItem.op_id == self.new_op_id: + new_operation = widgetItem else: - show_popup(self.ui, "Error", "Session expired, new login required") - self.logout() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.operation_archive_browser.listArchivedOperations.addItem(widgetItem) + if new_operation is not None: + logging.debug("%s %s %s", new_operation, self.new_op_id, self.active_op_id) + self.ui.listOperationsMSC.itemActivated.emit(new_operation) + elif active_operation is not None: + logging.debug("%s %s %s", new_operation, self.new_op_id, self.active_op_id) + self.ui.listOperationsMSC.itemActivated.emit(active_operation) + elif self.active_op_id is not None: + logging.debug("%s %s %s", new_operation, self.new_op_id, self.active_op_id) + show_popup(self.ui, "Information", + f'Active operation "{self.active_operation_name}" is inaccessible!', icon=1) + self._activate_first_local_flighttrack() + + self.ui.listOperationsMSC.itemActivated.connect(self.set_active_op_id) + self.new_op_id = None return r def show_operation_options_in_inactivated_state(self, access_level): + logging.debug('show_operation_options_in_inactivated_state') self.ui.actionUnarchiveOperation.setEnabled(False) if access_level in ["creator", "admin"]: self.ui.actionUnarchiveOperation.setEnabled(True) - def archive_operation(self): + @verify_user_token + def archive_operation(self, _): logging.debug("handle_archive_operation") - if verify_user_token(self.mscolab_server_url, self.token): - ret = QtWidgets.QMessageBox.warning( - self.ui, self.tr("Mission Support System"), - self.tr(f"Do you want to archive this operation '{self.active_operation_name}'?"), - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) - if ret == QtWidgets.QMessageBox.Yes: - data = { - "token": self.token, - "op_id": self.active_op_id, - # when a user archives an operation we set the max “natural” integer in days - "days": sys.maxsize, - } - url = urljoin(self.mscolab_server_url, 'set_last_used') - try: - res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - except requests.exceptions.RequestException as e: - logging.debug(e) - show_popup(self.ui, "Error", "Some error occurred! Could not archive operation.") - else: - res.raise_for_status() - self.reload_operations() - self.signal_operation_removed.emit(self.active_op_id) - logging.debug("activate local") - self.ui.listFlightTracks.setCurrentRow(0) - self.ui.activate_selected_flight_track() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + ret = QMessageBox.warning( + self.ui, self.tr("Mission Support System"), + self.tr(f"Do you want to archive this operation '{self.active_operation_name}'?"), + QMessageBox.Yes | QMessageBox.No, QMessageBox.No) + if ret == QMessageBox.Yes: + data = { + "token": self.token, + "op_id": self.active_op_id, + # when a user archives an operation we set the max “natural” integer in days + "attribute": "active", + "value": "False" + } + url = urljoin(self.mscolab_server_url, 'update_operation') + try: + res = requests.post(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + except requests.exceptions.RequestException as ex: + raise MSColabConnectionError(f"Some error occurred ({ex})! Could not archive operation.") + res.raise_for_status() + self.reload_operations() + self.signal_operation_removed.emit(self.active_op_id) + logging.debug("activate local") + self._activate_first_local_flighttrack() + @verify_user_token def set_active_op_id(self, item): logging.debug('set_active_op_id %s %s %s', item, item.op_id, self.active_op_id) - if verify_user_token(self.mscolab_server_url, self.token): - if not self.ui.local_active: - if item.op_id == self.active_op_id: - font = QtGui.QFont() - font.setBold(True) - item.setFont(font) - return + if not self.ui.local_active and item.op_id == self.active_op_id: + return - # close all hanging window - self.close_external_windows() - self.hide_operation_options() + # close all hanging window + self.close_external_windows() + self.hide_operation_options() - # Turn off work locally toggle - self.ui.workLocallyCheckbox.blockSignals(True) - self.ui.workLocallyCheckbox.setChecked(False) - self.ui.workLocallyCheckbox.blockSignals(False) - - # set active_op_id here - self.active_op_id = item.op_id - self.access_level = item.access_level - self.active_operation_name = item.operation_path - self.active_operation_description = item.active_operation_description - self.active_operation_category = item.operation_category - self.waypoints_model = None + # Turn off work locally toggle + self.ui.workLocallyCheckbox.blockSignals(True) + self.ui.workLocallyCheckbox.setChecked(False) + self.ui.workLocallyCheckbox.blockSignals(False) - self.signal_unarchive_operation.emit(self.active_op_id) + # set active_op_id here + self.active_op_id = item.op_id + self.access_level = item.access_level + self.active_operation_name = item.operation_path + self.active_operation_description = item.active_operation_description + self.active_operation_category = item.operation_category + self.waypoints_model = None - self.inactive_op_id = None - font = QtGui.QFont() - for i in range(self.ui.listOperationsMSC.count()): - self.ui.listOperationsMSC.item(i).setFont(font) - font.setBold(False) + self.signal_unarchive_operation.emit(self.active_op_id) - # Set active operation description - self.set_operation_desc_label(self.active_operation_description) - # set active flightpath here - self.load_wps_from_server() - # display working status - self.ui.workingStatusLabel.setText( - self.ui.tr( - "Working Online.\nAll your changes will be shared with everyone. " - "You can work on the operation asynchronously by checking the 'Work Asynchronously' box.") - ) - # self.ui.workingStatusLabel.show() - # enable access level specific widgets - self.show_operation_options() + # Set active operation description + self.set_operation_desc_label(self.active_operation_description) + # set active flightpath here + self.load_wps_from_server() + # display working status + self.ui.workingStatusLabel.setText( + self.ui.tr( + "Working Online.\nAll your changes will be shared with everyone. " + "You can work on the operation asynchronously by checking the 'Work Asynchronously' box.") + ) + # self.ui.workingStatusLabel.show() + # enable access level specific widgets + self.show_operation_options() - # change font style for selected - font = QtGui.QFont() - for i in range(self.ui.listOperationsMSC.count()): - self.ui.listOperationsMSC.item(i).setFont(font) - font.setBold(True) - item.setFont(font) + # change font style for selected + self._handle_font_bolding(item) - # set new waypoints model to open views - for window in self.ui.get_active_views(): - window.setFlightTrackModel(self.waypoints_model) - if self.access_level == "viewer": - window.disable_navbar_action_buttons() - else: - window.enable_navbar_action_buttons() + # set new waypoints model to open views + for window in self.ui.get_active_views(): + window.setFlightTrackModel(self.waypoints_model) + if self.access_level == "viewer": + window.disable_navbar_action_buttons() + else: + window.enable_navbar_action_buttons() - self.ui.switch_to_mscolab() - else: - if self.mscolab_server_url is not None: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + self.ui.switch_to_mscolab() + + # Enable the active user count label + self.ui.userCountLabel.show() + + # call select operation method from connection manager to emit signal + self.conn.select_operation(item.op_id) def switch_to_local(self): logging.debug('switch_to_local') self.ui.local_active = True if self.active_op_id is not None: - if verify_user_token(self.mscolab_server_url, self.token): - # change font style for selected - font = QtGui.QFont() + self._handle_font_bolding() - for i in range(self.ui.listOperationsMSC.count()): - self.ui.listOperationsMSC.item(i).setFont(font) - - # close all hanging operation option windows - self.close_external_windows() - self.hide_operation_options() - self.ui.menu_handler() - else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + # close all hanging operation option windows + self.close_external_windows() + self.hide_operation_options() + self.ui.menu_handler() + self.active_op_id = None def show_operation_options(self): self.ui.actionChat.setEnabled(False) @@ -1934,6 +1912,7 @@ def show_operation_options(self): self.ui.actionChangeDescription.setEnabled(True) self.ui.filterCategoryCb.setEnabled(True) self.ui.actionRenameOperation.setEnabled(True) + self.ui.actionArchiveOperation.setEnabled(True) else: if self.admin_window is not None: self.admin_window.close() @@ -1941,7 +1920,6 @@ def show_operation_options(self): if self.access_level in ["creator"]: self.ui.actionDeleteOperation.setEnabled(True) self.ui.actionLeaveOperation.setEnabled(False) - self.ui.actionArchiveOperation.setEnabled(True) self.ui.menuImportFlightTrack.setEnabled(True) @@ -1962,22 +1940,19 @@ def hide_operation_options(self): # change working status label self.ui.workingStatusLabel.setText(self.ui.tr("\n\nNo Operation Selected")) + @verify_user_token def request_wps_from_server(self): - if verify_user_token(self.mscolab_server_url, self.token): - data = { - "token": self.token, - "op_id": self.active_op_id - } - url = urljoin(self.mscolab_server_url, "get_operation_by_id") - r = requests.get(url, data=data) - if r.text != "False": - xml_content = json.loads(r.text)["content"] - return xml_content - else: - show_popup(self.ui, "Error", "Session expired, new login required") + data = { + "token": self.token, + "op_id": self.active_op_id + } + url = urljoin(self.mscolab_server_url, "get_operation_by_id") + r = requests.get(url, data=data) + if r.text != "False": + xml_content = json.loads(r.text)["content"] + return xml_content else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + raise MSColabConnectionError("Session expired, new login required") def load_wps_from_server(self): if self.ui.workLocallyCheckbox.isChecked(): @@ -1985,10 +1960,12 @@ def load_wps_from_server(self): xml_content = self.request_wps_from_server() if xml_content is not None: self.waypoints_model = ft.WaypointsTableModel(xml_content=xml_content) + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) self.waypoints_model.name = self.active_operation_name self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) def reload_operations(self): + logging.debug('reload_operations') ops = self.add_operations_to_ui() selected_category = self.ui.filterCategoryCb.currentText() self.show_categories_to_ui(ops) @@ -2002,17 +1979,17 @@ def reload_wps_from_server(self): self.load_wps_from_server() self.reload_view_windows() - def handle_waypoints_changed(self): + @verify_user_token + def handle_waypoints_changed(self, _1=None, _2=None, _3=None): logging.debug("handle_waypoints_changed") - if verify_user_token(self.mscolab_server_url, self.token): - if self.ui.workLocallyCheckbox.isChecked(): - self.waypoints_model.save_to_ftml(self.local_ftml_file) - else: - xml_content = self.waypoints_model.get_xml_content() - self.conn.save_file(self.token, self.active_op_id, xml_content, comment=None) + if self.ui.workLocallyCheckbox.isChecked(): + self.waypoints_model.save_to_ftml(self.local_ftml_file) else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + xml_content = self.waypoints_model.get_xml_content() + self.conn.save_file(self.token, self.active_op_id, xml_content, comment=None, + messageText=self.lastChangeMessage) + # Reset the last change message to make sure that it is used only once + self.lastChangeMessage = "" def reload_view_windows(self): logging.debug("reload_view_windows") @@ -2030,66 +2007,66 @@ def reload_view_windows(self): except AttributeError as err: logging.error("%s" % err) + @verify_user_token def handle_import_msc(self, file_path, extension, function, pickertype): logging.debug("handle_import_msc") - if verify_user_token(self.mscolab_server_url, self.token): - if self.active_op_id is None: - return - if file_path is None: - return - dir_path, file_name = fs.path.split(file_path) - file_name = fs.path.basename(file_path) - name, file_ext = fs.path.splitext(file_name) - if function is None: - with open_fs(dir_path) as file_dir: - xml_content = file_dir.readtext(file_name) - try: - model = ft.WaypointsTableModel(xml_content=xml_content) - except SyntaxError: + if self.active_op_id is None: + return + if file_path is None: + return + dir_path, file_name = fs.path.split(file_path) + file_name = fs.path.basename(file_path) + if function is None: + with open_fs(dir_path) as file_dir: + xml_content = file_dir.readtext(file_name) + if not verify_waypoint_data(xml_content): show_popup(self.ui, "Import Failed", f"The file - {file_name}, does not contain valid XML") return - else: - # _function = self.ui.import_plugins[file_ext[1:]] - _, new_waypoints = function(file_path) - model = ft.WaypointsTableModel(waypoints=new_waypoints) - xml_doc = self.waypoints_model.get_xml_doc() - xml_content = xml_doc.toprettyxml(indent=" ", newl="\n") - self.waypoints_model.dataChanged.disconnect(self.handle_waypoints_changed) - self.waypoints_model = model - self.handle_waypoints_changed() - self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) - self.reload_view_windows() - show_popup(self.ui, "Import Success", f"The file - {file_name}, was imported successfully!", 1) + try: + model = ft.WaypointsTableModel(xml_content=xml_content) + except SyntaxError: + show_popup(self.ui, "Import Failed", f"The file - {file_name}, does not contain valid XML") + return else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + # _function = self.ui.import_plugins[file_ext[1:]] + _, new_waypoints = function(file_path) + model = ft.WaypointsTableModel(waypoints=new_waypoints) + xml_doc = self.waypoints_model.get_xml_doc() + xml_content = xml_doc.toprettyxml(indent=" ", newl="\n") + if not verify_waypoint_data(xml_content): + show_popup(self.ui, "Import Failed", f"The file - {file_name}, was not imported!", 0) + return + self.waypoints_model.dataChanged.disconnect(self.handle_waypoints_changed) + self.waypoints_model = model + self.waypoints_model.changeMessageSignal.connect(self.handle_change_message) + self.handle_waypoints_changed() + self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) + self.reload_view_windows() + show_popup(self.ui, "Import Success", f"The file - {file_name}, was imported successfully!", 1) + @verify_user_token def handle_export_msc(self, extension, function, pickertype): logging.debug("handle_export_msc") - if verify_user_token(self.mscolab_server_url, self.token): - if self.active_op_id is None: - return + if self.active_op_id is None: + return - # Setting default filename path for filedialogue - default_filename = f'{self.active_operation_name}.{extension}' - file_name = get_save_filename( - self.ui, "Export From Server", - default_filename, f"Flight track (*.{extension})", - pickertype=pickertype) - if file_name is None: - return - if function is None: - xml_doc = self.waypoints_model.get_xml_doc() - dir_path, file_name = fs.path.split(file_name) - with open_fs(dir_path).open(file_name, 'w') as file: - xml_doc.writexml(file, indent=" ", addindent=" ", newl="\n", encoding="utf-8") - else: - name = fs.path.basename(file_name) - function(file_name, name, self.waypoints_model.waypoints) - show_popup(self.ui, "Export Success", f"The file - {file_name}, was exported successfully!", 1) + # Setting default filename path for filedialogue + default_filename = f'{self.active_operation_name}.{extension}' + file_name = get_save_filename( + self.ui, "Export From Server", + default_filename, f"Flight track (*.{extension})", + pickertype=pickertype) + if file_name is None: + return + if function is None: + xml_doc = self.waypoints_model.get_xml_doc() + dir_path, file_name = fs.path.split(file_name) + with open_fs(dir_path).open(file_name, 'w') as file: + xml_doc.writexml(file, indent=" ", addindent=" ", newl="\n", encoding="utf-8") else: - show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") - self.logout() + name = fs.path.basename(file_name) + function(file_name, name, self.waypoints_model.waypoints) + show_popup(self.ui, "Export Success", f"The file - {file_name}, was exported successfully!", 1) def listFlighttrack_itemDoubleClicked(self): logging.debug("listFlighttrack_itemDoubleClicked") @@ -2097,6 +2074,7 @@ def listFlighttrack_itemDoubleClicked(self): self.signal_listFlighttrack_doubleClicked.emit() def logout(self): + logging.debug('logout') if self.mscolab_server_url is None: return self.ui.local_active = True @@ -2162,16 +2140,19 @@ def logout(self): self.operation_archive_browser.hide() + # reset profile image pixmap if hasattr(self, 'profile_dialog'): del self.profile_dialog self.profile_dialog = None + # reset the user count label to 0 + self.ui.userCountLabel.setText("Active Users: 0") + # activate first local flighttrack after logging out - self.ui.listFlightTracks.setCurrentRow(0) - self.ui.activate_selected_flight_track() + self._activate_first_local_flighttrack() -class MscolabMergeWaypointsDialog(QtWidgets.QDialog, merge_wp_ui.Ui_MergeWaypointsDialog): +class MscolabMergeWaypointsDialog(QDialog, merge_wp_ui.Ui_MergeWaypointsDialog): def __init__(self, local_waypoints_model, server_waypoints_model, fetch=False, parent=None): super().__init__(parent) self.setupUi(self) @@ -2243,7 +2224,7 @@ def get_values(self): return self.xml_content -class MscolabHelpDialog(QtWidgets.QDialog, msc_help_dialog.Ui_mscolabHelpDialog): +class MscolabHelpDialog(QDialog, msc_help_dialog.Ui_mscolabHelpDialog): def __init__(self, parent=None): super().__init__(parent) diff --git a/mslib/msui/mscolab_chat.py b/mslib/msui/mscolab_chat.py index bb74e314a..97aa899c2 100644 --- a/mslib/msui/mscolab_chat.py +++ b/mslib/msui/mscolab_chat.py @@ -117,6 +117,7 @@ def __init__(self, token, op_id, user, operation_name, access_level, conn, paren self.conn.signal_message_reply_receive.connect(self.handle_incoming_message_reply) self.conn.signal_message_edited.connect(self.handle_message_edited) self.conn.signal_message_deleted.connect(self.handle_deleted_message) + self.conn.signal_update_collaborator_list.connect(self.update_user_list) # Set Label text self.set_label_text() # Hide Edit Message section @@ -327,19 +328,64 @@ def edit_message(self): # API REQUESTS def load_users(self): # load users to side-tab here - # make request to get users + # make requests to get all users and active users of the operation data = { "token": self.token, "op_id": self.op_id } - url = urljoin(self.mscolab_server_url, 'authorized_users') - r = requests.get(url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) - if r.text != "False": + users_url = urljoin(self.mscolab_server_url, 'authorized_users') + active_users_url = urljoin(self.mscolab_server_url, 'active_users') + + # Fetch both authorized and active users + users_response = requests.get(users_url, data=data, timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + active_response = requests.get(active_users_url, data=data, + timeout=tuple(config_loader(dataset="MSCOLAB_timeout"))) + + if users_response != "False": self.collaboratorsList.clear() - users = r.json()["users"] + users = users_response.json()["users"] + active_users = set(active_response.json()["active_users"]) for user in users: - item = QtWidgets.QListWidgetItem(f'{user["username"]} - {user["access_level"]}', - parent=self.collaboratorsList) + display_text = f'{user["username"]} - {user["access_level"]}' + item = QtWidgets.QListWidgetItem(display_text, parent=self.collaboratorsList) + + # Pixmap for icon i.e. profile image + url = urljoin(self.mscolab_server_url, 'fetch_profile_image') + data = { + "user_id": str(user["id"]), + "token": self.token + } + response = requests.get(url, data=data) + pixmap = QtGui.QPixmap() + if response.status_code == 200: + # pixmap = QtGui.QPixmap() + pixmap.loadFromData(response.content) + else: + first_alphabet = user["username"][0].lower() if user["username"] else "default" + default_avatar_path = f":/gravatars/default-gravatars/{first_alphabet}.png" + pixmap.load(default_avatar_path) + + # Scale pixmap to a standard size + icon_size = QtCore.QSize(50, 50) + pixmap = pixmap.scaled(icon_size, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + + # Load avatar and overlay green dot on profile image pixmap if user is active + if user["id"] in active_users: + painter = QtGui.QPainter(pixmap) + painter.setBrush(QtGui.QColor(0, 230, 0, 230)) # RGBA + # Set a thin pen for the border around green dot + pen = QtGui.QPen(QtGui.QColor(0, 0, 0), 3) # (border color, width) + painter.setPen(pen) + # Draw circle at bottom-right corner + diameter = 13 # Size of the dot + margin = -2 # Distance from the edges + position = QtCore.QPoint(pixmap.width() - diameter - margin, pixmap.height() - diameter - margin) + painter.drawEllipse(position, diameter, diameter) + painter.end() + + # Set the icon + icon = QtGui.QIcon(pixmap) + item.setIcon(icon) self.collaboratorsList.addItem(item) else: show_popup(self, "Error", "Session expired, new login required") @@ -363,17 +409,24 @@ def load_all_messages(self): for message in messages: self.render_new_message(message, scroll=False) self.messageList.scrollToBottom() + self.serviceMessageList.scrollToBottom() else: show_popup(self, "Error", "Session expired, new login required") def render_new_message(self, message, scroll=True): message_item = MessageItem(message, self) - list_widget_item = QtWidgets.QListWidgetItem(self.messageList) + list_widget_item = QtWidgets.QListWidgetItem() list_widget_item.setSizeHint(message_item.sizeHint()) - self.messageList.addItem(list_widget_item) - self.messageList.setItemWidget(list_widget_item, message_item) + # Check if the message is a service message or a normal message and add to its corresponding list + if message['message_type'] == MessageType.SYSTEM_MESSAGE: + self.serviceMessageList.addItem(list_widget_item) + self.serviceMessageList.setItemWidget(list_widget_item, message_item) + else: + self.messageList.addItem(list_widget_item) + self.messageList.setItemWidget(list_widget_item, message_item) if scroll: self.messageList.scrollToBottom() + self.serviceMessageList.scrollToBottom() # SOCKET HANDLERS @QtCore.pyqtSlot(int) @@ -437,6 +490,10 @@ def handle_deleted_message(self, message): self.messageList.takeItem(i) break + @QtCore.pyqtSlot() + def update_user_list(self): + self.load_users() + def closeEvent(self, event): self.viewCloses.emit() diff --git a/mslib/msui/msui.py b/mslib/msui/msui.py index 0a4c4108e..b15bab1aa 100644 --- a/mslib/msui/msui.py +++ b/mslib/msui/msui.py @@ -42,7 +42,6 @@ from mslib.msui import constants from mslib.utils import setup_logging from mslib.msui.icons import icons -from mslib.utils.qt import Worker, Updater from mslib.utils.config import read_config_file from PyQt5 import QtGui, QtCore, QtWidgets @@ -62,7 +61,6 @@ def main(tutorial_mode=False): parser.add_argument("--debug", help="show debugging log messages on console", action="store_true", default=False) parser.add_argument("--logfile", help="Specify logfile location. Set to empty string to disable.", action="store", default=os.path.join(constants.MSUI_CONFIG_PATH, "msui.log")) - parser.add_argument("--update", help="Updates MSS to the newest version", action="store_true", default=False) args = parser.parse_args() @@ -74,16 +72,6 @@ def main(tutorial_mode=False): print("Version:", __version__) sys.exit() - if args.update: - updater = Updater() - updater.on_update_available.connect(lambda old, new: updater.update_mss()) - updater.on_log_update.connect(lambda s: print(s.replace("\n", ""))) - updater.on_status_update.connect(lambda s: print(s.replace("\n", ""))) - updater.run() - while Worker.workers: - list(Worker.workers)[0].wait() - sys.exit() - setup_logging(args) logging.info("MSS Version: %s", __version__) diff --git a/mslib/msui/msui_mainwindow.py b/mslib/msui/msui_mainwindow.py index d287a9d9e..b7dab5941 100644 --- a/mslib/msui/msui_mainwindow.py +++ b/mslib/msui/msui_mainwindow.py @@ -46,11 +46,11 @@ from mslib.msui import flighttrack as ft from mslib.msui import tableview, topview, sideview, linearview from mslib.msui import constants, editor, mscolab -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, show_popup from mslib.utils.config import read_config_file, config_loader +from mslib.utils import release_info from PyQt5 import QtGui, QtCore, QtWidgets, QtTest from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas @@ -408,6 +408,7 @@ def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.lblVersion.setText(f"Version: {__version__}") + self.lblNewVersion.setText(f"{release_info.check_for_new_release()[0]}") self.milestone_url = f'https://github.com/Open-MSS/MSS/issues?q=is%3Aclosed+milestone%3A{__version__[:-1]}' self.lblChanges.setText(f'New Features and Changes') blub = QtGui.QPixmap(python_powered()) @@ -431,7 +432,16 @@ class MSUIMainWindow(QtWidgets.QMainWindow, ui.Ui_MSUIMainWindow): signal_render_new_permission = QtCore.pyqtSignal(int, str) refresh_signal_connect = QtCore.pyqtSignal() - def __init__(self, mscolab_data_dir=None, tutorial_mode=False, *args): + def __init__(self, local_operations_data=None, tutorial_mode=False, *args): + """ + This method initializes the main window of the application. + It sets up the user interface, icons, menu actions, and connects signals to slots. + + :param local_operations_data: Base path used by "work asynchronously" to store operations. + :param tutorial_mode: Whether to run the application in tutorial mode. Default is False. + :param args: Additional arguments to pass to the parent class. + + """ super().__init__(*args) self.tutorial_mode = tutorial_mode self.setupUi(self) @@ -607,7 +617,7 @@ def __init__(self, mscolab_data_dir=None, tutorial_mode=False, *args): self.statusBar.showMessage(self.status()) # Create MSColab instance to handle all MSColab functionalities - self.mscolab = mscolab.MSUIMscolab(parent=self, data_dir=mscolab_data_dir) + self.mscolab = mscolab.MSUIMscolab(parent=self, local_operations_data=local_operations_data) # Setting up MSColab Tab self.connectBtn.clicked.connect(self.mscolab.open_connect_window) @@ -633,10 +643,6 @@ def __init__(self, mscolab_data_dir=None, tutorial_mode=False, *args): self.mscolab.signal_render_new_permission.connect( lambda op_id, path: self.signal_render_new_permission.emit(op_id, path)) - # Don't start the updater during a test run of msui - if "pytest" not in sys.modules: - self.updater = UpdaterUI(self) - self.actionUpdater.triggered.connect(self.updater.show) self.openOperationsGb.hide() def bring_main_window_to_front(self): @@ -913,6 +919,7 @@ def activate_flight_track(self, item): self.listFlightTracks.item(i).setFont(font) font.setBold(True) item.setFont(font) + self.userCountLabel.hide() self.menu_handler() self.signal_activate_flighttrack.emit(self.active_flight_track) diff --git a/mslib/msui/msui_web_browser.py b/mslib/msui/msui_web_browser.py deleted file mode 100644 index 6347ad155..000000000 --- a/mslib/msui/msui_web_browser.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -""" - - mslib.msui.msui_web_browser.py - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - MSUIWebBrowser can be used for localhost usage and testing purposes. - - This file is part of MSS. - - :copyright: Copyright 2023 Nilupul Manodya - :license: APACHE-2.0, see LICENSE for details. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -""" - -import os -import sys - -from PyQt5.QtCore import QUrl, QTimer -from PyQt5.QtWidgets import QMainWindow, QPushButton, QToolBar, QApplication -from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile - -from mslib.msui.constants import MSUI_CONFIG_PATH - - -class MSUIWebBrowser(QMainWindow): - def __init__(self, url: str): - super().__init__() - - self.web_view = QWebEngineView(self) - self.setCentralWidget(self.web_view) - - self._url = url - self.profile = QWebEngineProfile().defaultProfile() - self.profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies) - self.browser_storage_folder = os.path.join(MSUI_CONFIG_PATH, 'webbrowser', '.cookies') - self.profile.setPersistentStoragePath(self.browser_storage_folder) - - self.back_button = QPushButton("← Back", self) - self.forward_button = QPushButton("→ Forward", self) - self.refresh_button = QPushButton("🔄 Refresh", self) - - self.back_button.clicked.connect(self.web_view.back) - self.forward_button.clicked.connect(self.web_view.forward) - self.refresh_button.clicked.connect(self.web_view.reload) - - toolbar = QToolBar() - toolbar.addWidget(self.back_button) - toolbar.addWidget(self.forward_button) - toolbar.addWidget(self.refresh_button) - self.addToolBar(toolbar) - - self.web_view.load(QUrl(self._url)) - self.setWindowTitle("MSS Web Browser") - self.resize(800, 600) - self.show() - - def closeEvent(self, event): - """ - Delete all cookies when closing the web browser - """ - self.profile.cookieStore().deleteAllCookies() - - -if __name__ == "__main__": - ''' - This function will be moved to handle accordingly the test cases. - The 'connection' variable determines when the web browser should be - closed, typically after the user logged in and establishes a connection - ''' - - CONNECTION = False - - def close_qtwebengine(): - """ - Close the main window - """ - main.close() - - def check_connection(): - """ - Schedule the close_qtwebengine function to be called asynchronously - """ - if CONNECTION: - QTimer.singleShot(0, close_qtwebengine) - - # app = QApplication(sys.argv) - app = QApplication(['', '--no-sandbox']) - WEB_URL = "https://www.google.com/" - main = MSUIWebBrowser(WEB_URL) - - QTimer.singleShot(0, check_connection) - - sys.exit(app.exec_()) diff --git a/mslib/msui/qt5/ui_about_dialog.py b/mslib/msui/qt5/ui_about_dialog.py index 32d6a9f15..46725b0ff 100644 --- a/mslib/msui/qt5/ui_about_dialog.py +++ b/mslib/msui/qt5/ui_about_dialog.py @@ -2,9 +2,10 @@ # Form implementation generated from reading ui file 'ui_about_dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# WARNING! All changes made in this file will be lost! +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. from PyQt5 import QtCore, QtGui, QtWidgets @@ -13,7 +14,7 @@ class Ui_AboutMSUIDialog(object): def setupUi(self, AboutMSUIDialog): AboutMSUIDialog.setObjectName("AboutMSUIDialog") - AboutMSUIDialog.resize(1052, 600) + AboutMSUIDialog.resize(1052, 771) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -82,6 +83,9 @@ def setupUi(self, AboutMSUIDialog): self.lblChanges.setObjectName("lblChanges") self.horizontalLayout_2.addWidget(self.lblChanges) self.verticalLayout_2.addLayout(self.horizontalLayout_2) + self.lblNewVersion = QtWidgets.QLabel(AboutMSUIDialog) + self.lblNewVersion.setObjectName("lblNewVersion") + self.verticalLayout_2.addWidget(self.lblNewVersion) spacerItem2 = QtWidgets.QSpacerItem(20, 10, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) self.verticalLayout_2.addItem(spacerItem2) self.lblLicense = QtWidgets.QLabel(AboutMSUIDialog) @@ -109,7 +113,7 @@ def setupUi(self, AboutMSUIDialog): self.verticalLayout.addLayout(self.verticalLayout_2) self.retranslateUi(AboutMSUIDialog) - self.btOK.clicked.connect(AboutMSUIDialog.accept) + self.btOK.clicked.connect(AboutMSUIDialog.accept) # type: ignore QtCore.QMetaObject.connectSlotsByName(AboutMSUIDialog) def retranslateUi(self, AboutMSUIDialog): @@ -120,25 +124,26 @@ def retranslateUi(self, AboutMSUIDialog): self.textBrowser.setHtml(_translate("AboutMSUIDialog", "\n" "
\n" -"Please read the reference documentation:
\n" -"Bauer, R., Grooß, J.-U., Ungermann, J., Bär, M., Geldenhuys, M., and Hoffmann, L.: The Mission Support
\n" -"System (MSS v7.0.4) and its use in planning for the SouthTRAC aircraft campaign, Geosci.
\n" -"Model Dev., 15, 8983–8997, https://doi.org/10.5194/gmd-15-8983-2022, 2022.
\n" -"Rautenhaus, M., Bauer, G., and Doernbrack, A.: A web service based tool to plan
\n" -"atmospheric research flights, Geosci. Model Dev., 5,55-71, https://doi.org/10.5194/gmd-5-55-2012, 2012.
\n" -"and the paper\'s Supplement (which includes a tutorial) before using the application. The documents are available at:
\n" -"* http://www.geosci-model-dev.net/5/55/2012/gmd-5-55-2012.pdf
\n" -"* http://www.geosci-model-dev.net/5/55/2012/gmd-5-55-2012-supplement.pdf
\n" -"\n" -"
When using this software, please be so kind and acknowledge its use by citing the above mentioned reference documentation in publications, presentations, reports, etc. that you create. Thank you very much.
")) +"\n" +"Please read the reference documentation:
\n" +"Bauer, R., Grooß, J.-U., Ungermann, J., Bär, M., Geldenhuys, M., and Hoffmann, L.: The Mission Support
\n" +"System (MSS v7.0.4) and its use in planning for the SouthTRAC aircraft campaign, Geosci.
\n" +"Model Dev., 15, 8983–8997, https://doi.org/10.5194/gmd-15-8983-2022, 2022.
\n" +"Rautenhaus, M., Bauer, G., and Doernbrack, A.: A web service based tool to plan
\n" +"atmospheric research flights, Geosci. Model Dev., 5,55-71, https://doi.org/10.5194/gmd-5-55-2012, 2012.
\n" +"and the paper\'s Supplement (which includes a tutorial) before using the application. The documents are available at:
\n" +"* http://www.geosci-model-dev.net/5/55/2012/gmd-5-55-2012.pdf
\n" +"* http://www.geosci-model-dev.net/5/55/2012/gmd-5-55-2012-supplement.pdf
\n" +"\n" +"
When using this software, please be so kind and acknowledge its use by citing the above mentioned reference documentation in publications, presentations, reports, etc. that you create. Thank you very much.