Skip to content

Commit 517fb82

Browse files
fix(medcat-trainer): Fix issue with meta-annotation tasks disappearing (#558)
* Add test for signals * Add fix for signals to allow keeping manually selected meta tasks * Add comment regarding multiple sources of meta-tasks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 322a827 commit 517fb82

2 files changed

Lines changed: 66 additions & 2 deletions

File tree

medcat-trainer/webapp/api/api/signals.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,31 @@ def project_tasks_changed(sender, instance, action, **kwargs):
107107
# post_remove or post_add actions, overwrite to model_pack supplied MetaCAT tasks.
108108
if (action.startswith('post') and isinstance(instance, ProjectAnnotateEntitiesFields) and
109109
instance.model_pack is not None):
110-
instance.tasks.set([MetaTask.objects.filter(prediction_model_id=meta_cat.id).first() for meta_cat in
111-
instance.model_pack.meta_cats.all()])
110+
# NOTE: This part deals with two different sources of information:
111+
# 1. sometimes the model pack associated with the project can have meta-cats for meta-annotations
112+
# 2. sometimes the project itself defines meta-tasks for the annotator to use
113+
#
114+
# Currently the proccess here defaults to useing model-pack defined meta-tasks (if present),
115+
# while allowing for the project-defined ones otherwise.
116+
117+
# Find automated tasks from the model pack
118+
db_tasks = [
119+
MetaTask.objects.filter(prediction_model_id=meta_cat.id).first()
120+
for meta_cat in instance.model_pack.meta_cats.all()
121+
]
122+
# Filter out None values
123+
automated_tasks = [t for t in db_tasks if t is not None]
124+
125+
# Only overwrite if the model pack actually brought automated tasks to the table.
126+
# This preserves manual workflows when training from scratch.
127+
if automated_tasks:
128+
# Disconnect the signal temporarily to prevent infinite recursion loops
129+
m2m_changed.disconnect(project_tasks_changed, sender=ProjectAnnotateEntitiesFields.tasks.through)
130+
try:
131+
instance.tasks.set(automated_tasks)
132+
finally:
133+
# Always reconnect the signal
134+
m2m_changed.connect(project_tasks_changed, sender=ProjectAnnotateEntitiesFields.tasks.through)
112135

113136

114137
m2m_changed.connect(project_tasks_changed, sender=ProjectAnnotateEntitiesFields.tasks.through)

medcat-trainer/webapp/api/api/tests/test_signals.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,44 @@ def test_syncs_tasks_from_model_pack_meta_cats(self):
170170
action='post_add',
171171
)
172172
self.assertIn(task, project.tasks.all())
173+
174+
def test_manual_tasks_persist_when_model_pack_has_no_meta_cats(self):
175+
# 1. Create a manual task not tied to any automated prediction model
176+
manual_task = MetaTask.objects.create(name='ManualPresenceTask')
177+
178+
# 2. Build a lightweight ModelPack that does NOT contain any meta_cats
179+
cdb = ConceptDB(name='manual-cdb', cdb_file='manual-cdb.dat')
180+
cdb.save(skip_load=True)
181+
vocab = Vocabulary(name='manual-vocab', vocab_file='manual-vocab.dat')
182+
vocab.save(skip_load=True)
183+
184+
mp = ModelPack(name='base-ner-pack', concept_db=cdb, vocab=vocab)
185+
mp.save(skip_load=True)
186+
187+
# Write a dummy zip file onto the filesystem so model_pack file validation passes
188+
pack_path = os.path.join(settings.MEDIA_ROOT, 'manual_base.zip')
189+
with open(pack_path, 'wb') as fh:
190+
fh.write(b'fake-zip')
191+
ModelPack.objects.filter(pk=mp.pk).update(model_pack='manual_base.zip')
192+
mp.refresh_from_db()
193+
194+
# 3. Create a project and attach this empty ModelPack
195+
project = create_basic_project(name='manual-tasks-proj')
196+
project.model_pack = mp
197+
project.concept_db = None
198+
project.vocab = None
199+
project.save()
200+
201+
# 4. Add the manual task to the project's many-to-many field
202+
project.tasks.add(manual_task)
203+
204+
# 5. Manually trigger the signal matching a Django admin 'post_add' save operation
205+
api_signals.project_tasks_changed(
206+
sender=ProjectAnnotateEntitiesFields.tasks.through,
207+
instance=project,
208+
action='post_add',
209+
)
210+
211+
# 6. Assert that our manual task survived the signal handler execution
212+
self.assertIn(manual_task, project.tasks.all())
213+
self.assertEqual(project.tasks.count(), 1)

0 commit comments

Comments
 (0)