Repository: mozmeao/snippets-service

Issue:
Schedule snippets at least 5 minutes ahead of now() to allow undo
When snippets are set to publish now, automatically set the publication time to `now()+5'` to allow Editors to undo publication if they spot an error.


Discussion:

Resolving patch:
diff --git a/snippets/base/models.py b/snippets/base/models.py
--- a/snippets/base/models.py
+++ b/snippets/base/models.py
@@ -7,7 +7,7 @@
 import uuid
 import subprocess
 from collections import namedtuple
-from datetime import datetime
+from datetime import datetime, timedelta
 from urllib.parse import urljoin, urlparse
 
 from django.conf import settings
@@ -1680,6 +1680,10 @@ def __str__(self):
     def clean(self):
         super().clean()
 
+        self.publish_start = max(
+            datetime.utcnow() + timedelta(minutes=settings.SNIPPETS_PUBLICATION_OFFSET),
+            self.publish_start or datetime.utcnow())
+
         if ((all([self.publish_start, self.publish_end]) and
              self.publish_start >= self.publish_end)):
             raise ValidationError('Publish start must come before publish end.')
diff --git a/snippets/settings.py b/snippets/settings.py
--- a/snippets/settings.py
+++ b/snippets/settings.py
@@ -381,3 +381,6 @@
 IMAGE_MAX_SIZE = config('IMAGE_MAX_SIZE', default=40960, cast=int)
 
 TAGGIT_CASE_INSENSITIVE = True
+
+# In minutes
+SNIPPETS_PUBLICATION_OFFSET = config('SNIPPETS_PUBLICATION_OFFSET', default=5, cast=int)


Test patch:
diff --git a/snippets/base/tests/test_models.py b/snippets/base/tests/test_models.py
--- a/snippets/base/tests/test_models.py
+++ b/snippets/base/tests/test_models.py
@@ -667,16 +667,41 @@ def test_channels(self):
         self.assertTrue(job.channels, set(['release', 'beta', 'nightly']))
 
     def test_clean(self):
-        job_clean = JobFactory.create(publish_start=datetime.utcnow() - timedelta(days=1),
-                                      publish_end=datetime.utcnow())
+        job_clean = JobFactory.create(publish_start=datetime.utcnow() + timedelta(days=1),
+                                      publish_end=datetime.utcnow() + timedelta(days=2))
         job_clean.clean()
 
-        job_dirty = JobFactory.create(publish_start=datetime.utcnow(),
-                                      publish_end=datetime.utcnow() - timedelta(days=1))
+        job_dirty = JobFactory.create(publish_start=datetime.utcnow() + timedelta(days=3),
+                                      publish_end=datetime.utcnow() + timedelta(days=2))
 
         self.assertRaisesMessage(
             ValidationError, 'Publish start must come before publish end.', job_dirty.clean)
 
+    @override_settings(SNIPPETS_PUBLICATION_OFFSET=5)
+    def test_clean_publication_offset(self):
+        utcnow = datetime.utcnow()
+
+        job_no_publish_start = JobFactory.create(publish_start=None, publish_end=None)
+        with patch('snippets.base.models.datetime') as datetime_mock:
+            datetime_mock.utcnow.return_value = utcnow
+            job_no_publish_start.clean()
+        self.assertEqual(job_no_publish_start.publish_start, utcnow + timedelta(minutes=5))
+
+        job_publish_start_distant_future = JobFactory.create(
+            publish_start=utcnow + timedelta(days=5), publish_end=None)
+        with patch('snippets.base.models.datetime') as datetime_mock:
+            datetime_mock.utcnow.return_value = utcnow
+            job_publish_start_distant_future.clean()
+        self.assertEqual(job_publish_start_distant_future.publish_start, utcnow + timedelta(days=5))
+
+        job_publish_start_now = JobFactory.create(
+            publish_start=utcnow, publish_end=None)
+
+        with patch('snippets.base.models.datetime') as datetime_mock:
+            datetime_mock.utcnow.return_value = utcnow
+            job_publish_start_now.clean()
+        self.assertEqual(job_publish_start_now.publish_start, utcnow + timedelta(minutes=5))
+
     def test_render(self):
         self.maxDiff = None
         job = JobFactory.create(