Skip to content

Commit 4fc9b4d

Browse files
annapurna-guptaReimarBauer
authored andcommitted
Feat: Implement view sharing in operations with Manage Views widget (#2893)
Adds functionality to share custom views between operation members. Includes a 'Manage Views' widget allowing users to browse and apply shared views, as well as options for renaming views before sharing. Addresses the need for collaborative view configurations within an operation.
1 parent 3a657cb commit 4fc9b4d

14 files changed

Lines changed: 966 additions & 85 deletions

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ in alphabetic order by first name
88
- Anveshan Lal <anveshanrx8@gmail.com>
99
- Aravind Murali <aravindmurali711@gmail.com>
1010
- Aryan Gupta <work.aryangupta@gmail.com>
11+
- Annapurna Gupta <ganu48600@gmail.com>
1112
- Christian Rolf <c.rolf@fz-juelich.de>
1213
- Debajyoti Dasgupta <debajyotidasgupta6@gmail.com>
1314
- Hrithik Kumar Verma <vermahrithik812@gmail.com>

mslib/mscolab/file_manager.py

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from werkzeug.utils import secure_filename
4040
from sqlalchemy.exc import IntegrityError
4141
from mslib.utils.verify_waypoint_data import verify_waypoint_data
42-
from mslib.mscolab.models import db, Operation, Permission, User, Change, Message, ViewSettings
42+
from mslib.mscolab.models import db, Operation, Permission, User, Change, Message, ViewSettings, SharedView, ManageViews
4343
from mslib.mscolab.conf import mscolab_settings
4444

4545

@@ -780,6 +780,85 @@ def import_permissions(self, import_op_id, current_op_id, u_id):
780780
db.session.rollback()
781781
return False, None, "Some error occurred! Could not import permissions. Please try again."
782782

783+
def list_views(self, op_id, user):
784+
if not self.is_member(user.id, op_id):
785+
return False, "Access denied", {}
786+
views = ManageViews.query.filter_by(op_id=op_id).all()
787+
views_data = []
788+
for v in views:
789+
views_data.append({
790+
"id": v.id,
791+
"op_id": v.op_id,
792+
"u_id": v.u_id,
793+
"view_name": v.view_name,
794+
"created_at": v.created_at.isoformat() if v.created_at else None,
795+
"username": v.user.username if v.user else None,
796+
})
797+
return True, "Views fetched successfully", views_data
798+
799+
def check_view_name(self, user, op_id, view_name):
800+
if not (self.is_member(user.id, op_id) or self.is_viewer(user.id, op_id)):
801+
return False, "Access denied"
802+
return ManageViews.query.filter_by(op_id=op_id, view_name=view_name).first() is not None
803+
804+
def save_manage_view_metadata(self, user, op_id, view_name):
805+
if not (self.is_member(user.id, op_id) or self.is_viewer(user.id, op_id)):
806+
return False, "Access denied"
807+
try:
808+
data = ManageViews(
809+
op_id=op_id,
810+
u_id=user.id,
811+
view_name=view_name
812+
)
813+
db.session.add(data)
814+
db.session.commit()
815+
return True, "Metadata saved to database"
816+
except Exception as e:
817+
logging.error("Error saving metadata for user %s: %s", user.id, str(e))
818+
return False, f"Error saving metadata: {str(e)}"
819+
820+
def share_view(self, op_id, view_name, user, settings_data):
821+
"""Share view settings with other users in the same operation."""
822+
if not (self.is_member(user.id, op_id) or self.is_viewer(user.id, op_id)):
823+
return False, "Access denied"
824+
try:
825+
shared_data = json.dumps(settings_data)
826+
existing = SharedView.query.filter_by(op_id=op_id, u_id=user.id, view_name=view_name).first()
827+
if existing:
828+
existing.settings = shared_data
829+
existing.updated_at = datetime.datetime.now(tz=datetime.timezone.utc)
830+
db.session.commit()
831+
return True, "Shared view settings updated successfully"
832+
else:
833+
view_setting = SharedView(
834+
op_id=op_id,
835+
u_id=user.id,
836+
view_name=view_name,
837+
shared_data=shared_data
838+
)
839+
db.session.add(view_setting)
840+
db.session.commit()
841+
return True, "View settings shared successfully"
842+
except Exception as e:
843+
logging.error("Error sharing view settings for user %s: %s", user.id, str(e))
844+
return False, f"Failed to share view settings: {str(e)}"
845+
846+
def get_shared_views(self, op_id, view_name, user):
847+
"""Retrieve shared view settings for a user from the database."""
848+
if not self.is_member(user.id, op_id):
849+
return False, "Access denied", {}
850+
if view_name:
851+
shared_view = SharedView.query.filter_by(op_id=op_id, view_name=view_name).first()
852+
if not shared_view:
853+
return False, "No shared view found with that ID", {}
854+
try:
855+
setting = json.loads(shared_view.shared_data)
856+
if not isinstance(setting, dict):
857+
return False, f"Invalid settings type after parsing: {type(shared_view).__name__}", {}
858+
return True, "View settings shared successfully", setting
859+
except Exception as e:
860+
return False, f"Error parsing settings: {str(e)}", {}
861+
783862
def save_view_settings(self, op_id, user, view_settings):
784863
"""Save view settings for an operation and user to the database."""
785864
if not self.is_member(user.id, op_id) and self.is_viewer(user.id, op_id):
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""new_models
2+
3+
Revision ID: 2daa5c5142a1
4+
Revises: 922e4d9c94e2
5+
Create Date: 2025-09-27 16:29:39.069372
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import mslib.mscolab.custom_migration_types as cu
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '2daa5c5142a1'
15+
down_revision = '922e4d9c94e2'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
# ### commands auto generated by Alembic - please adjust! ###
22+
op.create_table('manageviews',
23+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
24+
sa.Column('op_id', sa.Integer(), nullable=False),
25+
sa.Column('u_id', sa.Integer(), nullable=False),
26+
sa.Column('view_name', sa.String(length=255), nullable=False),
27+
sa.Column('created_at', cu.AwareDateTime(), nullable=True),
28+
sa.ForeignKeyConstraint(['op_id'], ['operations.id'], name=op.f('fk_manageviews_op_id_operations')),
29+
sa.ForeignKeyConstraint(['u_id'], ['users.id'], name=op.f('fk_manageviews_u_id_users')),
30+
sa.PrimaryKeyConstraint('id', name=op.f('pk_manageviews'))
31+
)
32+
op.create_table('sharedviews',
33+
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
34+
sa.Column('op_id', sa.Integer(), nullable=False),
35+
sa.Column('u_id', sa.Integer(), nullable=False),
36+
sa.Column('view_name', sa.String(length=255), nullable=False),
37+
sa.Column('shared_data', sa.JSON(), nullable=False),
38+
sa.Column('created_at', cu.AwareDateTime(), nullable=True),
39+
sa.ForeignKeyConstraint(['op_id'], ['operations.id'], name=op.f('fk_sharedviews_op_id_operations')),
40+
sa.ForeignKeyConstraint(['u_id'], ['users.id'], name=op.f('fk_sharedviews_u_id_users')),
41+
sa.PrimaryKeyConstraint('id', name=op.f('pk_sharedviews')),
42+
sa.UniqueConstraint('op_id', 'view_name', name='_op_view_uc')
43+
)
44+
op.create_table('viewsettings',
45+
sa.Column('op_id', sa.Integer(), nullable=False),
46+
sa.Column('u_id', sa.Integer(), nullable=False),
47+
sa.Column('settings', sa.JSON(), nullable=True),
48+
sa.Column('created_at', cu.AwareDateTime(), nullable=True),
49+
sa.Column('updated_at', cu.AwareDateTime(), nullable=True),
50+
sa.ForeignKeyConstraint(['op_id'], ['operations.id'], name=op.f('fk_viewsettings_op_id_operations')),
51+
sa.ForeignKeyConstraint(['u_id'], ['users.id'], name=op.f('fk_viewsettings_u_id_users')),
52+
sa.PrimaryKeyConstraint('op_id', 'u_id', name=op.f('pk_viewsettings')),
53+
sa.UniqueConstraint('u_id', 'op_id', name='u_id_op_id')
54+
)
55+
# ### end Alembic commands ###
56+
57+
58+
def downgrade():
59+
# ### commands auto generated by Alembic - please adjust! ###
60+
op.drop_table('viewsettings')
61+
op.drop_table('sharedviews')
62+
op.drop_table('manageviews')
63+
# ### end Alembic commands ###

mslib/mscolab/migrations/versions/4ad0fd989443_viewsettings.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

mslib/mscolab/models.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,38 @@ def __init__(self, op_id, u_id, settings):
242242
self.op_id = int(op_id)
243243
self.u_id = int(u_id)
244244
self.settings = settings
245+
246+
247+
class SharedView(db.Model):
248+
249+
__tablename__ = "sharedviews"
250+
id = db.Column(db.Integer, primary_key=True, autoincrement=True) # noqa: A003
251+
op_id = db.Column(db.Integer, db.ForeignKey('operations.id'), nullable=False)
252+
u_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
253+
view_name = db.Column(db.String(255), nullable=False)
254+
shared_data = db.Column(db.JSON, nullable=False)
255+
created_at = db.Column(AwareDateTime, default=lambda: datetime.datetime.now(tz=datetime.timezone.utc))
256+
user = db.relationship('User', backref='shared_views')
257+
__table_args__ = (db.UniqueConstraint('op_id', 'view_name', name='_op_view_uc'),)
258+
259+
def __init__(self, op_id, view_name, shared_data, u_id):
260+
self.op_id = int(op_id)
261+
self.u_id = int(u_id)
262+
self.view_name = str(view_name)
263+
self.shared_data = shared_data
264+
265+
266+
class ManageViews(db.Model):
267+
268+
__tablename__ = "manageviews"
269+
id = db.Column(db.Integer, primary_key=True, autoincrement=True) # noqa: A003
270+
op_id = db.Column(db.Integer, db.ForeignKey('operations.id'), nullable=False)
271+
u_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
272+
view_name = db.Column(db.String(255), nullable=False)
273+
created_at = db.Column(AwareDateTime, default=lambda: datetime.datetime.now(tz=datetime.timezone.utc))
274+
user = db.relationship('User', backref='manage_views')
275+
276+
def __init__(self, op_id, u_id, view_name):
277+
self.op_id = int(op_id)
278+
self.u_id = int(u_id)
279+
self.view_name = str(view_name)

mslib/mscolab/server.py

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,97 @@ def reset_request():
873873
return render_template('errors/403.html'), 403
874874

875875

876+
@APP.route("/share_view", methods=["POST"])
877+
@verify_user
878+
def share_view():
879+
try:
880+
view_settings = request.form.get('view_settings', False)
881+
view_name = request.form.get('view_name', None)
882+
op_id = request.form.get('op_id')
883+
user = g.user
884+
if not all([view_settings, view_name, op_id]):
885+
return jsonify({"success": False, "message": "Missing required parameters"}), 400
886+
if isinstance(view_settings, str):
887+
settings_data = json.loads(view_settings)
888+
else:
889+
settings_data = view_settings
890+
success, message = fm.share_view(op_id, view_name, user, settings_data)
891+
if success:
892+
return jsonify({"success": True, "message": message}), 200
893+
else:
894+
return jsonify({"success": False, "message": message}), 400
895+
except json.JSONDecodeError as e:
896+
logging.error("Invalid view_settings JSON: %s", str(e))
897+
return jsonify({"success": False, "message": "Invalid view_settings format"}), 400
898+
except Exception as e:
899+
logging.error("Error in share_view endpoint: %s", str(e))
900+
return jsonify({"success": False, "message": f"Server error: {str(e)}"}), 500
901+
902+
903+
@APP.route("/get_shared_view", methods=["GET"])
904+
@verify_user
905+
def get_shared_view():
906+
op_id = request.form.get('op_id')
907+
view_name = request.form.get('view_name')
908+
user = g.user
909+
try:
910+
success, message, settings = fm.get_shared_views(op_id, view_name, user)
911+
if success:
912+
return jsonify({"success": True, "message": message, "settings": settings}), 200
913+
else:
914+
return jsonify({"success": False, "message": message, "settings": settings}), 400
915+
except Exception as e:
916+
logging.error("Error in get_shared_view endpoint: %s", str(e))
917+
return jsonify({"success": False, "message": f"Server error: {str(e)}", "settings": {}}), 500
918+
919+
920+
@APP.route("/get_views_name", methods=["GET"])
921+
@verify_user
922+
def get_views_name():
923+
op_id = request.args.get('op_id')
924+
user = g.user
925+
try:
926+
success, message, views = fm.list_views(op_id, user)
927+
if success:
928+
return jsonify({"success": True, "message": message, "views": views}), 200
929+
else:
930+
return jsonify({"success": False, "message": message, "views": views}), 400
931+
except Exception as e:
932+
logging.error("Error listing views: %s", str(e))
933+
return jsonify({"success": False, "message": f"Error listing views: {str(e)}", "views": []}), 500
934+
935+
936+
@APP.route("/manage_sharedView_metadata", methods=["POST"])
937+
@verify_user
938+
def manage_sharedView_metadata():
939+
view_name = request.form.get('view_name', None)
940+
op_id = request.form.get('op_id', None)
941+
user = g.user
942+
if not all([view_name, op_id]):
943+
return jsonify({"success": False, "message": "Missing required parameters"}), 400
944+
success, message = fm.save_manage_view_metadata(user, op_id, view_name)
945+
if success:
946+
return jsonify({"success": True, "message": message}), 200
947+
else:
948+
return jsonify({"success": False, "message": message}), 400
949+
950+
951+
@APP.route("/check_view_names", methods=["GET"])
952+
@verify_user
953+
def check_view_names():
954+
op_id = request.form.get('op_id')
955+
view_name = request.form.get('view_name')
956+
user = g.user
957+
if not all([op_id is not None, view_name is not None]):
958+
return jsonify({"success": False, "message": "Missing required parameters", "exists": False}), 400
959+
try:
960+
exists = fm.check_view_name(user, op_id, view_name)
961+
return jsonify({"success": True, "exists": exists}), 200
962+
except Exception as e:
963+
logging.error("Error checking view name: %s", str(e))
964+
return jsonify({"success": False, "message": f"Error checking view name: {str(e)}", "exists": False}), 400
965+
966+
876967
@APP.route("/save_operation_view_settings", methods=["POST"])
877968
@verify_user
878969
def save_operation_view_settings():
@@ -896,7 +987,6 @@ def save_operation_view_settings():
896987
logging.error("Missing operation ID in payload")
897988
return jsonify({"success": False, "message": "Missing operation ID"}), 400
898989

899-
user = g.user
900990
success, message = fm.save_view_settings(op_id, user, settings_data)
901991
if success:
902992
return jsonify({"success": True, "message": message}), 200

0 commit comments

Comments
 (0)