diff --git a/mslib/mscolab/file_manager.py b/mslib/mscolab/file_manager.py index 5b41d3a50..3cfdada71 100644 --- a/mslib/mscolab/file_manager.py +++ b/mslib/mscolab/file_manager.py @@ -385,7 +385,8 @@ def get_authorized_users(self, op_id): users = [] for permission in permissions: user = User.query.filter_by(id=permission.u_id).first() - users.append({"username": user.username, "access_level": permission.access_level}) + users.append({"username": user.username, "access_level": permission.access_level, + "id": permission.u_id}) return users def save_file(self, op_id, content, user, comment=""): diff --git a/mslib/mscolab/server.py b/mslib/mscolab/server.py index 948b33205..0034d6455 100644 --- a/mslib/mscolab/server.py +++ b/mslib/mscolab/server.py @@ -530,6 +530,13 @@ def authorized_users(): return json.dumps({"users": fm.get_authorized_users(int(op_id))}) +@APP.route('/active_users', methods=["GET"]) +@verify_user +def active_users(): + op_id = request.args.get('op_id', request.form.get('op_id', None)) + return jsonify(active_users=list(sockio.sm.active_users_per_operation[int(op_id)])) + + @APP.route('/operations', methods=['GET']) @verify_user def get_operations(): diff --git a/mslib/mscolab/sockets_manager.py b/mslib/mscolab/sockets_manager.py index 8a4159301..a51db4199 100644 --- a/mslib/mscolab/sockets_manager.py +++ b/mslib/mscolab/sockets_manager.py @@ -275,6 +275,7 @@ def handle_file_save(self, json_req): op_id = json_req['op_id'] content = json_req['content'] comment = json_req.get('comment', "") + messageText = json_req.get('messageText') user = User.verify_auth_token(json_req['token']) if user is not None: # when the socket connection is expired this in None and also on wrong tokens @@ -282,7 +283,7 @@ def handle_file_save(self, json_req): # if permission is correct and file saved properly if perm and self.fm.save_file(int(op_id), content, user, comment): # send service message - message_ = f"[service message] **{user.username}** saved changes" + message_ = f"[service message] **{user.username}** saved changes. {messageText}" new_message = self.cm.add_message(user, message_, str(op_id), message_type=MessageType.SYSTEM_MESSAGE) new_message_dict = get_message_dict(new_message) socketio.emit('chat-message-client', json.dumps(new_message_dict)) diff --git a/mslib/msui/flighttrack.py b/mslib/msui/flighttrack.py index 1d3ad2b15..5f9903f7a 100644 --- a/mslib/msui/flighttrack.py +++ b/mslib/msui/flighttrack.py @@ -177,6 +177,9 @@ class WaypointsTableModel(QtCore.QAbstractTableModel): flight performance calculations. """ + # Signal emitted when a waypoint is moved, inserted or deleted + changeMessageSignal = QtCore.pyqtSignal(str) + def __init__(self, name="", filename=None, waypoints=None, mscolab_mode=False, data_dir=mss_default.mss_dir, xml_content=None): super().__init__() @@ -352,6 +355,7 @@ def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): waypoint.location = loc[1] # A change of position requires an update of the distances. if update: + self.changeMessageSignal.emit(f'Moved waypoint {index.row()}') self.update_distances(index.row()) # Notify the views that items between the edited item and # the distance item of the corresponding waypoint have been @@ -378,6 +382,7 @@ def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): waypoint.lat, waypoint.lon = loc[0] waypoint.location = loc[1] if update: + self.changeMessageSignal.emit(f'Moved waypoint {index.row()}') self.update_distances(index.row()) index2 = self.createIndex(index.row(), LOCATION) elif column == FLIGHTLEVEL: @@ -394,6 +399,7 @@ def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): waypoint.flightlevel = flightlevel waypoint.pressure = pressure if update: + self.changeMessageSignal.emit(f'Moved waypoint {index.row()}') self.update_distances(index.row()) # need to notify view of the second item that has been # changed as well. @@ -415,6 +421,7 @@ def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): waypoint.pressure = pressure waypoint.flightlevel = flightlevel if update: + self.changeMessageSignal.emit(f'Moved waypoint {index.row()}') self.update_distances(index.row()) index2 = self.createIndex(index.row(), FLIGHTLEVEL) else: @@ -427,7 +434,7 @@ def setData(self, index, value, role=QtCore.Qt.EditRole, update=True): return False def insertRows(self, position, rows=1, index=QtCore.QModelIndex(), - waypoints=None): + waypoints=None, hexagonCreated=False): """ Insert waypoint; overrides the corresponding QAbstractTableModel method. @@ -437,6 +444,9 @@ def insertRows(self, position, rows=1, index=QtCore.QModelIndex(), assert len(waypoints) == rows, (waypoints, rows) + savedChangeMessage = "Hexagon created." if hexagonCreated else ("Inserted a new waypoint.") + self.changeMessageSignal.emit(savedChangeMessage) + self.beginInsertRows(QtCore.QModelIndex(), position, position + rows - 1) for row, wp in enumerate(waypoints): @@ -447,11 +457,17 @@ def insertRows(self, position, rows=1, index=QtCore.QModelIndex(), self.modified = True return True - def removeRows(self, position, rows=1, index=QtCore.QModelIndex()): + def removeRows(self, position, rows=1, index=QtCore.QModelIndex(), hexagonDeleted=False): """ Remove waypoint; overrides the corresponding QAbstractTableModel method. """ + if hexagonDeleted: + savedChangeMessage = f"Deleted waypoints {position}-{position + rows - 1}." + else: + savedChangeMessage = f"Deleted waypoint {position}." + self.changeMessageSignal.emit(savedChangeMessage) + # beginRemoveRows emits rowsAboutToBeRemoved(index, first, last). self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1) diff --git a/mslib/msui/hexagon_dockwidget.py b/mslib/msui/hexagon_dockwidget.py index 493148ac7..5373703dd 100644 --- a/mslib/msui/hexagon_dockwidget.py +++ b/mslib/msui/hexagon_dockwidget.py @@ -114,7 +114,7 @@ def _add_hexagon(self): waypoints.append( ft.Waypoint(lon=float(point[1]), lat=float(point[0]), flightlevel=float(flightlevel), comments=f"Hexagon {(i + 1):d}")) - waypoints_model.insertRows(row, rows=len(waypoints), waypoints=waypoints) + waypoints_model.insertRows(row, rows=len(waypoints), waypoints=waypoints, hexagonCreated=True) index = waypoints_model.index(row, 0) table_view.setCurrentIndex(index) table_view.resizeRowsToContents() @@ -156,7 +156,7 @@ def _remove_hexagon(self): QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes) if sel == QtWidgets.QMessageBox.Yes: - waypoints_model.removeRows(row_min, rows=7) + waypoints_model.removeRows(row_min, rows=7, hexagonDeleted=True) else: raise HexagonException("Cannot remove hexagon, please select a hexagon " "waypoint ('Hexagon x' in comments field)") diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index 8c559ef3b..b977fb475 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -575,6 +575,9 @@ def __init__(self, parent=None, data_dir=None): # Gravatar image path self.gravatar = None + # Service message text for flight-track changes (waypoints inserted, moved or deleted) + self.lastChangeMessage = "" + # set data dir, uri if data_dir is None: self.data_dir = config_loader(dataset="mss_dir") @@ -1157,6 +1160,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): @@ -1441,6 +1445,7 @@ 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() @@ -1482,6 +1487,7 @@ def fetch_wp_mscolab(self): 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() @@ -1504,6 +1510,7 @@ def save_wp_mscolab(self, comment=None): 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() @@ -1686,6 +1693,10 @@ def update_active_user_label(self, op_id, count): 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 + def show_categories_to_ui(self, ops=None): """ adds the list of operation categories to the UI @@ -2001,6 +2012,7 @@ 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) @@ -2025,7 +2037,10 @@ def handle_waypoints_changed(self): 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) + 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 = "" else: show_popup(self.ui, "Error", "Your Connection is expired. New Login required!") self.logout() @@ -2072,6 +2087,7 @@ def handle_import_msc(self, file_path, extension, function, pickertype): xml_content = xml_doc.toprettyxml(indent=" ", newl="\n") 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() 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/qt5/ui_mainwindow.py b/mslib/msui/qt5/ui_mainwindow.py index 3934c4f91..1ba2ca0fd 100644 --- a/mslib/msui/qt5/ui_mainwindow.py +++ b/mslib/msui/qt5/ui_mainwindow.py @@ -389,13 +389,3 @@ def retranslateUi(self, MSUIMainWindow): self.actionLeaveOperation.setText(_translate("MSUIMainWindow", "&Leave Operation")) self.actionArchiveOperation.setText(_translate("MSUIMainWindow", "Archive Operation")) self.actionChangeCategory.setText(_translate("MSUIMainWindow", "Change Category")) - - -if __name__ == "__main__": - import sys - app = QtWidgets.QApplication(sys.argv) - MSUIMainWindow = QtWidgets.QMainWindow() - ui = Ui_MSUIMainWindow() - ui.setupUi(MSUIMainWindow) - MSUIMainWindow.show() - sys.exit(app.exec_()) diff --git a/mslib/msui/qt5/ui_mscolab_operation_window.py b/mslib/msui/qt5/ui_mscolab_operation_window.py index 0b23730bf..2abd37a8f 100644 --- a/mslib/msui/qt5/ui_mscolab_operation_window.py +++ b/mslib/msui/qt5/ui_mscolab_operation_window.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'ui_mscolab_operation_window.ui' +# Form implementation generated from reading ui file 'ui/ui_mscolab_operation_window.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 @@ -15,7 +16,7 @@ def setupUi(self, MscolabOperation): MscolabOperation.setObjectName("MscolabOperation") MscolabOperation.setWindowModality(QtCore.Qt.NonModal) MscolabOperation.setEnabled(True) - MscolabOperation.resize(867, 687) + MscolabOperation.resize(1066, 687) MscolabOperation.setMinimumSize(QtCore.QSize(600, 400)) self.centralwidget = QtWidgets.QWidget(MscolabOperation) self.centralwidget.setObjectName("centralwidget") @@ -25,6 +26,7 @@ def setupUi(self, MscolabOperation): self.verticalLayout_4.setObjectName("verticalLayout_4") self.horizontalLayout_3 = QtWidgets.QHBoxLayout() self.horizontalLayout_3.setSizeConstraint(QtWidgets.QLayout.SetMinimumSize) + self.horizontalLayout_3.setSpacing(7) self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.user_info = QtWidgets.QLabel(self.centralwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) @@ -42,13 +44,22 @@ def setupUi(self, MscolabOperation): self.proj_info.setSizePolicy(sizePolicy) self.proj_info.setObjectName("proj_info") self.horizontalLayout_3.addWidget(self.proj_info) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem = QtWidgets.QSpacerItem(330, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem) + self.changes_info = QtWidgets.QLabel(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.changes_info.sizePolicy().hasHeightForWidth()) + self.changes_info.setSizePolicy(sizePolicy) + self.changes_info.setMinimumSize(QtCore.QSize(266, 0)) + self.changes_info.setObjectName("changes_info") + self.horizontalLayout_3.addWidget(self.changes_info) self.horizontalLayout_3.setStretch(0, 1) self.horizontalLayout_3.setStretch(1, 1) - self.horizontalLayout_3.setStretch(2, 2) self.verticalLayout_4.addLayout(self.horizontalLayout_3) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setSpacing(7) self.horizontalLayout_4.setObjectName("horizontalLayout_4") self.collaboratorsList = QtWidgets.QListWidget(self.centralwidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) @@ -56,7 +67,7 @@ def setupUi(self, MscolabOperation): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.collaboratorsList.sizePolicy().hasHeightForWidth()) self.collaboratorsList.setSizePolicy(sizePolicy) - self.collaboratorsList.setMinimumSize(QtCore.QSize(256, 300)) + self.collaboratorsList.setMinimumSize(QtCore.QSize(200, 300)) self.collaboratorsList.setObjectName("collaboratorsList") self.horizontalLayout_4.addWidget(self.collaboratorsList) self.verticalLayout_3 = QtWidgets.QVBoxLayout() @@ -130,6 +141,16 @@ def setupUi(self, MscolabOperation): self.verticalLayout_3.setStretch(1, 4) self.verticalLayout_3.setStretch(2, 1) self.horizontalLayout_4.addLayout(self.verticalLayout_3) + self.serviceMessageList = QtWidgets.QListWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.serviceMessageList.sizePolicy().hasHeightForWidth()) + self.serviceMessageList.setSizePolicy(sizePolicy) + self.serviceMessageList.setMinimumSize(QtCore.QSize(275, 0)) + self.serviceMessageList.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.serviceMessageList.setObjectName("serviceMessageList") + self.horizontalLayout_4.addWidget(self.serviceMessageList) self.horizontalLayout_4.setStretch(1, 1) self.verticalLayout_4.addLayout(self.horizontalLayout_4) self.gridLayout.addLayout(self.verticalLayout_4, 0, 0, 1, 1) @@ -139,7 +160,7 @@ def setupUi(self, MscolabOperation): MscolabOperation.addAction(self.actionCloseWindow) self.retranslateUi(MscolabOperation) - self.actionCloseWindow.triggered.connect(MscolabOperation.close) + self.actionCloseWindow.triggered.connect(MscolabOperation.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MscolabOperation) def retranslateUi(self, MscolabOperation): @@ -147,6 +168,7 @@ def retranslateUi(self, MscolabOperation): MscolabOperation.setWindowTitle(_translate("MscolabOperation", "Mscolab Operation Chat")) self.user_info.setText(_translate("MscolabOperation", "Logged In: ")) self.proj_info.setText(_translate("MscolabOperation", "Operation:")) + self.changes_info.setText(_translate("MscolabOperation", "Change Log:")) self.searchMessageLineEdit.setPlaceholderText(_translate("MscolabOperation", "Search Message")) self.searchPrevBtn.setText(_translate("MscolabOperation", "Previous")) self.searchNextBtn.setText(_translate("MscolabOperation", "Next")) diff --git a/mslib/msui/socket_control.py b/mslib/msui/socket_control.py index 3d161cd48..7bfc8c4b1 100644 --- a/mslib/msui/socket_control.py +++ b/mslib/msui/socket_control.py @@ -47,6 +47,7 @@ class ConnectionManager(QtCore.QObject): signal_operation_list_updated = QtCore.pyqtSignal(name="operation list updated") signal_operation_deleted = QtCore.pyqtSignal(int, name="operation deleted") signal_active_user_update = QtCore.pyqtSignal(int, int) + signal_update_collaborator_list = QtCore.pyqtSignal() def __init__(self, token, user, mscolab_server_url=mss_default.mscolab_server_url): super(ConnectionManager, self).__init__() @@ -91,6 +92,7 @@ def handle_active_user_update(self, data): op_id = data['op_id'] count = data['count'] self.signal_active_user_update.emit(op_id, count) + self.signal_update_collaborator_list.emit() def handle_update_permission(self, message): """ @@ -196,7 +198,7 @@ def select_operation(self, op_id): # Emit an event to notify the server of the operation selection. self.sio.emit('operation-selected', {'token': self.token, 'op_id': op_id}) - def save_file(self, token, op_id, content, comment=None): + def save_file(self, token, op_id, content, comment=None, messageText=""): # ToDo refactor API if verify_user_token(self.mscolab_server_url, self.token): logging.debug("saving file") @@ -204,7 +206,8 @@ def save_file(self, token, op_id, content, comment=None): "op_id": op_id, "token": self.token, "content": content, - "comment": comment}) + "comment": comment, + "messageText": messageText}) else: # this triggers disconnect self.signal_reload.emit(op_id) diff --git a/mslib/msui/ui/ui_mscolab_operation_window.ui b/mslib/msui/ui/ui_mscolab_operation_window.ui index 76dc0dccc..c4266cf67 100644 --- a/mslib/msui/ui/ui_mscolab_operation_window.ui +++ b/mslib/msui/ui/ui_mscolab_operation_window.ui @@ -12,7 +12,7 @@ 0 0 - 867 + 1066 687 @@ -30,9 +30,9 @@ - + - -1 + 7 QLayout::SetMinimumSize @@ -70,18 +70,37 @@ - 40 + 330 20 + + + + + 0 + 0 + + + + + 266 + 0 + + + + Change Log: + + + - + - -1 + 7 @@ -93,7 +112,7 @@ - 256 + 200 300 @@ -259,6 +278,25 @@ + + + + + 0 + 0 + + + + + 275 + 0 + + + + QAbstractItemView::NoSelection + + + diff --git a/tests/_test_mscolab/test_file_manager.py b/tests/_test_mscolab/test_file_manager.py index 2c63e34e1..00c03aea1 100644 --- a/tests/_test_mscolab/test_file_manager.py +++ b/tests/_test_mscolab/test_file_manager.py @@ -241,7 +241,7 @@ def test_get_authorized_users(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path='operation5') assert self.fm.get_authorized_users(operation.id) == [{'access_level': 'creator', - 'username': self.userdata[1]}] + 'username': self.userdata[1], 'id': 1}] def test_save_file(self): with self.app.test_client(): diff --git a/tests/_test_mscolab/test_files_api.py b/tests/_test_mscolab/test_files_api.py index 43dcb4864..e7c8ba3de 100644 --- a/tests/_test_mscolab/test_files_api.py +++ b/tests/_test_mscolab/test_files_api.py @@ -79,7 +79,7 @@ def test_get_authorized_users(self): with self.app.test_client(): flight_path, operation = self._create_operation(flight_path="V1") users = self.fm.get_authorized_users(operation.id) - assert users[0] == {'username': 'UV10', 'access_level': 'creator'} + assert users[0] == {'username': 'UV10', 'access_level': 'creator', 'id': 1} def test_fetch_users_without_permission(self): with self.app.test_client(): diff --git a/tests/_test_mscolab/test_server.py b/tests/_test_mscolab/test_server.py index 57636f5cd..6d4caf172 100644 --- a/tests/_test_mscolab/test_server.py +++ b/tests/_test_mscolab/test_server.py @@ -322,7 +322,7 @@ def test_authorized_users(self): "op_id": operation.id}) assert response.status_code == 200 data = json.loads(response.data.decode('utf-8')) - assert data["users"] == [{'access_level': 'creator', 'username': self.userdata[1]}] + assert data["users"] == [{'access_level': 'creator', 'username': self.userdata[1], 'id': 1}] def test_delete_operation(self): assert add_user(self.userdata[0], self.userdata[1], self.userdata[2])