Skip to content

Commit

Permalink
Propagate modify time from object to containers
Browse files Browse the repository at this point in the history
Modifications propagate from:
* data -> entity -> collection
* annotation value -> entity
* relation -> collection
  • Loading branch information
gregorjerse committed Dec 7, 2023
1 parent cbbad35 commit 3ccd69b
Show file tree
Hide file tree
Showing 10 changed files with 239 additions and 16 deletions.
3 changes: 3 additions & 0 deletions docs/CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Changed
authentication backend
- Use ``keys`` instead of ``scan_iter`` in ``clear`` method of ``redis`` cache,
since ``scan_iter`` is much slower
- Modified time propagates from ``Data`` object to ``Entity`` to ``Collection``
- Modified time propagates from ``AnnotationValue`` to ``Entity``
- Modified time propagates from ``Relation`` to ``Collection``


===================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.7 on 2023-12-06 11:59

from django.db import migrations
import resolwe.flow.models.base


class Migration(migrations.Migration):
dependencies = [
("flow", "0018_add_annotationvalue_default_order"),
]

operations = [
migrations.AlterField(
model_name="annotationpreset",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="collection",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="data",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="descriptorschema",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="entity",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="process",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="relation",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
migrations.AlterField(
model_name="storage",
name="modified",
field=resolwe.flow.models.base.ModifiedField(auto_now=True, db_index=True),
),
]
3 changes: 3 additions & 0 deletions resolwe/flow/models/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.timezone import now

from resolwe.flow.models.base import BaseModel
from resolwe.permissions.models import Permission, PermissionObject
Expand Down Expand Up @@ -502,6 +503,8 @@ def save(self, *args, **kwargs):
if "label" not in self._value:
self.recompute_label()
super().save(*args, **kwargs)
self.entity.modified = now()
self.entity.save(update_fields=["modified"])

def recompute_label(self):
"""Recompute label from value and set it to the model instance."""
Expand Down
20 changes: 19 additions & 1 deletion resolwe/flow/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
MAX_NAME_LENGTH = 100


class ModifiedField(models.DateTimeField):
"""Custom datetime field.
When attribute 'skip_auto_now' is set to True, the field is not updated in the
pre_save method even if auto_now is set to True. The 'skip_auto_now' is reset
on every save to the default value 'False'.
"""

def pre_save(self, model_instance, add):
"""Prepare field for saving."""
# When skip_auto_now is set do not generate the new value even is auto_now is
# set to True.
if getattr(model_instance, "skip_auto_now", False) is True:
return getattr(model_instance, self.attname)
else:
return super().pre_save(model_instance, add)


class UniqueSlugError(IntegrityError):
"""The error indicates slug collision."""

Expand Down Expand Up @@ -85,7 +103,7 @@ class Meta:
created = models.DateTimeField(auto_now_add=True, db_index=True)

#: modified date and time
modified = models.DateTimeField(auto_now=True, db_index=True)
modified = ModifiedField(auto_now=True, db_index=True)

#: user that created the entry
contributor = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
Expand Down
14 changes: 12 additions & 2 deletions resolwe/flow/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,16 @@ def save(self, render_name=False, *args, **kwargs):

with transaction.atomic():
self._perform_save(*args, **kwargs)
if self.entity:
self.entity.skip_auto_now = True # type: ignore
self.entity.modified = self.modified
self.entity.save(update_fields=["modified"])
self.entity.skip_auto_now = False # type: ignore
elif self.collection:
self.collection.skip_auto_now = True # type: ignore
self.collection.modified = self.modified
self.collection.save(update_fields=["modified"])
self.collection.skip_auto_now = False # type: ignore

self._original_output = self.output

Expand Down Expand Up @@ -621,7 +631,7 @@ def move_to_collection(self, collection):
self.permission_group = collection.permission_group
if collection:
self.tags = collection.tags
self.save(update_fields=["tags", "permission_group", "collection"])
self.save(update_fields=["tags", "permission_group", "collection", "modified"])

@move_to_container
def move_to_entity(self, entity):
Expand All @@ -635,7 +645,7 @@ def move_to_entity(self, entity):
else:
self.permission_group = entity.permission_group
self.tags = entity.tags
self.save(update_fields=["permission_group", "entity", "tags"])
self.save(update_fields=["permission_group", "entity", "tags", "modified"])

def validate_change_collection(self, collection):
"""Raise validation error if data object cannot change collection."""
Expand Down
14 changes: 14 additions & 0 deletions resolwe/flow/models/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,15 @@ def validate_annotations(self):
if validation_errors:
raise ValidationError(validation_errors)

def save(self, *args, **kwargs):
"""Propagate the modified time to the collection."""
super().save(*args, **kwargs)
if self.collection:
self.collection.skip_auto_now = True # type: ignore
self.collection.modified = self.modified
self.collection.save(update_fields=["modified"])
self.collection.skip_auto_now = False # type: ignore


class RelationType(models.Model):
"""Model for storing relation types."""
Expand Down Expand Up @@ -501,6 +510,11 @@ def save(self, *args, **kwargs):
"`descriptor_schema` must be defined if `descriptor` is given"
)
super().save()
if self.collection:
self.collection.skip_auto_now = True # type: ignore
self.collection.modified = self.modified
self.collection.save(update_fields=["modified"])
self.collection.skip_auto_now = False # type: ignore


class RelationPartition(models.Model):
Expand Down
29 changes: 29 additions & 0 deletions resolwe/flow/tests/test_annotations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# pylint: disable=missing-docstring
import time
from typing import Any, Sequence

from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -164,6 +165,34 @@ def test_all_fields_included(self):
self.assertIn(method_name, dir(self))


class TestModifiedPropagated(TestCase):
def test_modified_propagated(self):
"""Test modify propagates to entity."""
entity = Entity.objects.create(contributor=self.contributor)
annotation_group = AnnotationGroup.objects.create(
name="group", label="Annotation group", sort_order=1
)
field = AnnotationField.objects.create(
name="field name",
label="field label",
type=AnnotationType.STRING.value,
sort_order=1,
group=annotation_group,
)

old_modified = entity.modified
time.sleep(0.1)
value = AnnotationValue.objects.create(entity=entity, value="Test", field=field)
entity.refresh_from_db()
self.assertGreater(entity.modified, old_modified)
old_modified = entity.modified
time.sleep(0.1)
value.value = "New value"
value.save()
entity.refresh_from_db()
self.assertGreater(entity.modified, old_modified)


class TestOrderEntityByAnnotations(TestCase):
"""Test ordering Entities by annotation values."""

Expand Down
51 changes: 51 additions & 0 deletions resolwe/flow/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# pylint: disable=missing-docstring,too-many-lines
import os
import shutil
import time
from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock, patch

Expand Down Expand Up @@ -310,6 +311,39 @@ def test_dependencies_list(self):
{d.kind for d in third.parents_dependency.all()}, {DataDependency.KIND_IO}
)

def test_modified_propagated(self):
"""Test modified field is propagated to the entity and collection."""
process = Process.objects.create(contributor=self.contributor)
data = Data.objects.create(contributor=self.contributor, process=process)
collection = Collection.objects.create(contributor=self.contributor)
entity = Entity.objects.create(
contributor=self.contributor, collection=collection
)
old_modified = data.modified
old_collection_modified = collection.modified
data.move_to_collection(collection)
time.sleep(0.1)
data.refresh_from_db()
collection.refresh_from_db()
self.assertGreater(data.modified, old_modified)
self.assertGreater(data.modified, old_collection_modified)
self.assertEqual(collection.modified, data.modified)

data = Data.objects.create(contributor=self.contributor, process=process)
old_modified = data.modified
old_entity_modified = entity.modified
old_collection_modified = collection.modified
time.sleep(0.1)
data.move_to_entity(entity)
data.refresh_from_db()
collection.refresh_from_db()
entity.refresh_from_db()
self.assertGreater(data.modified, old_modified)
self.assertGreater(data.modified, old_collection_modified)
self.assertGreater(data.modified, old_entity_modified)
self.assertEqual(collection.modified, data.modified)
self.assertEqual(entity.modified, data.modified)


class EntityModelTest(TestCase):
def setUp(self):
Expand All @@ -330,6 +364,21 @@ def setUp(self):
name="Test data", contributor=self.contributor, process=self.process
)

def test_modified_propagated(self):
"""Test modified field is propagated to the entity and collection."""
collection = Collection.objects.create(contributor=self.contributor)
entity = Entity.objects.create(
contributor=self.contributor, collection=collection
)
old_entity_modified = entity.modified
entity.name = "New name"
time.sleep(0.1)
entity.save()
entity.refresh_from_db()
collection.refresh_from_db()
self.assertGreater(entity.modified, old_entity_modified)
self.assertEqual(collection.modified, entity.modified)

def test_delete_data(self):
# Create another Data object and add it to the same Entity.
data_2 = Data.objects.create(
Expand Down Expand Up @@ -1728,3 +1777,5 @@ def test_referenced_mix(self):
refs.remove("jsonout.txt")
refs.remove("stdout.txt")
self.assertSetEqual(set(refs), {"file1", "file2"})
self.assertSetEqual(set(refs), {"file1", "file2"})
self.assertSetEqual(set(refs), {"file1", "file2"})
12 changes: 12 additions & 0 deletions resolwe/flow/tests/test_relations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# pylint: disable=missing-docstring
import os
import time

from django.apps import apps
from django.contrib.auth.models import AnonymousUser
Expand Down Expand Up @@ -122,6 +123,17 @@ def setUp(self):
)
self.collection.set_permission(Permission.VIEW, AnonymousUser())

def test_modified_propagated(self):
"""Test modified date propagetes to collection."""
old_modified = self.collection.modified
time.sleep(0.1)
self.relation_group.contributor = self.user
self.relation_group.save()
self.collection.refresh_from_db()
self.relation_group.refresh_from_db()
self.assertGreater(self.collection.modified, old_modified)
self.assertEqual(self.relation_group.modified, self.collection.modified)

def test_prefetch(self):
self.relation_group.delete()
self.relation_series.delete()
Expand Down
Loading

0 comments on commit 3ccd69b

Please sign in to comment.