diff --git a/django/website/data_layer/migrations/0005_message_last_modified.py b/django/website/data_layer/migrations/0005_message_last_modified.py new file mode 100644 index 0000000000000000000000000000000000000000..4570f9d33c61a323c23e5707fb3bb98d32de42c6 --- /dev/null +++ b/django/website/data_layer/migrations/0005_message_last_modified.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import datetime +from django.utils.timezone import utc + + +class Migration(migrations.Migration): + + dependencies = [ + ('data_layer', '0004_auto_20150720_1723'), + ] + + operations = [ + migrations.AddField( + model_name='message', + name='last_modified', + field=models.DateTimeField(default=datetime.datetime(2015, 7, 29, 11, 25, 40, 915975, tzinfo=utc), auto_now=True), + preserve_default=False, + ), + ] diff --git a/django/website/data_layer/models.py b/django/website/data_layer/models.py index fa04cfe55ba847dac221853f1f24ea3953ad2e7d..97fbebfad810f236561d0ad8cdb82da7288227ea 100644 --- a/django/website/data_layer/models.py +++ b/django/website/data_layer/models.py @@ -1,13 +1,21 @@ from django.db import models +from django.dispatch.dispatcher import receiver +from django.utils import timezone + from taxonomies.models import Term class DataLayerModel(models.Model): created = models.DateTimeField(auto_now_add=True) + last_modified = models.DateTimeField(auto_now=True) class Meta: abstract = True + def note_external_modification(self): + # This will set the last_modified field + self.save() + class Message(DataLayerModel): body = models.TextField() @@ -42,3 +50,18 @@ class Message(DataLayerModel): # TODO: rename this class Item = Message + + +@receiver(models.signals.m2m_changed, sender=Item.terms.through, + dispatch_uid="data_layer.models.terms_signal_handler") +def terms_signal_handler(sender, **kwargs): + if kwargs.get('action') not in ('post_add', 'post_remove'): + return + + if kwargs.get('reverse'): + items = Item.objects.filter(pk__in=kwargs.get('pk_set')) + else: + items = [kwargs.get('instance')] + + for item in items: + item.note_external_modification() diff --git a/django/website/data_layer/tests/item_tests.py b/django/website/data_layer/tests/item_tests.py index d69158a868a73a76af6c9870ca0af1f9f6bfb6ea..404aa938106715f743f32da04a70393d9cac4797 100644 --- a/django/website/data_layer/tests/item_tests.py +++ b/django/website/data_layer/tests/item_tests.py @@ -1,9 +1,121 @@ import pytest +import datetime +from mock import patch, MagicMock -from .factories import ItemFactory +from django.utils import timezone + +from factories import ItemFactory +from ..models import Item from taxonomies.tests.factories import TermFactory, TaxonomyFactory +def last_modified(item): + return Item.objects.get(id=item.id).last_modified + + +# Ensure value of "now" always increases by amount sufficient +# to show up as a change, even if db resolution for datetime +# is one second. +def time_granularity(): + return datetime.timedelta(hours=1) + + +def now_iter(start): + t = start + while True: + t += time_granularity() + yield t + + +def num_updates(old_time, new_time): + elapsed_time = new_time - old_time + + return elapsed_time.seconds / time_granularity().seconds + + +@pytest.fixture +def item(): + return ItemFactory() + + +@pytest.fixture +def mock_time_now(): + return MagicMock(wraps=timezone.now, + side_effect=now_iter(timezone.now())) + + +@pytest.mark.django_db +def test_last_modified_date_updates_on_body_change(item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + orig_last_modified = last_modified(item) + item.body = 'replacement text' + item.save() + + assert num_updates(orig_last_modified, last_modified(item)) == 1 + + +@pytest.mark.django_db +def test_last_modified_date_updates_on_item_category_add( + item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + orig_last_modified = last_modified(item) + term = TermFactory() + item.terms.add(term) + + assert num_updates(orig_last_modified, last_modified(item)) == 1 + + +@pytest.mark.django_db +def test_last_modified_date_updates_on_item_category_delete( + item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + term = TermFactory() + item.terms.add(term) + orig_last_modified = last_modified(item) + item.terms.remove(term) + + assert num_updates(orig_last_modified, last_modified(item)) == 1 + + +@pytest.mark.django_db +def test_last_modified_date_updates_on_category_item_add( + item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + orig_last_modified = last_modified(item) + term = TermFactory() + term.items.add(item) + + assert num_updates(orig_last_modified, last_modified(item)) == 1 + + +@pytest.mark.django_db +def test_last_modified_date_updates_on_category_item_delete( + item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + term = TermFactory() + term.items.add(item) + orig_last_modified = last_modified(item) + term.items.remove(item) + + assert num_updates(orig_last_modified, last_modified(item)) == 1 + + +@pytest.mark.django_db +def test_last_modified_date_on_other_item_not_updated( + item, mock_time_now): + with patch('django.utils.timezone.now', new=mock_time_now): + term = TermFactory() + term.items.add(item) + + other_item = ItemFactory() + term.items.add(other_item) + + orig_last_modified = last_modified(other_item) + term.items.remove(item) + + assert num_updates(orig_last_modified, last_modified(other_item)) == 0 + + @pytest.mark.django_db def test_apply_term_replaces_term_for_categories(): item = ItemFactory()