Skip to content

Commit

Permalink
Merge pull request #2437 from workaryangupta/feat/visualize-concurren…
Browse files Browse the repository at this point in the history
…t-users

feat: Real-Time tracking of active users in an operation
  • Loading branch information
ReimarBauer authored Aug 5, 2024
2 parents 878ecd8 + 0bda5a2 commit fb41b66
Show file tree
Hide file tree
Showing 14 changed files with 382 additions and 132 deletions.
5 changes: 2 additions & 3 deletions mslib/mscolab/mscolab.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,14 @@


def handle_start(args):
from mslib.mscolab.server import APP, initialize_managers, start_server
from mslib.mscolab.server import APP, sockio, cm, fm, start_server
setup_logging(args)
logging.info("MSS Version: %s", __version__)
logging.info("Python Version: %s", sys.version)
logging.info("Platform: %s (%s)", platform.platform(), platform.architecture())
logging.info("Launching MSColab Server")

app, sockio, cm, fm = initialize_managers(APP)
start_server(app, sockio, cm, fm)
start_server(APP, sockio, cm, fm)


def confirm_action(confirmation_prompt):
Expand Down
9 changes: 5 additions & 4 deletions mslib/mscolab/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@

from mslib.mscolab.conf import mscolab_settings, setup_saml2_backend
from mslib.mscolab.models import Change, MessageType, User
from mslib.mscolab.sockets_manager import setup_managers
from mslib.mscolab.sockets_manager import _setup_managers
from mslib.mscolab.utils import create_files, get_message_dict
from mslib.utils import conditional_decorator
from mslib.index import create_app
Expand Down Expand Up @@ -126,16 +126,16 @@ def confirm_token(token, expiration=3600):
return email


def initialize_managers(app):
sockio, cm, fm = setup_managers(app)
def _initialize_managers(app):
sockio, cm, fm = _setup_managers(app)
# initializing socketio and db
app.wsgi_app = socketio.Middleware(socketio.server, app.wsgi_app)
sockio.init_app(app)
# db.init_app(app)
return app, sockio, cm, fm


_app, sockio, cm, fm = initialize_managers(APP)
_app, sockio, cm, fm = _initialize_managers(APP)


def check_login(emailid, password):
Expand Down Expand Up @@ -688,6 +688,7 @@ def delete_bulk_permissions():
success = fm.delete_bulk_permission(op_id, user, u_ids)
if success:
for u_id in u_ids:
sockio.sm.remove_active_user_id_from_specific_operation(u_id, op_id)
sockio.sm.emit_revoke_permission(u_id, op_id)
sockio.sm.emit_operation_permissions_updated(user.id, op_id)
return jsonify({"success": True, "message": "User permissions successfully deleted!"})
Expand Down
78 changes: 71 additions & 7 deletions mslib/mscolab/sockets_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from mslib.mscolab.file_manager import FileManager
from mslib.mscolab.models import MessageType, Permission, User
from mslib.mscolab.utils import get_message_dict
from mslib.mscolab.utils import get_session_id
from mslib.mscolab.utils import get_session_id, get_user_id
from mslib.mscolab.conf import mscolab_settings

socketio = SocketIO(logger=mscolab_settings.SOCKETIO_LOGGER, engineio_logger=mscolab_settings.ENGINEIO_LOGGER,
Expand All @@ -51,20 +51,41 @@ def __init__(self, chat_manager, file_manager):
"""
super(SocketsManager, self).__init__()
self.sockets = []
self.active_users_per_operation = {}
self.cm = chat_manager
self.fm = file_manager

def handle_connect(self):
logging.debug(request.sid)

def handle_operation_selected(self, json_config):
logging.debug("Operation selected: {}".format(json_config))
token = json_config['token']
op_id = json_config['op_id']
user = User.verify_auth_token(token)
if user is None:
return

# Remove the active user_id from any other operations first
self.update_active_users(user.id)

# Add the user to the new operation
if op_id not in self.active_users_per_operation:
self.active_users_per_operation[op_id] = set()
self.active_users_per_operation[op_id].add(user.id)

# Emit the updated count to all users
active_count = len(self.active_users_per_operation[op_id])
socketio.emit('active-user-update', {'op_id': op_id, 'count': active_count})

def update_operation_list(self, json_config):
"""
json_config has:
- token: authentication token
"""
token = json_config["token"]
user = User.verify_auth_token(token)
if not user:
if user is None:
return
socketio.emit('operation-list-update')

Expand All @@ -76,7 +97,7 @@ def join_creator_to_operation(self, json_config):
"""
token = json_config['token']
user = User.verify_auth_token(token)
if not user:
if user is None:
return
op_id = json_config['op_id']
join_room(str(op_id))
Expand Down Expand Up @@ -104,7 +125,7 @@ def handle_start_event(self, json_config):
# authenticate socket
token = json_config['token']
user = User.verify_auth_token(token)
if not user:
if user is None:
return

# fetch operations
Expand Down Expand Up @@ -132,11 +153,52 @@ def handle_start_event(self, json_config):
self.sockets.append(socket_storage)

def handle_disconnect(self):
logging.info("disconnected")
logging.info(request.sid)
logging.debug("Handling disconnect.")

# remove the user from any active operations
user_id = get_user_id(self.sockets, request.sid)
if user_id:
self.update_active_users(user_id)

logging.debug(f"Disconnected: {request.sid}")
# remove socket from socket_storage
self.sockets[:] = [d for d in self.sockets if d['s_id'] != request.sid]

def update_active_users(self, user_id):
"""
Remove the given user_id from all operations and emit updates for active user counts.
"""
for op_id, user_ids in list(self.active_users_per_operation.items()):
if user_id in user_ids:
user_ids.remove(user_id)
active_count = len(user_ids)
logging.debug(f"Updated {op_id}: {active_count} active users")
if user_ids:
# Emit update if there are still active users
socketio.emit('active-user-update', {'op_id': op_id, 'count': active_count})
else:
# If no users left, delete the operation key
del self.active_users_per_operation[op_id]
socketio.emit('active-user-update', {'op_id': op_id, 'count': 0})

def remove_active_user_id_from_specific_operation(self, user_id, op_id):
"""
Remove the given user_id from a specific operation in active_users_per_operation
and emit updates for active user counts.
"""
if op_id in self.active_users_per_operation:
if user_id in self.active_users_per_operation[op_id]:
self.active_users_per_operation[op_id].remove(user_id)
active_count = len(self.active_users_per_operation[op_id])

if self.active_users_per_operation[op_id]:
# Emit update if there are still active users
socketio.emit('active-user-update', {'op_id': op_id, 'count': active_count})
else:
# If no users left, delete the operation key
del self.active_users_per_operation[op_id]
socketio.emit('active-user-update', {'op_id': op_id, 'count': 0})

def handle_message(self, _json):
"""
json is a dictionary version of data sent to back-end
Expand Down Expand Up @@ -262,7 +324,7 @@ def emit_operation_delete(self, op_id):
socketio.emit("operation-deleted", json.dumps({"op_id": op_id}))


def setup_managers(app):
def _setup_managers(app):
"""
takes app as parameter to extract config data,
initializes ChatManager, FileManager, SocketManager and return them
Expand All @@ -283,6 +345,8 @@ def setup_managers(app):
socketio.on_event('file-save', sm.handle_file_save)
socketio.on_event('add-user-to-operation', sm.join_creator_to_operation)
socketio.on_event('update-operation-list', sm.update_operation_list)
# Register the 'operation-selected' event to update active user tracking when an operation is selected
socketio.on_event('operation-selected', sm.handle_operation_selected)

socketio.sm = sm
return socketio, cm, fm
8 changes: 8 additions & 0 deletions mslib/mscolab/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ def get_session_id(sockets, u_id):
return s_id


def get_user_id(sockets, s_id):
u_id = None
for ss in sockets:
if ss["s_id"] == s_id:
u_id = ss["u_id"]
return u_id


def get_message_dict(message):
return {
"id": message.id,
Expand Down
20 changes: 20 additions & 0 deletions mslib/msui/mscolab.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,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()
Expand Down Expand Up @@ -1653,8 +1654,11 @@ def delete_operation_from_list(self, op_id):
@QtCore.pyqtSlot(int, int)
def handle_revoke_permission(self, op_id, u_id):
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,
Expand All @@ -1676,6 +1680,12 @@ def handle_operation_deleted(self, op_id):
operation_name = old_operation_name
show_popup(self.ui, "Success", f'Operation "{operation_name}" was deleted!', 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}")

def show_categories_to_ui(self, ops=None):
"""
adds the list of operation categories to the UI
Expand Down Expand Up @@ -1871,6 +1881,12 @@ def set_active_op_id(self, item):
window.enable_navbar_action_buttons()

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)
else:
if self.mscolab_server_url is not None:
show_popup(self.ui, "Error", "Your Connection is expired. New Login required!")
Expand Down Expand Up @@ -2161,10 +2177,14 @@ 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()
Expand Down
1 change: 1 addition & 0 deletions mslib/msui/msui_mainwindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,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)

Expand Down
Loading

0 comments on commit fb41b66

Please sign in to comment.