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])