From cbebba6614d0d6c0a78d6d479e0a6b2e6d5ebeda Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Sat, 17 Oct 2026 22:57:26 +0800 Subject: [PATCH 1/3] Limit contest dump scope Prevent exporting shared users and teams from pulling in participations from unrelated contests. The importer now traverses only objects rooted in selected contests, filtering unrelated references from older dumps. --- cmscontrib/DumpExporter.py | 7 ++++--- cmscontrib/DumpImporter.py | 28 +++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/cmscontrib/DumpExporter.py b/cmscontrib/DumpExporter.py index d6a06790c7..667af71878 100755 --- a/cmscontrib/DumpExporter.py +++ b/cmscontrib/DumpExporter.py @@ -183,9 +183,6 @@ def __init__( ) self.tasks_ids = [task.id for task in tasks] else: - # FIXME: this is ATM broken, because if you export a contest, you - # then export the users who participated in it and then all of the - # contests those users participated in. self.contests_ids = contest_ids self.users_ids = [] self.tasks_ids = [] @@ -376,6 +373,10 @@ class of the given object), an item for each column property elif isinstance(val, other_cls): data[prp.key] = self.get_id(val) elif isinstance(val, list): + # Shared users and teams must not pull in other contests. + if other_cls is Participation: + val = [i for i in val + if i.contest_id in self.contests_ids] data[prp.key] = list(self.get_id(i) for i in val) elif isinstance(val, dict): data[prp.key] = \ diff --git a/cmscontrib/DumpImporter.py b/cmscontrib/DumpImporter.py index 1096ef56b1..f2f9a47f7d 100755 --- a/cmscontrib/DumpImporter.py +++ b/cmscontrib/DumpImporter.py @@ -262,9 +262,35 @@ def do_import(self): assert self.datas["_version"] == model_version + # Only root contests were selected for export; shared users + # and teams can also reference unrelated contests in old dumps. + contest_keys = {key for key in self.datas["_objects"] + if self.datas[key]["_class"] == "Contest"} + import_keys = set() + queue = list(self.datas["_objects"]) + while queue: + key = queue.pop() + if key in import_keys: + continue + data = self.datas[key] + if ((data["_class"] == "Contest" and key not in contest_keys) + or (data.get("contest") is not None + and data["contest"] not in contest_keys)): + continue + import_keys.add(key) + cls = getattr(class_hook, data["_class"]) + for prp in cls._rel_props: + references = data.get(prp.key) + if isinstance(references, str): + queue.append(references) + elif isinstance(references, list): + queue.extend(references) + elif isinstance(references, dict): + queue.extend(references.values()) + self.objs = dict() for id_, data in self.datas.items(): - if not id_.startswith("_"): + if id_ in import_keys: self.objs[id_] = self.import_object(data) for k, v in list(self.objs.items()): From bb41404fc8cee9d759fa8c5e396a31313220d595 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Sat, 17 Oct 2026 23:15:18 +0800 Subject: [PATCH 2/3] Preserve existing admin credentials on import Reuse destination administrators matched by username during dump import so their local credentials and permissions are preserved. --- cmscontrib/DumpImporter.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmscontrib/DumpImporter.py b/cmscontrib/DumpImporter.py index f2f9a47f7d..a1cb1789dc 100755 --- a/cmscontrib/DumpImporter.py +++ b/cmscontrib/DumpImporter.py @@ -64,6 +64,7 @@ FilenameSchemaArray, Digest, SessionGen, + Admin, Contest, Submission, SubmissionResult, @@ -291,6 +292,15 @@ def do_import(self): self.objs = dict() for id_, data in self.datas.items(): if id_ in import_keys: + # Keep destination credentials and permissions for shared admins. + if data["_class"] == "Admin": + admin = session.query(Admin).filter( + Admin.username == data["username"]).one_or_none() + if admin is not None: + self.objs[id_] = admin + logger.info("Reusing existing administrator %s.", + admin.username) + continue self.objs[id_] = self.import_object(data) for k, v in list(self.objs.items()): From 0ef4405ae261ac5a341928440e0503b0bc1e5a75 Mon Sep 17 00:00:00 2001 From: Pasit Sangprachathanarak Date: Sat, 17 Oct 2026 23:26:51 +0800 Subject: [PATCH 3/3] Update DumpImporter.py --- cmscontrib/DumpImporter.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/cmscontrib/DumpImporter.py b/cmscontrib/DumpImporter.py index a1cb1789dc..8566dd63eb 100755 --- a/cmscontrib/DumpImporter.py +++ b/cmscontrib/DumpImporter.py @@ -65,6 +65,7 @@ Digest, SessionGen, Admin, + Team, Contest, Submission, SubmissionResult, @@ -290,16 +291,21 @@ def do_import(self): queue.extend(references.values()) self.objs = dict() + reused_keys = set() + shared_classes = {"Admin": (Admin, "username"), + "User": (User, "username"), + "Team": (Team, "code")} for id_, data in self.datas.items(): if id_ in import_keys: - # Keep destination credentials and permissions for shared admins. - if data["_class"] == "Admin": - admin = session.query(Admin).filter( - Admin.username == data["username"]).one_or_none() - if admin is not None: - self.objs[id_] = admin - logger.info("Reusing existing administrator %s.", - admin.username) + if data["_class"] in shared_classes: + cls, field = shared_classes[data["_class"]] + existing = session.query(cls).filter( + getattr(cls, field) == data[field]).one_or_none() + if existing is not None: + self.objs[id_] = existing + reused_keys.add(id_) + logger.info("Reusing existing %s %s.", + data["_class"], data[field]) continue self.objs[id_] = self.import_object(data) @@ -325,7 +331,8 @@ def do_import(self): del self.objs[k] for id_, data in self.datas.items(): - if not id_.startswith("_") and id_ in self.objs: + # Preserve existing memberships, credentials and permissions. + if id_ in self.objs and id_ not in reused_keys: self.add_relationships(data, self.objs[id_]) contest_id = list()