From 4fb9425e9d99f897e7ebcea727dd92cbd9a93ab4 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 20 Oct 2023 14:03:06 +0200 Subject: [PATCH 1/5] Add replaced_by to the deletion log This allows workouts and their logs to be updated with an alternative when an exercise is deleted, instead of just disappearing --- wger/exercises/api/serializers.py | 1 + wger/exercises/fixtures/test-exercises.json | 2 +- .../management/commands/sync-exercises.py | 4 +- .../0026_deletionlog_replaced_by.py | 23 ++++++++ wger/exercises/models/deletion_log.py | 10 ++++ wger/exercises/sync.py | 45 ++++++++++++++-- wger/exercises/tasks.py | 4 +- wger/exercises/tests/test_sync.py | 52 +++++++++++++++---- 8 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 wger/exercises/migrations/0026_deletionlog_replaced_by.py diff --git a/wger/exercises/api/serializers.py b/wger/exercises/api/serializers.py index 50bf816c2..57cb3456c 100644 --- a/wger/exercises/api/serializers.py +++ b/wger/exercises/api/serializers.py @@ -78,6 +78,7 @@ class Meta: fields = [ 'model_type', 'uuid', + 'replaced_by', 'timestamp', 'comment', ] diff --git a/wger/exercises/fixtures/test-exercises.json b/wger/exercises/fixtures/test-exercises.json index 96fcd69f1..798edc8ab 100644 --- a/wger/exercises/fixtures/test-exercises.json +++ b/wger/exercises/fixtures/test-exercises.json @@ -166,7 +166,7 @@ "pk": 3, "model": "exercises.exercise", "fields": { - "uuid": "946afe7b54a644a69c36c3e31e6b4c3b", + "uuid": "946afe7b-54a6-44a6-9c36-c3e31e6b4c3b", "language": 2, "exercise_base": 3, "description": "", diff --git a/wger/exercises/management/commands/sync-exercises.py b/wger/exercises/management/commands/sync-exercises.py index b72023ef8..6d87c314e 100644 --- a/wger/exercises/management/commands/sync-exercises.py +++ b/wger/exercises/management/commands/sync-exercises.py @@ -23,7 +23,7 @@ # wger from wger.exercises.sync import ( - delete_entries, + handle_deleted_entries, sync_categories, sync_equipment, sync_exercises, @@ -88,4 +88,4 @@ def handle(self, **options): sync_licenses(self.stdout.write, self.remote_url, self.style.SUCCESS) sync_exercises(self.stdout.write, self.remote_url, self.style.SUCCESS) if not options['skip_delete']: - delete_entries(self.stdout.write, self.remote_url, self.style.SUCCESS) + handle_deleted_entries(self.stdout.write, self.remote_url, self.style.SUCCESS) diff --git a/wger/exercises/migrations/0026_deletionlog_replaced_by.py b/wger/exercises/migrations/0026_deletionlog_replaced_by.py new file mode 100644 index 000000000..a31b23c54 --- /dev/null +++ b/wger/exercises/migrations/0026_deletionlog_replaced_by.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.9 on 2023-10-19 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('exercises', '0025_rename_update_date_exercise_last_update_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='deletionlog', + name='replaced_by', + field=models.UUIDField( + default=None, + editable=False, + help_text='UUID of the object ', + null=True, + verbose_name='Replaced by' + ), + ), + ] diff --git a/wger/exercises/models/deletion_log.py b/wger/exercises/models/deletion_log.py index b97f89717..8a2dc9f09 100644 --- a/wger/exercises/models/deletion_log.py +++ b/wger/exercises/models/deletion_log.py @@ -54,6 +54,16 @@ class DeletionLog(models.Model): verbose_name='UUID', ) + replaced_by = models.UUIDField( + default=None, + unique=False, + editable=False, + null=True, + verbose_name='Replaced by', + help_text='UUID of the object replaced by the deleted one. At the moment only available ' + 'for exercise bases', + ) + timestamp = models.DateTimeField(auto_now=True) comment = models.CharField(max_length=200, default='') diff --git a/wger/exercises/sync.py b/wger/exercises/sync.py index b19bb597f..7838294a6 100644 --- a/wger/exercises/sync.py +++ b/wger/exercises/sync.py @@ -53,6 +53,10 @@ ExerciseVideo, Muscle, ) +from wger.manager.models import ( + Setting, + WorkoutLog, +) from wger.utils.requests import ( get_paginated, wger_headers, @@ -75,6 +79,7 @@ def sync_exercises( for data in result: uuid = data['uuid'] + created = data['created'] license_id = data['license']['id'] category_id = data['category']['id'] license_author = data['license_author'] @@ -84,7 +89,10 @@ def sync_exercises( base, base_created = ExerciseBase.objects.update_or_create( uuid=uuid, - defaults={'category_id': category_id}, + defaults={ + 'category_id': category_id, + 'created': created + }, ) print_fn(f"{'created' if base_created else 'updated'} exercise {uuid}") @@ -272,13 +280,17 @@ def sync_equipment( print_fn(style_fn('done!\n')) -def delete_entries( - print_fn, +def handle_deleted_entries( + print_fn=None, remote_url=settings.WGER_SETTINGS['WGER_INSTANCE'], style_fn=lambda x: x, ): + if not print_fn: + def print_fn(_): + return None + """Delete exercises that were removed on the server""" - print_fn('*** Deleting exercises data that was removed on the server...') + print_fn('*** Deleting exercise data that was removed on the server...') headers = wger_headers() url = make_uri(DELETION_LOG_ENDPOINT, server_url=remote_url, query={'limit': 100}) @@ -286,13 +298,38 @@ def delete_entries( for data in result: uuid = data['uuid'] + replaced_by_uuid = data['replaced_by'] model_type = data['model_type'] if model_type == DeletionLog.MODEL_BASE: + obj_replaced = None + nr_settings = None + nr_logs = None + try: + obj_replaced = ExerciseBase.objects.get(uuid=replaced_by_uuid) + except ExerciseBase.DoesNotExist: + pass + try: obj = ExerciseBase.objects.get(uuid=uuid) + + # Replace exercise in workouts and logs + if obj_replaced: + nr_settings = ( + Setting.objects.filter(exercise_base=obj + ).update(exercise_base=obj_replaced) + ) + nr_logs = ( + WorkoutLog.objects.filter(exercise_base=obj + ).update(exercise_base=obj_replaced) + ) + obj.delete() print_fn(f'Deleted exercise base {uuid}') + if nr_settings: + print_fn(f'- replaced {nr_settings} time(s) in workouts by {replaced_by_uuid}') + if nr_logs: + print_fn(f'- replaced {nr_logs} time(s) in workout logs by {replaced_by_uuid}') except ExerciseBase.DoesNotExist: pass diff --git a/wger/exercises/tasks.py b/wger/exercises/tasks.py index 757bacb55..3a46af9cc 100644 --- a/wger/exercises/tasks.py +++ b/wger/exercises/tasks.py @@ -25,9 +25,9 @@ # wger from wger.celery_configuration import app from wger.exercises.sync import ( - delete_entries, download_exercise_images, download_exercise_videos, + handle_deleted_entries, sync_categories, sync_equipment, sync_exercises, @@ -51,7 +51,7 @@ def sync_exercises_task(): sync_muscles(logger.info) sync_equipment(logger.info) sync_exercises(logger.info) - delete_entries(logger.info) + handle_deleted_entries(logger.info) @app.task diff --git a/wger/exercises/tests/test_sync.py b/wger/exercises/tests/test_sync.py index 2f991f3cc..4591c1d7f 100644 --- a/wger/exercises/tests/test_sync.py +++ b/wger/exercises/tests/test_sync.py @@ -29,7 +29,7 @@ Muscle, ) from wger.exercises.sync import ( - delete_entries, + handle_deleted_entries, sync_categories, sync_equipment, sync_exercises, @@ -37,6 +37,10 @@ sync_licenses, sync_muscles, ) +from wger.manager.models import ( + Setting, + WorkoutLog, +) from wger.utils.requests import wger_headers @@ -272,43 +276,50 @@ def json(): "results": [ { "model_type": "base", - "uuid": "acad394936fb44819a72be2ddae2bc05", + "uuid": "acad3949-36fb-4481-9a72-be2ddae2bc05", + "replaced_by": "ae3328ba-9a35-4731-bc23-5da50720c5aa", "timestamp": "2023-01-30T19:32:56.761426+01:00", "comment": "Exercise base with ID 1" }, { "model_type": "base", "uuid": "577ee012-70c6-4517-b0fe-dcf340926ae7", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.761426+01:00", "comment": "Unknown Exercise base" }, { "model_type": "translation", "uuid": "0ef4cec8-d9c9-464f-baf9-3dbdf20083cb", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.764582+01:00", "comment": "Translation that is not in this DB" }, { "model_type": "translation", - "uuid": "946afe7b54a644a69c36c3e31e6b4c3b", + "uuid": "946afe7b-54a6-44a6-9c36-c3e31e6b4c3b", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.765350+01:00", "comment": "Translation with ID 3" }, { "model_type": "image", "uuid": "c72b4463-48ae-4c7d-8093-2c347c38e05a", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.765350+01:00", "comment": "Unknown image" }, { "model_type": "video", "uuid": "e0d25624-348b-4a23-adcd-17190f96f005", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.765350+01:00", "comment": "Unknown video" }, { "model_type": "foobar", - "uuid": "37b5813c76bd4658820a572d9dd93f31", + "uuid": "37b5813c-76bd-4658-820a-572d9dd93f31", + "replaced_by": None, "timestamp": "2023-01-30T19:32:56.765350+01:00", "comment": "UUID of existing translation but other model type" }, @@ -334,7 +345,9 @@ def json(): { "id": 123, "uuid": "1b020b3a-3732-4c7e-92fd-a0cec90ed69b", - "creation_date": "2022-10-11", + "created": "2022-10-11T19:45:01.914000+01:00", + "last_update": "2023-02-05T19:45:01.914000+01:00", + "last_update_global": "2023-02-05T19:45:01.914000+01:00", "category": { "id": 2, "name": "Another category" @@ -380,7 +393,7 @@ def json(): "name": "Zweihandiges Kettlebell", "exercise_base": 123, "description": "Hier könnte Ihre Werbung stehen!", - "creation_date": "2015-08-03", + "created": "2015-08-03", "language": 1, "aliases": [ "Kettlebell mit zwei Händen" @@ -406,7 +419,7 @@ def json(): "name": "2 Handed Kettlebell Swing", "exercise_base": 123, "description": "TBD", - "creation_date": "2023-08-03", + "created": "2023-08-03", "language": 2, "aliases": [], "notes": [], @@ -434,7 +447,9 @@ def json(): { "id": 2, "uuid": "ae3328ba-9a35-4731-bc23-5da50720c5aa", - "creation_date": "2022-10-11", + "created": "2022-10-11T13:11:01.779000+01:00", + "last_update": "2023-02-05T19:45:01.779000+01:00", + "last_update_global": "2023-08-15T23:33:11.779000+01:00", "category": { "id": 3, "name": "Yet another category" @@ -480,7 +495,7 @@ def json(): "name": "A new, better, updated name", "exercise_base": 2, "description": "Two Handed Russian Style Kettlebell swing", - "creation_date": "2015-08-03", + "created": "2015-08-03", "language": 1, "aliases": [ "A new alias here", @@ -505,7 +520,7 @@ def json(): "name": "Balançoire Kettlebell à 2 mains", "exercise_base": 2, "description": "Balançoire Kettlebell à deux mains de style russe", - "creation_date": "2015-08-03", + "created": "2015-08-03", "language": 3, "aliases": [], "notes": [], @@ -616,7 +631,16 @@ def test_equipment_sync(self, mock_request): def test_deletion_log(self, mock_request): self.assertEqual(ExerciseBase.objects.count(), 8) self.assertEqual(Exercise.objects.count(), 11) - delete_entries(lambda x: x) + + base = ExerciseBase.objects.get(pk=1) + base2 = ExerciseBase.objects.get(pk=3) + self.assertFalse(Setting.objects.filter(exercise_base=base2).count()) + self.assertFalse(WorkoutLog.objects.filter(exercise_base=base2).count()) + + settings = Setting.objects.filter(exercise_base=base) + logs = WorkoutLog.objects.filter(exercise_base=base) + + handle_deleted_entries(print) mock_request.assert_called_with( 'https://wger.de/api/v2/deletion-log/?limit=100', @@ -627,6 +651,12 @@ def test_deletion_log(self, mock_request): self.assertRaises(Exercise.DoesNotExist, Exercise.objects.get, pk=3) self.assertRaises(ExerciseBase.DoesNotExist, ExerciseBase.objects.get, pk=1) + # Workouts and logs have been moved + for setting_pk in settings: + self.assertEqual(Setting.objects.get(pk=setting_pk).exercise_base_id, 2) + for setting_pk in logs: + self.assertEqual(WorkoutLog.objects.get(pk=setting_pk).exercise_base_id, 2) + @patch('requests.get', return_value=MockExerciseResponse()) def test_exercise_sync(self, mock_request): self.assertEqual(ExerciseBase.objects.count(), 8) From 51996b0edf3148c95c38394a77d9833f2aba3c71 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 22 Oct 2023 17:49:35 +0200 Subject: [PATCH 2/5] Allow setting a replacement object when deleting exercise bases --- wger/exercises/fixtures/test-exercises.json | 2 +- wger/exercises/models/base.py | 16 +++++++ wger/exercises/signals.py | 15 +++---- wger/exercises/tests/test_deletion_log.py | 47 +++++++++++++++++++-- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/wger/exercises/fixtures/test-exercises.json b/wger/exercises/fixtures/test-exercises.json index 798edc8ab..5146c16e9 100644 --- a/wger/exercises/fixtures/test-exercises.json +++ b/wger/exercises/fixtures/test-exercises.json @@ -140,7 +140,7 @@ "pk": 1, "model": "exercises.exercise", "fields": { - "uuid": "9838235ce38f4ca6921e9d237d8e0813", + "uuid": "9838235c-e38f-4ca6-921e-9d237d8e0813", "language": 2, "exercise_base": 1, "description": "Lorem ipsum dolor sit amet", diff --git a/wger/exercises/models/base.py b/wger/exercises/models/base.py index d55b7b41b..a4cb936a6 100644 --- a/wger/exercises/models/base.py +++ b/wger/exercises/models/base.py @@ -234,3 +234,19 @@ def get_exercise(self, language: Optional[str] = None): exercise = self.exercises.filter(language__short_name=language).first() return exercise + + def delete(self, using=None, keep_parents=False, replace_by: str = None): + """ + Save entry to log + """ + # wger + from wger.exercises.models import DeletionLog + log = DeletionLog( + model_type=DeletionLog.MODEL_BASE, + uuid=self.uuid, + comment=f"Exercise base of {self.get_exercise(ENGLISH_SHORT_NAME).name}", + replaced_by=replace_by, + ) + log.save() + + return super().delete(using, keep_parents) diff --git a/wger/exercises/signals.py b/wger/exercises/signals.py index b0eaaecf9..63d175460 100644 --- a/wger/exercises/signals.py +++ b/wger/exercises/signals.py @@ -108,19 +108,18 @@ def delete_exercise_video_on_update(sender, instance: ExerciseVideo, **kwargs): path.unlink() -@receiver(pre_delete, sender=ExerciseBase) -def add_deletion_log_base(sender, instance: ExerciseBase, **kwargs): - log = DeletionLog( - model_type=DeletionLog.MODEL_BASE, - uuid=instance.uuid, - ) - log.save() +# Deletion log for exercise bases is handled in the model +# @receiver(pre_delete, sender=ExerciseBase) +# def add_deletion_log_base(sender, instance: ExerciseBase, **kwargs): +# pass @receiver(pre_delete, sender=Exercise) def add_deletion_log_translation(sender, instance: Exercise, **kwargs): log = DeletionLog( - model_type=DeletionLog.MODEL_TRANSLATION, uuid=instance.uuid, comment=instance.name + model_type=DeletionLog.MODEL_TRANSLATION, + uuid=instance.uuid, + comment=instance.name, ) log.save() diff --git a/wger/exercises/tests/test_deletion_log.py b/wger/exercises/tests/test_deletion_log.py index d5c0de7ce..c4639eed0 100644 --- a/wger/exercises/tests/test_deletion_log.py +++ b/wger/exercises/tests/test_deletion_log.py @@ -12,6 +12,9 @@ # # You should have received a copy of the GNU Affero General Public License +# Standard Library +from uuid import UUID + # wger from wger.core.tests.base_testcase import WgerTestCase from wger.exercises.models import ( @@ -36,14 +39,52 @@ def test_base(self): base.delete() # Base is deleted - count_base = DeletionLog.objects.filter(model_type=DeletionLog.MODEL_BASE, - uuid=base.uuid).count() - self.assertEqual(count_base, 1) + count_base_logs = DeletionLog.objects.filter( + model_type=DeletionLog.MODEL_BASE, uuid=base.uuid + ).count() + log = DeletionLog.objects.get(pk=1) + + self.assertEqual(count_base_logs, 1) + self.assertEqual(log.model_type, 'base') + self.assertEqual(log.uuid, base.uuid) + self.assertEqual(log.comment, 'Exercise base of An exercise') + self.assertEqual(log.replaced_by, None) # All translations are also deleted count = DeletionLog.objects.filter(model_type=DeletionLog.MODEL_TRANSLATION).count() self.assertEqual(count, 2) + # First translation + log2 = DeletionLog.objects.get(pk=4) + self.assertEqual(log2.model_type, 'translation') + self.assertEqual(log2.uuid, UUID('9838235c-e38f-4ca6-921e-9d237d8e0813')) + self.assertEqual(log2.comment, 'An exercise') + self.assertEqual(log2.replaced_by, None) + + # Second translation + log3 = DeletionLog.objects.get(pk=5) + self.assertEqual(log3.model_type, 'translation') + self.assertEqual(log3.uuid, UUID('13b532f9-d208-462e-a000-7b9982b2b53e')) + self.assertEqual(log3.comment, 'Test exercise 123') + self.assertEqual(log3.replaced_by, None) + + def test_base_with_replaced_by(self): + """ + Test that an entry is generated when a base is deleted and the replaced by is + set correctly + """ + self.assertEqual(DeletionLog.objects.all().count(), 0) + + base = ExerciseBase.objects.get(pk=1) + base.delete(replace_by="ae3328ba-9a35-4731-bc23-5da50720c5aa") + + # Base is deleted + log = DeletionLog.objects.get(pk=1) + + self.assertEqual(log.model_type, 'base') + self.assertEqual(log.uuid, base.uuid) + self.assertEqual(log.replaced_by, UUID('ae3328ba-9a35-4731-bc23-5da50720c5aa')) + def test_translation(self): """ Test that an entry is generated when a translation is deleted From 1d2260c15bacc58374b2a7b748e82fa0efc0e1f9 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 22 Oct 2023 17:49:54 +0200 Subject: [PATCH 3/5] Remove unused import --- wger/exercises/signals.py | 1 - 1 file changed, 1 deletion(-) diff --git a/wger/exercises/signals.py b/wger/exercises/signals.py index 63d175460..3762a5fda 100644 --- a/wger/exercises/signals.py +++ b/wger/exercises/signals.py @@ -34,7 +34,6 @@ from wger.exercises.models import ( DeletionLog, Exercise, - ExerciseBase, ExerciseImage, ExerciseVideo, ) From d8ef9f9134cce195ac6881b3f19c2076c00bf299 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Mon, 23 Oct 2023 21:10:14 +0200 Subject: [PATCH 4/5] Allow passing the replaced_by UUID over the API --- wger/exercises/api/views.py | 12 ++++++++++++ wger/exercises/models/base.py | 7 +++++++ wger/exercises/tests/test_deletion_log.py | 16 ++++++++++++++++ wger/exercises/tests/test_exercise_base.py | 19 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/wger/exercises/api/views.py b/wger/exercises/api/views.py index 724281dd5..3cdff5f12 100644 --- a/wger/exercises/api/views.py +++ b/wger/exercises/api/views.py @@ -17,6 +17,7 @@ # Standard Library import logging +from uuid import UUID # Django from django.conf import settings @@ -133,6 +134,17 @@ def perform_update(self, serializer): action_object=serializer.instance, ) + def perform_destroy(self, instance: ExerciseBase): + """Manually delete the exercise and set the replacement, if any""" + + uuid = self.request.query_params.get('replaced_by', '') + try: + UUID(uuid, version=4) + except ValueError: + uuid = None + + instance.delete(replace_by=uuid) + class ExerciseTranslationViewSet(ModelViewSet): """ diff --git a/wger/exercises/models/base.py b/wger/exercises/models/base.py index a4cb936a6..47bbadee4 100644 --- a/wger/exercises/models/base.py +++ b/wger/exercises/models/base.py @@ -241,6 +241,13 @@ def delete(self, using=None, keep_parents=False, replace_by: str = None): """ # wger from wger.exercises.models import DeletionLog + + if replace_by: + try: + ExerciseBase.objects.get(uuid=replace_by) + except ExerciseBase.DoesNotExist: + replace_by = None + log = DeletionLog( model_type=DeletionLog.MODEL_BASE, uuid=self.uuid, diff --git a/wger/exercises/tests/test_deletion_log.py b/wger/exercises/tests/test_deletion_log.py index c4639eed0..0dd555ede 100644 --- a/wger/exercises/tests/test_deletion_log.py +++ b/wger/exercises/tests/test_deletion_log.py @@ -85,6 +85,22 @@ def test_base_with_replaced_by(self): self.assertEqual(log.uuid, base.uuid) self.assertEqual(log.replaced_by, UUID('ae3328ba-9a35-4731-bc23-5da50720c5aa')) + def test_base_with_nonexistent_replaced_by(self): + """ + Test that an entry is generated when a base is deleted and the replaced by is + set correctly. If the UUID is not found in the DB, it's set to None + """ + self.assertEqual(DeletionLog.objects.all().count(), 0) + + base = ExerciseBase.objects.get(pk=1) + base.delete(replace_by="12345678-1234-1234-1234-1234567890ab") + + # Base is deleted + log = DeletionLog.objects.get(pk=1) + + self.assertEqual(log.model_type, 'base') + self.assertEqual(log.replaced_by, None) + def test_translation(self): """ Test that an entry is generated when a translation is deleted diff --git a/wger/exercises/tests/test_exercise_base.py b/wger/exercises/tests/test_exercise_base.py index a0bb8eb9b..ab42a7507 100644 --- a/wger/exercises/tests/test_exercise_base.py +++ b/wger/exercises/tests/test_exercise_base.py @@ -12,6 +12,9 @@ # # You should have received a copy of the GNU Affero General Public License +# Standard Library +from uuid import UUID + # Third Party from rest_framework import status @@ -19,6 +22,7 @@ from wger.core.tests.api_base_test import ExerciseCrudApiTestCase from wger.core.tests.base_testcase import WgerTestCase from wger.exercises.models import ( + DeletionLog, Exercise, ExerciseBase, ) @@ -105,6 +109,21 @@ class ExerciseCustomApiTestCase(ExerciseCrudApiTestCase): def get_resource_name(self): return 'exercise-base' + def test_delete_replace_by(self): + """Test that setting the replaced_by attribute works""" + + self.authenticate('admin') + + url = self.url_detail + '?replaced_by=ae3328ba-9a35-4731-bc23-5da50720c5aa' + response = self.client.delete(url) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + log = DeletionLog.objects.get(pk=1) + + self.assertEqual(log.model_type, 'base') + self.assertEqual(log.uuid, UUID('acad3949-36fb-4481-9a72-be2ddae2bc05')) + self.assertEqual(log.replaced_by, UUID('ae3328ba-9a35-4731-bc23-5da50720c5aa')) + def test_cant_change_license(self): """ Test that it is not possible to change the license of an existing From dec695c1669a62bc825060c76955735262350e42 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 29 Oct 2023 12:13:37 +0100 Subject: [PATCH 5/5] Mock the correct function --- wger/exercises/tests/test_management_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wger/exercises/tests/test_management_commands.py b/wger/exercises/tests/test_management_commands.py index a9d76e7fb..1cb65cc33 100644 --- a/wger/exercises/tests/test_management_commands.py +++ b/wger/exercises/tests/test_management_commands.py @@ -29,7 +29,7 @@ class TestSyncManagementCommands(SimpleTestCase): - @patch('wger.exercises.sync.delete_entries') + @patch('wger.exercises.sync.handle_deleted_entries') @patch('wger.exercises.sync.sync_muscles') @patch('wger.exercises.sync.sync_languages') @patch('wger.exercises.sync.sync_exercises')