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()