Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Real-Time tracking of active users in an operation #2437

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a906700
Implement real-time tracking of active users per operation
workaryangupta Jul 16, 2024
6033bf5
spelling error
workaryangupta Jul 16, 2024
4d836b3
storing active user_ids instead of active sessions
workaryangupta Jul 22, 2024
76fd0ed
hide active users label when selecting a flight track
workaryangupta Jul 22, 2024
422a9f9
fix multiple socket manager instances and add tests
workaryangupta Jul 29, 2024
181a0e5
remove unnecesary comments
workaryangupta Jul 29, 2024
0c98143
use flask-socketIO test client
workaryangupta Jul 29, 2024
e1c5d8b
update tests to test with multiple users
workaryangupta Jul 29, 2024
c496da2
add test for get_user_id
workaryangupta Jul 31, 2024
fedab62
Set fixed user count label size irrespective of operation description…
workaryangupta Jul 31, 2024
ac82ded
Merge branch 'feat/visualize-concurrent-users' of https://github.com/…
workaryangupta Jul 31, 2024
eb62035
add max size to op description
workaryangupta Jul 31, 2024
4369491
resolve merge conflict
workaryangupta Jul 31, 2024
869df79
Merge branch 'GSOC2024-AryanGupta' into feat/visualize-concurrent-users
workaryangupta Jul 31, 2024
a882918
hide active user count if revoked operation is active operation
workaryangupta Jul 31, 2024
e41c80a
Merge branch 'feat/visualize-concurrent-users' of https://github.com/…
workaryangupta Jul 31, 2024
62a450d
Fix active user decrement on revoked permissions across multiple oper…
workaryangupta Aug 1, 2024
0bda5a2
changes from #2447 : Fix tests failing due to socketio connection issues
workaryangupta Aug 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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