Commit e283b5d6 authored by Florent Chehab's avatar Florent Chehab

fix(nb_versions): coherence

* Added signal to catch version deletion and make sure nb_version in versioned models is coherent
* Added test for this
* Made sure pending moderation data (that was automatic) and versions are deleted when a model is deleted.
* Added test for this.

Fixes #84
parent bf0b3db5
Pipeline #37122 passed with stages
in 5 minutes and 7 seconds
......@@ -7,3 +7,4 @@ class BackendAppConfig(AppConfig):
def ready(self):
import backend_app.signals.squash_revisions # noqa:F401
import backend_app.signals.auto_creation # noqa:F401
import backend_app.signals.update_nb_version # noqa:F401
......@@ -8,6 +8,7 @@ from backend_app.models.abstract.essentialModule import (
)
from backend_app.signals.squash_revisions import new_revision_saved
from rest_framework import serializers
from reversion.models import Version
class VersionedEssentialModule(EssentialModule):
......@@ -27,6 +28,16 @@ class VersionedEssentialModule(EssentialModule):
"""
raise Exception("Get_serializer must be redefined in subclass")
def delete(self, using=None, keep_parents=False):
"""
Override the default delete behavior to make sure
versions instances corresponding to the
deleted instance are also deleted.
"""
# We need to delete the versions first. Otherwise for some reason it wouldn't work.
Version.objects.get_for_object(self).delete()
super().delete(using, keep_parents)
class Meta:
abstract = True
......
......@@ -32,9 +32,10 @@ class VersionSerializer(BaseModelSerializer):
return obj_serializer(tmp.object, context=new_context).data
except (DeserializationError, djangoSerializers.SerializerDoesNotExist):
# The version is not valid regarding the model, we should delete it !
# This might be due to an updated model.
# This might be due to an updated model structure at some point.
obj.delete()
# Might result in inconsistent nb of versions but that's fine.
# We take care of the nb_versions field update with signals.
# So it will remain coherent.
return None
class Meta:
......
from reversion.models import Version
from django.db.models.signals import post_delete
def update_nb_version_on_version_delete(sender, instance, **kwargs):
pk = instance.object_id
ct = instance.content_type
obj = ct.get_object_for_this_type(pk=pk)
# Make sure we have a new coherent value
obj.nb_versions = len(Version.objects.get_for_object(obj))
obj.save()
post_delete.connect(update_nb_version_on_version_delete, sender=Version)
from django.test import override_settings
from reversion.models import Version
from backend_app.models.for_testing.versioning import ForTestingVersioning
from backend_app.tests.utils import WithUserTestCase
from backend_app.models.for_testing.moderation import ForTestingModeration
from backend_app.models.pendingModeration import PendingModeration
from django.contrib.contenttypes.models import ContentType
def retrieve_instance_in_moderation(instance, should_be_empty=False):
ct = ContentType.objects.get_for_model(ForTestingModeration)
pending = PendingModeration.objects.filter(content_type=ct, object_id=instance.pk)
if should_be_empty:
assert len(pending) == 0
return None
else:
assert len(pending) > 0
return pending[0]
def get_object_moder(pk):
return ForTestingModeration.objects.filter(pk=pk)[0]
def get_object_versions(obj):
return Version.objects.get_for_object(obj)
class CascadeDeleteModerationTestCase(WithUserTestCase):
@classmethod
def setUpMoreTestData(cls):
cls.obj = ForTestingModeration.objects.create(aaa="v0")
cls.api_moderation = "/api/test/moderation/{}/".format(cls.obj.pk)
@override_settings(MODERATION_ACTIVATED=True)
def test_delete_model_cascade_to_pending_moderation(self):
"""
Test to check that when a model instance is deleted, all pending moderation
data related to that instance is also deleted.
"""
data_1 = {"aaa": "Test"}
self.authenticated_client.put(self.api_moderation, data_1)
new_obj_in_db = get_object_moder(self.obj.pk)
self.assertTrue(new_obj_in_db.has_pending_moderation)
retrieve_instance_in_moderation(new_obj_in_db, should_be_empty=False)
new_obj_in_db.delete()
retrieve_instance_in_moderation(new_obj_in_db, should_be_empty=True)
# Really make sure there is nothing pending moderation
self.assertEqual(len(PendingModeration.objects.all()), 0)
class CascadeDeleteVersioningTestCase(WithUserTestCase):
@classmethod
def setUpMoreTestData(cls):
cls.obj = ForTestingVersioning.objects.create(bbb="v0")
cls.api_version = "/api/test/versioning/{}/".format(cls.obj.pk)
def test_delete_model_cascade_to_versions(self):
"""
Test to check that when a versioned model instance is deleted, all
other versions of this model is also deleted.
"""
data_1 = {"bbb": "Test"}
self.staff_client.put(self.api_version, data_1)
data_2 = {"bbb": "Test"}
self.moderator_client.put(self.api_version, data_2)
self.assertEqual(len(get_object_versions(self.obj)), 2)
self.obj.delete()
self.assertEqual(len(get_object_versions(self.obj)), 0)
# Really make sure all versions have been deleted
self.assertEqual(len(Version.objects.all()), 0)
from reversion.models import Version
from backend_app.models.for_testing.versioning import ForTestingVersioning
from backend_app.tests.utils import WithUserTestCase
def get_object_ver(obj):
return ForTestingVersioning.objects.filter(pk=obj.pk)[0]
def get_object_versions(obj):
return Version.objects.get_for_object(obj)
class CascadeDeleteVersionsTestCase(WithUserTestCase):
@classmethod
def setUpMoreTestData(cls):
cls.obj = ForTestingVersioning.objects.create(bbb="v0")
cls.api_version = "/api/test/versioning/{}/".format(cls.obj.pk)
def test_delete_model_cascade_to_versions(self):
"""
Test to check that when a versioned model instance is deleted, all
other versions of this model is also deleted.
"""
data_1 = {"bbb": "Test"}
self.staff_client.put(self.api_version, data_1)
# Make sure we have a version
versions = get_object_versions(self.obj)
self.assertEqual(len(versions), 1)
# Make sure we have the correct version number
new_obj = get_object_ver(self.obj)
self.assertEqual(new_obj.nb_versions, 1)
versions.delete()
# Make sure they really have been deleted
versions = get_object_versions(self.obj)
self.assertEqual(len(versions), 0)
# Re-get the object, to make sure we have updated fields
new_obj = get_object_ver(self.obj)
# Final test
self.assertEqual(new_obj.nb_versions, 0)
......@@ -6,5 +6,11 @@ ALLOWED_PHOTOS_EXTENSION = ["jpg", "jpeg", "png", "svg"]
# Configuration of the CAS server
CAS_SERVER_URL = "https://cas.utc.fr/cas/"
CAS_APPLY_ATTRIBUTES_TO_USER = True # We want to map the attribute returned by the cas to the user model attribute
CAS_RENAME_ATTRIBUTES = {"mail": "email", "givenName": "first_name", "sn": "last_name"} # Mapping of the attribute
# We want to map the attribute returned by the cas to the user model attribute
CAS_APPLY_ATTRIBUTES_TO_USER = True
# Mapping of the attribute
CAS_RENAME_ATTRIBUTES = {
"mail": "email",
"givenName": "first_name",
"sn": "last_name",
}
......@@ -170,7 +170,7 @@ AUTHENTICATION_BACKENDS = [
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation."
"UserAttributeSimilarityValidator"
"UserAttributeSimilarityValidator"
},
{"NAME": "django.contrib.auth.password_validation." "MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation." "CommonPasswordValidator"},
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment